{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# ๐ŸŽฏ [WEL03] ๊ฐ€์ƒ ์ˆ˜์งˆ๊ฒ€์‚ฌ ๋ฐ์ดํ„ฐ ์œ„ํ—˜ ์‹ ํ˜ธ ๋Œ€์‹œ๋ณด๋“œ\n", "### Google Colab ์‹ค์Šต ํ™˜๊ฒฝ (์žฅ๋ณ‘ ๋ณต์ง€ยท์ง€์› / Level 2)\n", "\n", "[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/edunlab/milicontent2/blob/main/uploads/WEL03/WEL03_solution.ipynb)\n", "\n", "---\n", "\n", "## ๐Ÿ“Œ 1. ํ”„๋กœ์ ํŠธ ๊ฐœ์š” ๋ฐ ๋ฐฐ๊ฒฝ\n", "\n", "| ํ•ญ๋ชฉ | ๋‚ด์šฉ |\n", "| :--- | :--- |\n", "| **๊ณผ์ • ๋ถ„๋ฅ˜** | ์žฅ๋ณ‘ ๋ณต์ง€ยท์ง€์› (Level 2) |\n", "| **ํ•™์Šต ์†Œ์š” ์‹œ๊ฐ„** | 10๊ฐœ ๋ฏธ์…˜ (5์ผ / ์ด 27.5์‹œ๊ฐ„) |\n", "| **ํ™˜๊ฒฝ ์‚ฌ์–‘** | Python 3.10+, numpy, pandas, matplotlib, scipy, scikit-learn |\n", "\n", "### ๐Ÿ“– ํ”„๋กœ์ ํŠธ ๋ชฉํ‘œ\n", "์ „์žฅ์—์„œ๋Š” ๊ธ‰์ˆ˜์ฐจ, ์ •์ˆ˜ ์žฅ๋น„, ์šฐ๋ฌผ, ํ•˜์ฒœ ์ทจ์ˆ˜์› ๋“ฑ ๊ธ‰์ˆ˜์›์˜ ์ˆ˜์งˆ ์ด์ƒ์„ ๋นจ๋ฆฌ ์•Œ์•„์ฑ„์ง€ ๋ชปํ•˜๋ฉด ์žฅ๋ณ‘์˜ ๊ฑด๊ฐ•๊ณผ ๋ถ€๋Œ€ ํ™œ๋™์— ๋ฐ”๋กœ ์˜ํ–ฅ์„ ์ฃผ์ง€๋งŒ, ์„ผ์„œ ๊ฐ’์€ ์žก์Œ๊ณผ ๊ณ ์žฅ์ด ๋งŽ์•„ ์˜ค๊ฒฝ๋ณด๋กœ ๋ถˆํ•„์š”ํ•˜๊ฒŒ ๊ธ‰์ˆ˜๋ฅผ ๋Š๋Š” ์ผ๋„ ์ƒ๊ธด๋‹ค. ๋Ÿฌ์‹œ์•„-์šฐํฌ๋ผ์ด๋‚˜ ์ „์Ÿ ์‚ฌ๋ก€์—์„œ ํ•™์Šต ์ดˆ์ ์œผ๋กœ ์‚ผ์€ ์ „์žฅ ์œ„์ƒยท๊ฑด๊ฐ• ์œ„ํ—˜ ๊ด€๋ฆฌ๋ฅผ ๋ฐ˜์˜ํ•ด, ๋ณธ ํ”„๋กœ์ ํŠธ๋Š” ๊ฐ€์ƒ ์ˆ˜์งˆ๊ฒ€์‚ฌ ๋ฐ์ดํ„ฐ๋ฅผ ๋ถ„์„ํ•ด ์œ„ํ—˜ ์‹ ํ˜ธ๋ฅผ ๋ฐ์ดํ„ฐ๋กœ ํŒ๋‹จํ•˜๋Š” ๊ณผ์ œ๋ฅผ ๋‹ค๋ฃฌ๋‹ค. ๊ต์œก์šฉ ๊ฐ€์ƒ ๊ธ‰์ˆ˜์› 8๊ณณ์˜ pH, ํƒ๋„, ์ž”๋ฅ˜์—ผ์†Œ, ์ค‘๊ธˆ์†, ๋Œ€์žฅ๊ท  ์ธก์ • ์ž๋ฃŒ๋ฅผ ์ฝ”๋“œ๋กœ ๋งŒ๋“ค์–ด ์Šคํ‚ค๋งˆ ์ •์˜, ์„ผ์„œ ๊ฒฐ์ธกยท์ด์ƒ์น˜ ์ •์ œ, ๊ธฐ์ค€์น˜ Baseline ํŒ์ •, ๋ณตํ•ฉ ์˜ค์—ผ ์ง•ํ›„ ๋ถ„๋ฅ˜ ๋ชจ๋ธ, ์‹ ๋ขฐ๋„ ์ง€์ˆ˜์™€ ์˜ค๊ฒฝ๋ณด ๊ฐ์ถ•, ์ƒํ™ฉํŒ ๋Œ€์‹œ๋ณด๋“œ, ๊ธ‰์ˆ˜ ์ค‘๋‹จ HITL ๊ฒฐ์žฌ์„ , ๋Œ€๋Ÿ‰ ์˜ค์—ผ ๋ถ€ํ•˜ ์‹œํ—˜, AAR ์ž„๊ณ„์น˜ ์žฌ์กฐ์ •, ํ†ตํ•ฉ ํŒจํ‚ค์ง€๊นŒ์ง€ ๋‹จ๊ณ„๋ณ„๋กœ ๊ตฌํ˜„ํ•œ๋‹ค. ์ตœ์ข… ๋ชฉํ‘œ๋Š” ์ˆ˜์งˆ ์œ„ํ—˜ ์‹ ํ˜ธ๋ฅผ ๊ทผ๊ฑฐ์™€ ํ•จ๊ป˜ ๋น ๋ฅด๊ฒŒ ์ œ์‹œํ•˜๋˜, ์‹ค์ œ ์Œ์šฉ ํŒ์ •๊ณผ ๊ธ‰์ˆ˜ ์ค‘๋‹จ์€ ๊ณต์‹ ๊ธฐ์ค€๊ณผ ์˜๋ฌด(์˜ˆ๋ฐฉ์˜ํ•™) ๋‹ด๋‹น ํ™•์ธ, ์ง€ํœ˜๊ด€ ๊ฒฐ์‹ฌ์— ๋”ฐ๋ฅด๋„๋ก ์ง€์›ํ•˜๋Š” ๋„๊ตฌ๋ฅผ ์™„์„ฑํ•˜๋Š” ๊ฒƒ์ด๋‹ค.\n", "\n", "> **โš ๏ธ [๊ต์œก์šฉ ๊ฐ€์ƒ ๋ฐ์ดํ„ฐ์…‹ ์›์น™]**\n", "> - ๋ณธ ๋…ธํŠธ๋ถ์˜ ๋ชจ๋“  ๋ฐ์ดํ„ฐ(์ œ์›, ๋กœ๊ทธ, ์‹œ๋‚˜๋ฆฌ์˜ค ๋“ฑ)๋Š” ๋ณด์•ˆ ๊ทœ์ •์„ ์ค€์ˆ˜ํ•˜์—ฌ **์ฝ”๋“œ ๋‚ด ๋‚œ์ˆ˜ ๊ธฐ๋ฐ˜ ๊ฐ€์ƒ ๋ฐ์ดํ„ฐ๋กœ ์ƒ์„ฑ**๋ฉ๋‹ˆ๋‹ค.\n", "> - ์‹ค์ œ ๋ถ€๋Œ€ ์œ„์น˜, ์ž‘์ „ ์ธ๊ฐ€, ์‹ค์ œ ์ „์žฅ ์ •๋ณด, ๊ตฐ์‚ฌ ๊ธฐ๋ฐ€ ๋ฐ์ดํ„ฐ๋Š” ์ผ์ฒด ํฌํ•จ๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.\n", "\n", "---\n", "\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# ============================================================================\n", "# [Google Colab ํ™˜๊ฒฝ ์„ค์ •] ํ•„์ˆ˜ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์„ค์น˜ ๋ฐ ์ž„ํฌํŠธ\n", "# ============================================================================\n", "!pip install -q numpy pandas matplotlib scipy scikit-learn\n", "\n", "import os\n", "import sys\n", "import time\n", "import math\n", "import json\n", "import re\n", "import numpy as np\n", "import pandas as pd\n", "import matplotlib.pyplot as plt\n", "import warnings\n", "warnings.filterwarnings('ignore')\n", "\n", "# ๊ทธ๋ž˜ํ”„ ์˜๋ฌธ ํฐํŠธ ๊ธฐ๋ณธ ์„ค์ • (ํ™˜๊ฒฝ ๋…๋ฆฝ์„ฑ ๋ณด์žฅ)\n", "plt.rcParams['font.sans-serif'] = 'DejaVu Sans'\n", "plt.rcParams['axes.unicode_minus'] = False\n", "\n", "print(f\"์‹คํ–‰ ํ™˜๊ฒฝ ์ค€๋น„ ์™„๋ฃŒ (Python {sys.version.split()[0]})\")\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "---\n", "\n", "## ๐ŸŽฏ ๋ฏธ์…˜ 01. ์•ผ์ „ ๊ธ‰์ˆ˜์› ๊ฐ€์ƒ ์ˆ˜์งˆ๊ฒ€์‚ฌ ํ•ญ๋ชฉ(pH, ํƒ๋„, ์ž”๋ฅ˜์—ผ์†Œ, ์ค‘๊ธˆ์†, ๋Œ€์žฅ๊ท ) ๋ฐ์ดํ„ฐ์…‹ ์ •์˜\n", "**์†Œ์š”์‹œ๊ฐ„**: ์•ฝ 1.5์‹œ๊ฐ„\n", "\n", "### ๐Ÿ“‹ ๋ฏธ์…˜ ๋ฐฐ๊ฒฝ ๋ฐ ์ˆ˜ํ–‰ ์ง€์นจ\n", "#### ๋ฏธ์…˜ ๋ฐฐ๊ฒฝ ๋ฐ ๋‹น์œ„์„ฑ\n", "\n", "์•ผ์ „ ๊ธ‰์ˆ˜์› ๊ฐ€์ƒ ์ˆ˜์งˆ๊ฒ€์‚ฌ ํ•ญ๋ชฉ(pH, ํƒ๋„, ์ž”๋ฅ˜์—ผ์†Œ, ์ค‘๊ธˆ์†) ๋ฐ์ดํ„ฐ์…‹์„ ์ •์˜ํ•œ๋‹ค. ์‹ค์ œ ์ „์ˆ  ํ˜„์žฅ์—์„œ๋Š” ๋‹จ์ˆœ ํƒ์ง€์— ๊ทธ์น˜์ง€ ์•Š๊ณ  ์‹ ์†ํ•œ ์œ„ํ˜‘ ํŒ๋‹จ๊ณผ ์‹ ๋ขฐ๋„ ๋†’์€ ์˜์‚ฌ๊ฒฐ์ •์ด ํ•„์ˆ˜์ ์ด๋ฏ€๋กœ, **์•ผ์ „ ๊ธ‰์ˆ˜์› ๊ฐ€์ƒ ์ˆ˜์งˆ๊ฒ€์‚ฌ ํ•ญ๋ชฉ(pH, ํƒ๋„, ์ž”๋ฅ˜์—ผ์†Œ, ์ค‘๊ธˆ์†, ๋Œ€์žฅ๊ท )๊ณผ ์„ผ์„œยท์ „์ˆ  ํ™˜๊ฒฝ ์ •๋ณด์˜ ์Šคํ‚ค๋งˆ์™€ ๋ฐ์ดํ„ฐ ๋ช…์„ธ๋ฅผ ์ •์˜ํ•œ๋‹ค.** ๊ณผ์—…์„ ์™„์ˆ˜ํ•˜์—ฌ ์˜คํŒ์„ ๋ฐฉ์ง€ํ•˜๊ณ  ์ž‘์ „ ๋Œ€์‘๋ ฅ์„ ๊ทน๋Œ€ํ™”ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.\n", "\n", "์ด ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด ๋ณธ ๋ฏธ์…˜์—์„œ๋Š” **์ธก์ • ๋ ˆ์ฝ”๋“œ๋ฅผ ๊ธ‰์ˆ˜์› ์ •๋ณด, ์ˆ˜์งˆ 5ํ•ญ๋ชฉ, ์„ผ์„œ ์ƒํƒœ, ์ „์ˆ  ํŒŒ๋ผ๋ฏธํ„ฐ(๊ธฐ์ƒ, ์œ„์น˜ ์˜ค์ฐจ, ํ†ต์‹  ์ง€์—ฐ) ๋„ค ๋ฌถ์Œ 15๊ฐœ ํ•„๋“œ๋กœ ์ •์˜ํ•˜๊ณ  ์ž๋ฃŒํ˜•ยท๋‹จ์œ„ยทํ—ˆ์šฉ ๋ฒ”์œ„ยทํ˜•์‹ ๊ทœ์น™์„ ๋‘”๋‹ค ๊ณผ์ •์„ ์ง„ํ–‰ํ•˜๋Š”** ์‹ค์Šต์„ ์ฒด๊ณ„์ ์œผ๋กœ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค.\n", "\n", "ํ•™์Šต์ž๋Š” ์‹ค์Šต๊ฐ€์ด๋“œ์— ์ œ์‹œ๋œ ์ด 4๊ฐœ ์„ธ๋ถ€ ํ™œ๋™(๊ธฐ๋ณธ ๊ธฐ๋Šฅ ๊ตฌํ˜„ โž” ๊ฒฐ๊ณผ ์ง€ํ‘œ ๊ฒ€์ฆ โž” ํŒŒ๋ผ๋ฏธํ„ฐ ํŠœ๋‹ ๋ฐ ๋„์ „๊ณผ์ œ)์„ ์ˆœ์ฐจ์ ์œผ๋กœ ์ง„ํ–‰ํ•˜๋ฉฐ ํ•ต์‹ฌ ํŒ์ • ๊ธฐ์ค€์„ ๋‹ฌ์„ฑํ•ฉ๋‹ˆ๋‹ค.\n", "\n", "**์ด๋ฒˆ ๊ฒฐ๊ณผ๋ฌผ:** `data/schema/wel03_input_schema.json ๋ฐ docs/wel03_data_spec.md` ํ˜•ํƒœ์˜ ๊ฒฐ๊ณผ๋ฌผ์„ ์™„์„ฑํ•œ๋‹ค.\n", "\n", "### ๐Ÿ” ํ‰๊ฐ€ ๋ฐ ๊ฒ€์ฆ ๊ธฐ์ค€ (PASS ์กฐ๊ฑด)\n", "#### ๋ฏธ์…˜ ํŒ์ •\n", "\n", "**ํ‰๊ฐ€ ๊ตฌ๋ถ„:** ํ•„์ˆ˜\n", "\n", "**ํ•ต์‹ฌ ํ‰๊ฐ€์˜์—ญ:** ์žฅ๋ณ‘ ๋ณต์ง€ยท์ง€์› ์‹ค๋ฌด ๋ฐ ๋Ÿฌ์‹œ์•„-์šฐํฌ๋ผ์ด๋‚˜ ์ „์Ÿ ์ „์žฅ ํ™˜๊ฒฝ๊ณผ์˜ ๊ตฐ์‚ฌ์  ๋ถ€ํ•ฉ์„ฑ\n", "\n", "#### ๋ฏธ์…˜ PASS ์กฐ๊ฑด\n", "\n", "- ์ •๋Ÿ‰ ๊ธฐ์ค€: ์Šคํ‚ค๋งˆ ํ•„๋“œ ์ •์˜ ์™„์„ฑ๋„ 100%, ์ƒ˜ํ”Œ ๋ฐ์ดํ„ฐ ๊ฒ€์ฆ ์˜ค๋ฅ˜ 0๊ฑด\n", "\n", "\n", "- ์ง„ํ–‰ ๊ธฐ์ค€: ์ž…๋ ฅ ๋ฐ์ดํ„ฐ ์œ ํšจ์„ฑ ๊ฒ€์ฆ ์Šคํฌ๋ฆฝํŠธ ํ†ต๊ณผ ๋ฐ ๋ช…์„ธ์„œ ๋ฌธ์„œํ™” ์™„๋ฃŒ\n", "\n", "\n", "- ๊ฒ€์ฆ ๋ฐฉ๋ฒ•: JSON Schema ๊ฒ€์ฆ ๋„๊ตฌ ๋ฐ ์ƒ˜ํ”Œ ๋ฐ์ดํ„ฐ ํŒŒ์‹ฑ ๋‹จ์œ„ ํ…Œ์ŠคํŠธ\n", "\n", "\n", "#### ์ฑ„์ ์ž ํŒ์ • ๋ฉ”๋ชจ\n", "\n", "> ๋‹จ์ˆœ ์ฝ”๋“œ ์‹คํ–‰ ์—ฌ๋ถ€๋ฟ๋งŒ ์•„๋‹ˆ๋ผ ์ถœ๋ ฅ๋œ ์ˆ˜์น˜์™€ ๊ทผ๊ฑฐ ์ž๋ฃŒ๊ฐ€ ๊ตฐ ์‹ค๋ฌด ์š”๊ตฌ์‚ฌํ•ญ์— ๋ถ€ํ•ฉํ•˜๋Š”์ง€ ๋ฉด๋ฐ€ํžˆ ํ™•์ธํ•œ๋‹ค.\n", "\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "\"\"\"์•ผ์ „ ๊ธ‰์ˆ˜์› ๊ฐ€์ƒ ์ˆ˜์งˆ๊ฒ€์‚ฌ ํ•ญ๋ชฉ(pH, ํƒ๋„, ์ž”๋ฅ˜์—ผ์†Œ, ์ค‘๊ธˆ์†, ๋Œ€์žฅ๊ท )๊ณผ ๊ธ‰์ˆ˜์›ยท์„ผ์„œยท์ „์ˆ  ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ๋‹ด์€ ์ž…๋ ฅ ๋ฐ์ดํ„ฐ ์Šคํ‚ค๋งˆ๋ฅผ ์ •์˜ํ•˜๊ณ , ๊ฐ€์ƒ ์ƒ˜ํ”Œ 20๊ฑด๊ณผ ์˜ค๋ฅ˜ ์ฃผ์ž… ๋ฐ์ดํ„ฐ๋กœ ๊ฒ€์ฆ๊ธฐ๋ฅผ ์‹œํ—˜ํ•œ๋‹ค.\"\"\"\n", "import json\n", "import re\n", "import numpy as np\n", "import pandas as pd\n", "\n", "# ---------------------------------------------------------------------------\n", "# [๊ต์œก์šฉ๊ฐ€์ƒ์ž๋ฃŒ] ์•ผ์ „ ๊ธ‰์ˆ˜์› ๊ฐ€์ƒ ์ˆ˜์งˆ ์„ผ์„œ ๋ฐ์ดํ„ฐ ์ƒ์„ฑ๊ธฐ (์‹ค์ œ ๋ถ€๋Œ€ยท์œ„์น˜ยท๊ฒ€์‚ฌ ๊ฒฐ๊ณผ ์—†์Œ)\n", "# ๊ฐ€์ƒ ๊ธ‰์ˆ˜์› 8๊ณณ x 14์ผ x 1์‹œ๊ฐ„ ๊ฐ„๊ฒฉ. ์‹ค์ œ ์ˆ˜์งˆ(true_*)ยท์„ผ์„œ ์ธก์ •๊ฐ’ยท์˜ค์—ผ ์‚ฌ๊ฑดยท์„ผ์„œ ๊ณ ์žฅ ์ •๋‹ต์„ ํ•จ๊ป˜ ๋งŒ๋“ ๋‹ค.\n", "# ๊ธฐ์ค€์น˜๋Š” ๊ต์œก์šฉ ๊ฐ€์ •๊ฐ’์ด๋ฉฐ, ์‹ค์ œ ์Œ์šฉ ํŒ์ •์€ ๊ณต์ธ ๊ฒ€์‚ฌ ๊ฒฐ๊ณผ์™€ ์˜๋ฌด(์˜ˆ๋ฐฉ์˜ํ•™) ๋‹ด๋‹น ๊ธฐ์ค€์— ๋”ฐ๋ฅธ๋‹ค.\n", "# ---------------------------------------------------------------------------\n", "PARAMS = [\"ph\", \"turbidity\", \"chlorine\", \"metal\", \"ecoli\"]\n", "SOURCES = [(\"W01\", \"water_truck\", 1.0, 2.0), (\"W02\", \"water_truck\", 3.5, 1.2), (\"W03\", \"purification_unit\", 2.2, 4.1),\n", " (\"W04\", \"purification_unit\", 5.0, 3.3), (\"W05\", \"well\", 0.8, 5.6), (\"W06\", \"well\", 4.4, 6.0),\n", " (\"W07\", \"river_intake\", 6.3, 1.8), (\"W08\", \"river_intake\", 6.9, 4.9)]\n", "BASE = {\"water_truck\": (7.2, 0.25, 0.75, 1.5), \"purification_unit\": (7.0, 0.20, 0.85, 1.2),\n", " \"well\": (6.8, 0.45, 0.50, 3.0), \"river_intake\": (7.3, 0.55, 0.60, 2.0)} # pH, ํƒ๋„(NTU), ์ž”๋ฅ˜์—ผ์†Œ(mg/L), ์ค‘๊ธˆ์†(ug/L)\n", "LIMITS = {\"ph\": (5.8, 8.5), \"turbidity\": (1.0, 2.0), \"chlorine\": (0.2, 0.1), \"metal\": (5.0, 10.0), \"ecoli\": (1, 1)} # (์ฃผ์˜, ์œ„ํ—˜) ๊ฐ€์ • ๊ธฐ์ค€\n", "EVENT_TYPES = { # ์˜ค์—ผ ์‚ฌ๊ฑด: ๋ณ€์ˆ˜๋ณ„ ์ตœ๋Œ€ ๋ณ€ํ™”๋Ÿ‰, ์ตœ๋Œ€์น˜ ๋„๋‹ฌ ์‹œ๊ฐ„(h)\n", " \"sewage\": ({\"turbidity\": 2.8, \"chlorine\": -0.5, \"ecoli\": 30, \"ph\": -0.3}, 4),\n", " \"metal\": ({\"metal\": 22.0, \"ph\": -0.5}, 4),\n", " \"disinfect_fail\": ({\"chlorine\": -0.8, \"ecoli\": 6}, 6),\n", " \"chemical\": ({\"ph\": 1.7, \"turbidity\": 1.0}, 3),\n", " \"creeping_metal\": ({\"metal\": 13.0}, 18),\n", "}\n", "FAULT_TYPES = [\"stuck\", \"spike\", \"drift\", \"dropout\"]\n", "NOISE = {\"ph\": 0.04, \"turbidity\": 0.06, \"chlorine\": 0.03, \"metal\": 0.35}\n", "DIGITS = {\"ph\": 2, \"turbidity\": 2, \"chlorine\": 2, \"metal\": 1}\n", "\n", "\n", "def make_dataset(days=14, seed=7, n_events=6, n_faults=10, events=None, faults=None, sources=None):\n", " \"\"\"events: [(๊ธ‰์ˆ˜์› ๋ฒˆํ˜ธ, ์‚ฌ๊ฑด ์œ ํ˜•, ์‹œ์ž‘ ์‹œ๊ฐ h, ์ง€์† h, ๊ฐ•๋„)], faults: [(๊ธ‰์ˆ˜์› ๋ฒˆํ˜ธ, ๊ณ ์žฅ ์œ ํ˜•, ํ•ญ๋ชฉ, ์‹œ์ž‘ h, ์ง€์† h)]\"\"\"\n", " rng = np.random.default_rng(seed)\n", " sources = sources or SOURCES\n", " T = days * 24\n", " tt = np.arange(T)\n", " hour = tt % 24\n", " night = (hour < 6) | (hour >= 20)\n", " rain = np.zeros(T)\n", " for _ in range(max(1, days // 4)):\n", " s0, d = int(rng.integers(0, T - 14)), int(rng.integers(6, 14))\n", " rain[s0:s0 + d] = np.maximum(rain[s0:s0 + d], rng.uniform(0.6, 1.0))\n", " if events is None:\n", " events = [(int(rng.integers(len(sources))), str(rng.choice(list(EVENT_TYPES))), int(rng.integers(min(24, T // 4), max(T // 4 + 1, T - 48))),\n", " int(rng.integers(10, 30)), 1.0) for _ in range(n_events)]\n", " if faults is None:\n", " faults = [(int(rng.integers(len(sources))), str(rng.choice(FAULT_TYPES)), str(rng.choice([\"ph\", \"turbidity\", \"chlorine\", \"metal\"])),\n", " int(rng.integers(12, max(13, T - 30))), int(rng.integers(6, 24))) for _ in range(n_faults)]\n", " out = []\n", " for si, (sid, stype, x, y) in enumerate(sources):\n", " ph0, tb0, cl0, mt0 = BASE[stype]\n", " tr = {\"ph\": ph0 + 0.08 * np.sin(2 * np.pi * hour / 24) + rng.normal(0, 0.03, T),\n", " \"turbidity\": tb0 * (1 + 0.1 * rng.normal(0, 1, T)),\n", " \"chlorine\": (cl0 * np.exp(-0.022 * hour) if stype == \"water_truck\" else cl0 + rng.normal(0, 0.03, T)),\n", " \"metal\": mt0 + rng.normal(0, 0.2, T), \"ecoli\": np.zeros(T)}\n", " if stype in (\"river_intake\", \"well\"):\n", " tr[\"turbidity\"] = tr[\"turbidity\"] + rain * (0.9 if stype == \"river_intake\" else 0.45)\n", " f_all, etype = np.zeros(T), np.array([\"none\"] * T, dtype=object)\n", " for (s_i, et, onset, dur, scale) in events:\n", " if s_i != si:\n", " continue\n", " delta, ramp = EVENT_TYPES[et]\n", " f = np.clip((tt - onset + 1) / ramp, 0, 1) * ((tt >= onset) & (tt < onset + dur + ramp))\n", " for k, v in delta.items():\n", " tr[k] = tr[k] + v * f * scale\n", " etype[f > 0] = et\n", " f_all = np.maximum(f_all, f)\n", " tr[\"chlorine\"] = np.clip(tr[\"chlorine\"], 0.02, None)\n", " tr[\"turbidity\"] = np.clip(tr[\"turbidity\"], 0.05, None)\n", " tr[\"metal\"] = np.clip(tr[\"metal\"], 0.1, None)\n", " grade = np.where(f_all >= 0.2, 2, 0)\n", " mild = (tr[\"turbidity\"] > LIMITS[\"turbidity\"][0]) | (tr[\"chlorine\"] < LIMITS[\"chlorine\"][0]) | (tr[\"metal\"] > LIMITS[\"metal\"][0])\n", " grade = np.where((grade == 0) & mild, 1, grade)\n", " ms = {}\n", " for p in [\"ph\", \"turbidity\", \"chlorine\", \"metal\"]:\n", " sd = NOISE[p] * np.where(night, 1.5, 1.0) * (tr[p] if p == \"turbidity\" else 1)\n", " ms[p] = np.round(np.clip(tr[p] + rng.normal(0, 1, T) * sd, 0, None), DIGITS[p])\n", " ms[\"ecoli\"] = (rng.poisson(tr[\"ecoli\"]) + (rng.random(T) < 0.004)).astype(float)\n", " fault, fparam = np.array([\"none\"] * T, dtype=object), np.array([\"-\"] * T, dtype=object)\n", " for (s_i, ft, p, s0, d) in faults:\n", " if s_i != si:\n", " continue\n", " seg = slice(s0, min(T, s0 + d))\n", " if ft == \"stuck\":\n", " ms[p][seg] = ms[p][s0]\n", " elif ft == \"spike\":\n", " seg = slice(s0, s0 + 1)\n", " ms[p][s0] = {\"ph\": 11.8, \"turbidity\": 38.0, \"chlorine\": 0.0, \"metal\": 75.0}[p]\n", " elif ft == \"drift\":\n", " ramp = np.linspace(0, 1, seg.stop - s0)\n", " ms[p][seg] = np.clip(ms[p][seg] + ramp * {\"ph\": -1.6, \"turbidity\": 2.6, \"chlorine\": -0.75, \"metal\": 12.0}[p], 0, None).round(DIGITS[p])\n", " elif ft == \"dropout\":\n", " for q in PARAMS:\n", " ms[q][seg] = np.nan\n", " p = \"all\"\n", " fault[seg], fparam[seg] = ft, p\n", " df = pd.DataFrame({\"source_id\": sid, \"source_type\": stype, \"x_km\": x, \"y_km\": y, \"t\": tt,\n", " \"cond\": np.where(rain > 0, \"storm\", np.where(night, \"night\", \"day\")),\n", " **{p: ms[p] for p in PARAMS}, **{f\"true_{p}\": tr[p] for p in PARAMS},\n", " \"event\": etype, \"fault\": fault, \"fault_param\": fparam, \"grade\": grade})\n", " out.append(df)\n", " return pd.concat(out, ignore_index=True)\n", "# ---------------------------------------------------------------------------\n", "\n", "# 1) ์Šคํ‚ค๋งˆ ์ •์˜: ๊ธ‰์ˆ˜์› ์ •๋ณด, ์ˆ˜์งˆ ์ธก์ •๊ฐ’(5ํ•ญ๋ชฉ), ์„ผ์„œ ์ƒํƒœ, ์ „์ˆ  ํŒŒ๋ผ๋ฏธํ„ฐ(๊ธฐ์ƒยท์œ„์น˜์˜ค์ฐจยทํ†ต์‹ ์ง€์—ฐ)\n", "SCHEMA = {\n", " \"required\": [\"record_id\", \"source\", \"measured_at\", \"quality\", \"sensor\", \"tactical\"],\n", " \"fields\": {\n", " \"record_id\": {\"type\": str, \"pattern\": r\"^WQ-W\\d{2}-\\d{4}$\"},\n", " \"measured_at\": {\"type\": str, \"pattern\": r\"^D\\+\\d{2} \\d{2}:00$\"},\n", " },\n", " \"source\": {\n", " \"source_id\": {\"type\": str, \"pattern\": r\"^W\\d{2}$\"},\n", " \"source_type\": {\"type\": str, \"enum\": [\"water_truck\", \"purification_unit\", \"well\", \"river_intake\"]},\n", " \"grid_km\": {\"type\": list, \"length\": 2},\n", " },\n", " \"quality\": { # ๋‹จ์œ„์™€ ๋ฌผ๋ฆฌ์ ์œผ๋กœ ๊ฐ€๋Šฅํ•œ ๋ฒ”์œ„(๊ธฐ์ค€์น˜๊ฐ€ ์•„๋‹ˆ๋ผ ์ธก์ • ๊ฐ€๋Šฅ ๋ฒ”์œ„)\n", " \"ph\": {\"type\": (int, float), \"min\": 0, \"max\": 14, \"unit\": \"-\"},\n", " \"turbidity_ntu\": {\"type\": (int, float), \"min\": 0, \"max\": 1000, \"unit\": \"NTU\"},\n", " \"chlorine_mgL\": {\"type\": (int, float), \"min\": 0, \"max\": 10, \"unit\": \"mg/L\"},\n", " \"metal_ugL\": {\"type\": (int, float), \"min\": 0, \"max\": 1000, \"unit\": \"ug/L\"},\n", " \"ecoli_cfu\": {\"type\": int, \"min\": 0, \"max\": 100000, \"unit\": \"CFU/100mL\"},\n", " },\n", " \"sensor\": {\n", " \"status\": {\"type\": str, \"enum\": [\"ok\", \"calibration_due\", \"fault\", \"offline\"]},\n", " \"last_calibrated_h\": {\"type\": int, \"min\": 0, \"max\": 720},\n", " },\n", " \"tactical\": {\n", " \"weather\": {\"type\": str, \"enum\": [\"clear\", \"rain\", \"night\", \"snow\", \"dust\"]},\n", " \"position_error_m\": {\"type\": (int, float), \"min\": 0, \"max\": 500},\n", " \"comm_delay_ms\": {\"type\": (int, float), \"min\": 0, \"max\": 10000},\n", " },\n", "}\n", "\n", "\n", "def check(name, value, rule):\n", " \"\"\"๊ฐ’ ํ•˜๋‚˜๋ฅผ ๊ทœ์น™(type, enum, min, max, length, pattern)์œผ๋กœ ๊ฒ€์‚ฌํ•ด ์˜ค๋ฅ˜ ๋ชฉ๋ก์„ ๋Œ๋ ค์ค€๋‹ค.\"\"\"\n", " if not isinstance(value, rule[\"type\"]) or isinstance(value, bool):\n", " return [f\"{name}: ์ž๋ฃŒํ˜• ์˜ค๋ฅ˜({type(value).__name__})\"]\n", " errs = []\n", " if \"enum\" in rule and value not in rule[\"enum\"]:\n", " errs.append(f\"{name}: ํ—ˆ์šฉ๋˜์ง€ ์•Š์€ ๊ฐ’ '{value}'\")\n", " if \"min\" in rule and value < rule[\"min\"]:\n", " errs.append(f\"{name}: ์ตœ์†Œ๊ฐ’ {rule['min']} ๋ฏธ๋งŒ\")\n", " if \"max\" in rule and value > rule[\"max\"]:\n", " errs.append(f\"{name}: ์ตœ๋Œ€๊ฐ’ {rule['max']} ์ดˆ๊ณผ\")\n", " if \"length\" in rule and len(value) != rule[\"length\"]:\n", " errs.append(f\"{name}: ๊ธธ์ด {rule['length']} ํ•„์š”\")\n", " if \"pattern\" in rule and not re.match(rule[\"pattern\"], value):\n", " errs.append(f\"{name}: ํ˜•์‹ ๋ถˆ์ผ์น˜ '{value}'\")\n", " return errs\n", "\n", "\n", "def validate(rec):\n", " errs = [f\"{k}: ํ•„์ˆ˜ ํ•„๋“œ ๋ˆ„๋ฝ\" for k in SCHEMA[\"required\"] if k not in rec]\n", " for k, rule in SCHEMA[\"fields\"].items():\n", " if k in rec:\n", " errs += check(k, rec[k], rule)\n", " for group in (\"source\", \"quality\", \"sensor\", \"tactical\"):\n", " for k, rule in SCHEMA[group].items():\n", " if k not in rec.get(group, {}):\n", " errs.append(f\"{group}.{k}: ํ•„์ˆ˜ ํ•„๋“œ ๋ˆ„๋ฝ\")\n", " else:\n", " errs += check(f\"{group}.{k}\", rec[group][k], rule)\n", " return errs\n", "\n", "\n", "# 2) [๊ต์œก์šฉ๊ฐ€์ƒ์ž๋ฃŒ] ๊ฐ€์ƒ ์„ผ์„œ ๋ฐ์ดํ„ฐ์—์„œ ์ƒ˜ํ”Œ 20๊ฑด์„ ๋ฝ‘์•„ ์Šคํ‚ค๋งˆ ํ˜•์‹(JSON)์œผ๋กœ ๋ณ€ํ™˜\n", "data = make_dataset(days=3, seed=1, n_events=1, n_faults=0)\n", "rng = np.random.default_rng(1)\n", "rows = data.sample(20, random_state=1).sort_values([\"source_id\", \"t\"])\n", "records = []\n", "for _, r in rows.iterrows():\n", " records.append({\n", " \"record_id\": f\"WQ-{r['source_id']}-{int(r['t']):04d}\", \"measured_at\": f\"D+{int(r['t']) // 24:02d} {int(r['t']) % 24:02d}:00\",\n", " \"source\": {\"source_id\": r[\"source_id\"], \"source_type\": r[\"source_type\"], \"grid_km\": [r[\"x_km\"], r[\"y_km\"]]},\n", " \"quality\": {\"ph\": float(r[\"ph\"]), \"turbidity_ntu\": float(r[\"turbidity\"]), \"chlorine_mgL\": float(r[\"chlorine\"]),\n", " \"metal_ugL\": float(r[\"metal\"]), \"ecoli_cfu\": int(r[\"ecoli\"])},\n", " \"sensor\": {\"status\": \"ok\", \"last_calibrated_h\": int(rng.integers(1, 200))},\n", " \"tactical\": {\"weather\": {\"storm\": \"rain\", \"night\": \"night\", \"day\": \"clear\"}[r[\"cond\"]],\n", " \"position_error_m\": float(round(rng.uniform(3, 40), 1)), \"comm_delay_ms\": int(rng.integers(30, 900))}})\n", "\n", "ok = sum(1 for r in records if not validate(r))\n", "print(f\"์ •์ƒ ์ƒ˜ํ”Œ {len(records)}๊ฑด ์ค‘ ๊ฒ€์ฆ ํ†ต๊ณผ {ok}๊ฑด, ์˜ค๋ฅ˜ {len(records) - ok}๊ฑด\")\n", "parsed = json.loads(json.dumps(records, ensure_ascii=False)) # ์ง๋ ฌํ™” -> ํŒŒ์‹ฑ ์™•๋ณต ์‹œํ—˜\n", "print(\"JSON ์ง๋ ฌํ™”ยทํŒŒ์‹ฑ ์™•๋ณต ํ›„ ๊ฐ’ ์ผ์น˜:\", parsed == records)\n", "\n", "# 3) ์˜ค๋ฅ˜ ์ฃผ์ž… ์‹œํ—˜: ๊ฒ€์ฆ๊ธฐ๊ฐ€ ์ž˜๋ชป๋œ ๋ฐ์ดํ„ฐ๋ฅผ ์žก์•„๋‚ด๋Š”์ง€ ํ™•์ธ\n", "bad = json.loads(json.dumps(records[0]))\n", "bad[\"record_id\"] = \"WQ-1\" # ํ˜•์‹ ์˜ค๋ฅ˜\n", "bad[\"quality\"][\"ph\"] = 15.2 # ๋ฌผ๋ฆฌ ๋ฒ”์œ„ ์ดˆ๊ณผ\n", "bad[\"quality\"][\"ecoli_cfu\"] = \"๊ฒ€์ถœ\" # ์ž๋ฃŒํ˜• ์˜ค๋ฅ˜(์ˆซ์ž์—ฌ์•ผ ํ•จ)\n", "bad[\"source\"][\"source_type\"] = \"bottle\" # ํ—ˆ์šฉ๋˜์ง€ ์•Š์€ ๊ธ‰์ˆ˜์› ์œ ํ˜•\n", "del bad[\"tactical\"][\"comm_delay_ms\"] # ์ „์ˆ  ํŒŒ๋ผ๋ฏธํ„ฐ ๋ˆ„๋ฝ\n", "print(\"\\n[์˜ค๋ฅ˜ ์ฃผ์ž… ๊ฒฐ๊ณผ]\")\n", "errs = validate(bad)\n", "for e in errs:\n", " print(\" -\", e)\n", "print(f\"์ฃผ์ž… ์˜ค๋ฅ˜ 5์ข… ์ค‘ ๊ฒ€์ถœ {len(errs)}์ข… -> {'๋ชจ๋‘ ๊ฒ€์ถœ' if len(errs) >= 5 else '๋ˆ„๋ฝ ์žˆ์Œ'}\")\n", "\n", "# 4) ๋ฐ์ดํ„ฐ ๋ช…์„ธ ์š”์•ฝ(๋ช…์„ธ์„œ ์ดˆ์•ˆ)\n", "spec = []\n", "for g in (\"fields\", \"source\", \"quality\", \"sensor\", \"tactical\"):\n", " for k, r in SCHEMA[g].items():\n", " t = r[\"type\"].__name__ if isinstance(r[\"type\"], type) else \"number\"\n", " spec.append((g, k, t, r.get(\"unit\", \"\"), str({x: y for x, y in r.items() if x not in (\"type\", \"unit\")})))\n", "print(\"\\n[๋ฐ์ดํ„ฐ ๋ช…์„ธ ์š”์•ฝ]\")\n", "print(pd.DataFrame(spec, columns=[\"๊ทธ๋ฃน\", \"ํ•„๋“œ\", \"์ž๋ฃŒํ˜•\", \"๋‹จ์œ„\", \"๊ทœ์น™\"]).to_string(index=False))\n", "done = sum(1 for s in spec if s[2] and s[4] != \"{}\")\n", "print(f\"์Šคํ‚ค๋งˆ ํ•„๋“œ ์ •์˜ ์™„์„ฑ๋„ {done}/{len(spec)} ({done / len(spec):.0%}): ๋ชจ๋“  ํ•„๋“œ์— ์ž๋ฃŒํ˜•ยท๊ฒ€์ฆ ๊ทœ์น™ ์ •์˜\")\n", "print(\"\\n[๊ฐ€์ • ํŒ์ • ๊ธฐ์ค€(๊ต์œก์šฉ)] (์ฃผ์˜, ์œ„ํ—˜):\", {k: v for k, v in LIMITS.items()})\n", "print(\"๊ธ‰์ˆ˜์› ์œ ํ˜•๋ณ„ ์ƒ˜ํ”Œ ์ˆ˜:\", rows[\"source_type\"].value_counts().to_dict())\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "
\n", "๐Ÿ’ก [์ฐธ๊ณ ] ๋ฏธ์…˜ 01 ์˜ˆ์ƒ ์‹คํ–‰ ๊ฒฐ๊ณผ (Output)\n", "\n", "```text\n", "์ •์ƒ ์ƒ˜ํ”Œ 20๊ฑด ์ค‘ ๊ฒ€์ฆ ํ†ต๊ณผ 20๊ฑด, ์˜ค๋ฅ˜ 0๊ฑด\n", "JSON ์ง๋ ฌํ™”ยทํŒŒ์‹ฑ ์™•๋ณต ํ›„ ๊ฐ’ ์ผ์น˜: True\n", "\n", "[์˜ค๋ฅ˜ ์ฃผ์ž… ๊ฒฐ๊ณผ]\n", " - record_id: ํ˜•์‹ ๋ถˆ์ผ์น˜ 'WQ-1'\n", " - source.source_type: ํ—ˆ์šฉ๋˜์ง€ ์•Š์€ ๊ฐ’ 'bottle'\n", " - quality.ph: ์ตœ๋Œ€๊ฐ’ 14 ์ดˆ๊ณผ\n", " - quality.ecoli_cfu: ์ž๋ฃŒํ˜• ์˜ค๋ฅ˜(str)\n", " - tactical.comm_delay_ms: ํ•„์ˆ˜ ํ•„๋“œ ๋ˆ„๋ฝ\n", "์ฃผ์ž… ์˜ค๋ฅ˜ 5์ข… ์ค‘ ๊ฒ€์ถœ 5์ข… -> ๋ชจ๋‘ ๊ฒ€์ถœ\n", "\n", "[๋ฐ์ดํ„ฐ ๋ช…์„ธ ์š”์•ฝ]\n", " ๊ทธ๋ฃน ํ•„๋“œ ์ž๋ฃŒํ˜• ๋‹จ์œ„ ๊ทœ์น™\n", " fields record_id str {'pattern': '^WQ-W\\\\d{2}-\\\\d{4}$'}\n", " fields measured_at str {'pattern': '^D\\\\+\\\\d{2} \\\\d{2}:00$'}\n", " source source_id str {'pattern': '^W\\\\d{2}$'}\n", " source source_type str {'enum': ['water_truck', 'purification_unit', 'well', 'river_intake']}\n", " source grid_km list {'length': 2}\n", " quality ph number - {'min': 0, 'max': 14}\n", " quality turbidity_ntu number NTU {'min': 0, 'max': 1000}\n", " quality chlorine_mgL number mg/L {'min': 0, 'max': 10}\n", " quality metal_ugL number ug/L {'min': 0, 'max': 1000}\n", " quality ecoli_cfu int CFU/100mL {'min': 0, 'max': 100000}\n", " sensor status str {'enum': ['ok', 'calibration_due', 'fault', 'offline']}\n", " sensor last_calibrated_h int {'min': 0, 'max': 720}\n", "tactical weather str {'enum': ['clear', 'rain', 'night', 'snow', 'dust']}\n", "tactical position_error_m number {'min': 0, 'max': 500}\n", "tactical comm_delay_ms number {'min': 0, 'max': 10000}\n", "์Šคํ‚ค๋งˆ ํ•„๋“œ ์ •์˜ ์™„์„ฑ๋„ 15/15 (100%): ๋ชจ๋“  ํ•„๋“œ์— ์ž๋ฃŒํ˜•ยท๊ฒ€์ฆ ๊ทœ์น™ ์ •์˜\n", "\n", "[๊ฐ€์ • ํŒ์ • ๊ธฐ์ค€(๊ต์œก์šฉ)] (์ฃผ์˜, ์œ„ํ—˜): {'ph': (5.8, 8.5), 'turbidity': (1.0, 2.0), 'chlorine': (0.2, 0.1), 'metal': (5.0, 10.0), 'ecoli': (1, 1)}\n", "๊ธ‰์ˆ˜์› ์œ ํ˜•๋ณ„ ์ƒ˜ํ”Œ ์ˆ˜: {'purification_unit': 7, 'river_intake': 7, 'water_truck': 3, 'well': 3}\n", "```\n", "
\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "---\n", "\n", "## ๐ŸŽฏ ๋ฏธ์…˜ 02. ์ˆ˜์งˆ ์„ผ์„œ ๊ฒฐ์ธก์น˜ ๋Œ€์ฒด ๋ฐ ์‹œ๊ณ„์—ด ์„ผ์„œ ์ด์ƒ์น˜ ์ •์ œ ์ „์ฒ˜๋ฆฌ ๋ชจ๋“ˆ ๊ตฌํ˜„\n", "**์†Œ์š”์‹œ๊ฐ„**: ์•ฝ 2.0์‹œ๊ฐ„\n", "\n", "### ๐Ÿ“‹ ๋ฏธ์…˜ ๋ฐฐ๊ฒฝ ๋ฐ ์ˆ˜ํ–‰ ์ง€์นจ\n", "#### ๋ฏธ์…˜ ๋ฐฐ๊ฒฝ ๋ฐ ๋‹น์œ„์„ฑ\n", "\n", "์ˆ˜์งˆ ์„ผ์„œ ๊ฒฐ์ธก์น˜ ๋Œ€์ฒด ๋ฐ ์‹œ๊ณ„์—ด ์ด์ƒ์น˜ ์ •์ œ ์ „์ฒ˜๋ฆฌ ๋ชจ๋“ˆ์„ ๊ตฌํ˜„ํ•œ๋‹ค. ์‹ค์ œ ์ „์ˆ  ํ˜„์žฅ์—์„œ๋Š” ๋‹จ์ˆœ ํƒ์ง€์— ๊ทธ์น˜์ง€ ์•Š๊ณ  ์‹ ์†ํ•œ ์œ„ํ˜‘ ํŒ๋‹จ๊ณผ ์‹ ๋ขฐ๋„ ๋†’์€ ์˜์‚ฌ๊ฒฐ์ •์ด ํ•„์ˆ˜์ ์ด๋ฏ€๋กœ, **์ˆ˜์งˆ ์„ผ์„œ์˜ ๊ฒฐ์ธก์น˜๋ฅผ ๋ณด์ •ํ•˜๊ณ  ์‹œ๊ณ„์—ด ์ด์ƒ์น˜๋ฅผ ์ •์ œํ•˜๋Š” ์ „์ฒ˜๋ฆฌ ๋ชจ๋“ˆ์„ ๊ตฌํ˜„ํ•ด ์†๋„์™€ ์ •ํ™•๋„๋ฅผ ํ™•์ธํ•œ๋‹ค.** ๊ณผ์—…์„ ์™„์ˆ˜ํ•˜์—ฌ ์˜คํŒ์„ ๋ฐฉ์ง€ํ•˜๊ณ  ์ž‘์ „ ๋Œ€์‘๋ ฅ์„ ๊ทน๋Œ€ํ™”ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.\n", "\n", "์ด ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด ๋ณธ ๋ฏธ์…˜์—์„œ๋Š” **๊ธ‰์ˆ˜์›๋ณ„ ์‹œ๊ณ„์—ด์—์„œ ๋ฌผ๋ฆฌ ๋ฒ”์œ„ ๋ฐ– ๊ฐ’, ๊ฐ™์€ ๊ฐ’์ด ๋ฐ˜๋ณต๋˜๋Š” ๊ณ ์ •๊ฐ’(stuck), ํŠ€์—ˆ๋‹ค๊ฐ€ ๋ฐ”๋กœ ๋Œ์•„์˜ค๋Š” ๋‹จ๋ฐœ ์ŠคํŒŒ์ดํฌ๋ฅผ ํ‘œ์‹œยท์ œ๊ฑฐํ•˜๊ณ  ์งง์€ ๊ฒฐ์ธก์€ ์„ ํ˜• ๋ณด๊ฐ„, ๊ธด ๊ฒฐ์ธก์€ ํ‘œ์‹œ ์ˆ˜ํ–‰ํ•˜๊ณ  ๊ฒ€์ฆํ•˜๋Š”** ์‹ค์Šต์„ ์ฒด๊ณ„์ ์œผ๋กœ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค.\n", "\n", "ํ•™์Šต์ž๋Š” ์‹ค์Šต๊ฐ€์ด๋“œ์— ์ œ์‹œ๋œ ์ด 4๊ฐœ ์„ธ๋ถ€ ํ™œ๋™(๊ธฐ๋ณธ ๊ธฐ๋Šฅ ๊ตฌํ˜„ โž” ๊ฒฐ๊ณผ ์ง€ํ‘œ ๊ฒ€์ฆ โž” ํŒŒ๋ผ๋ฏธํ„ฐ ํŠœ๋‹ ๋ฐ ๋„์ „๊ณผ์ œ)์„ ์ˆœ์ฐจ์ ์œผ๋กœ ์ง„ํ–‰ํ•˜๋ฉฐ ํ•ต์‹ฌ ํŒ์ • ๊ธฐ์ค€์„ ๋‹ฌ์„ฑํ•ฉ๋‹ˆ๋‹ค.\n", "\n", "**์ด๋ฒˆ ๊ฒฐ๊ณผ๋ฌผ:** `src/preprocess/wel03_preprocessor.py ๋ฐ ์ „์ฒ˜๋ฆฌ ์ „ํ›„ ๋ฐ์ดํ„ฐ์…‹` ํ˜•ํƒœ์˜ ๊ฒฐ๊ณผ๋ฌผ์„ ์™„์„ฑํ•œ๋‹ค.\n", "\n", "### ๐Ÿ” ํ‰๊ฐ€ ๋ฐ ๊ฒ€์ฆ ๊ธฐ์ค€ (PASS ์กฐ๊ฑด)\n", "#### ๋ฏธ์…˜ ํŒ์ •\n", "\n", "**ํ‰๊ฐ€ ๊ตฌ๋ถ„:** ํ•„์ˆ˜\n", "\n", "**ํ•ต์‹ฌ ํ‰๊ฐ€์˜์—ญ:** ์ „์ˆ  ํ™˜๊ฒฝ ํŠน์œ ์˜ ๋…ธ์ด์ฆˆ(์Šคํ‘ธํ•‘, ๋ฐ˜์‚ฌํŒŒ, ์„ผ์„œ ๋‹จ์ ˆ ๋“ฑ) ์ œ๊ฑฐ ํƒ€๋‹น์„ฑ\n", "\n", "#### ๋ฏธ์…˜ PASS ์กฐ๊ฑด\n", "\n", "- ์ •๋Ÿ‰ ๊ธฐ์ค€: ๋ฐ์ดํ„ฐ ์ •์ œ ์ฒ˜๋ฆฌ ์†๋„ 1,000๊ฑด/์ดˆ ์ด์ƒ, ์ „์ฒ˜๋ฆฌ ์˜ค๋ฅ˜์œจ 0%\n", "\n", "\n", "- ์ง„ํ–‰ ๊ธฐ์ค€: ์ •์ œ ํŒŒ์ดํ”„๋ผ์ธ ์ž๋™ํ™” ๋ฐ ๋‹จ์œ„ ํ…Œ์ŠคํŠธ ์ผ€์ด์Šค 5์ข… ํ†ต๊ณผ\n", "\n", "\n", "- ๊ฒ€์ฆ ๋ฐฉ๋ฒ•: pytest ๊ธฐ๋ฐ˜ ์ „์ฒ˜๋ฆฌ ์ •ํ•ฉ์„ฑ ํ…Œ์ŠคํŠธ ๋ฐ ๊ฒฐ์ธก์น˜ ๋ณด์ •์œจ ํ™•์ธ\n", "\n", "\n", "#### ์ฑ„์ ์ž ํŒ์ • ๋ฉ”๋ชจ\n", "\n", "> ๋‹จ์ˆœ ์ฝ”๋“œ ์‹คํ–‰ ์—ฌ๋ถ€๋ฟ๋งŒ ์•„๋‹ˆ๋ผ ์ถœ๋ ฅ๋œ ์ˆ˜์น˜์™€ ๊ทผ๊ฑฐ ์ž๋ฃŒ๊ฐ€ ๊ตฐ ์‹ค๋ฌด ์š”๊ตฌ์‚ฌํ•ญ์— ๋ถ€ํ•ฉํ•˜๋Š”์ง€ ๋ฉด๋ฐ€ํžˆ ํ™•์ธํ•œ๋‹ค.\n", "\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "\"\"\"์ˆ˜์งˆ ์„ผ์„œ ์‹œ๊ณ„์—ด์˜ ๋ฌผ๋ฆฌ ๋ฒ”์œ„ ๋ฐ– ๊ฐ’ยท๊ณ ์ •๊ฐ’(stuck)ยท๋‹จ๋ฐœ ์ŠคํŒŒ์ดํฌ๋ฅผ ์ •์ œํ•˜๊ณ  ๊ฒฐ์ธก์„ ๋ณด์ •ํ•˜๋Š” ์ „์ฒ˜๋ฆฌ ๋ชจ๋“ˆ์„ ๊ตฌํ˜„ํ•œ ๋’ค, ์ฒ˜๋ฆฌ ์†๋„ยท์˜ค๋ฅ˜์œจยท๋ณด์ • ์ •ํ™•๋„์™€ ๋‹จ์œ„ ํ…Œ์ŠคํŠธ 5์ข…์„ ํ™•์ธํ•œ๋‹ค.\"\"\"\n", "import time\n", "import numpy as np\n", "import pandas as pd\n", "import matplotlib.pyplot as plt\n", "\n", "# ---------------------------------------------------------------------------\n", "# [๊ต์œก์šฉ๊ฐ€์ƒ์ž๋ฃŒ] ์•ผ์ „ ๊ธ‰์ˆ˜์› ๊ฐ€์ƒ ์ˆ˜์งˆ ์„ผ์„œ ๋ฐ์ดํ„ฐ ์ƒ์„ฑ๊ธฐ (์‹ค์ œ ๋ถ€๋Œ€ยท์œ„์น˜ยท๊ฒ€์‚ฌ ๊ฒฐ๊ณผ ์—†์Œ)\n", "# ๊ฐ€์ƒ ๊ธ‰์ˆ˜์› 8๊ณณ x 14์ผ x 1์‹œ๊ฐ„ ๊ฐ„๊ฒฉ. ์‹ค์ œ ์ˆ˜์งˆ(true_*)ยท์„ผ์„œ ์ธก์ •๊ฐ’ยท์˜ค์—ผ ์‚ฌ๊ฑดยท์„ผ์„œ ๊ณ ์žฅ ์ •๋‹ต์„ ํ•จ๊ป˜ ๋งŒ๋“ ๋‹ค.\n", "# ๊ธฐ์ค€์น˜๋Š” ๊ต์œก์šฉ ๊ฐ€์ •๊ฐ’์ด๋ฉฐ, ์‹ค์ œ ์Œ์šฉ ํŒ์ •์€ ๊ณต์ธ ๊ฒ€์‚ฌ ๊ฒฐ๊ณผ์™€ ์˜๋ฌด(์˜ˆ๋ฐฉ์˜ํ•™) ๋‹ด๋‹น ๊ธฐ์ค€์— ๋”ฐ๋ฅธ๋‹ค.\n", "# ---------------------------------------------------------------------------\n", "PARAMS = [\"ph\", \"turbidity\", \"chlorine\", \"metal\", \"ecoli\"]\n", "SOURCES = [(\"W01\", \"water_truck\", 1.0, 2.0), (\"W02\", \"water_truck\", 3.5, 1.2), (\"W03\", \"purification_unit\", 2.2, 4.1),\n", " (\"W04\", \"purification_unit\", 5.0, 3.3), (\"W05\", \"well\", 0.8, 5.6), (\"W06\", \"well\", 4.4, 6.0),\n", " (\"W07\", \"river_intake\", 6.3, 1.8), (\"W08\", \"river_intake\", 6.9, 4.9)]\n", "BASE = {\"water_truck\": (7.2, 0.25, 0.75, 1.5), \"purification_unit\": (7.0, 0.20, 0.85, 1.2),\n", " \"well\": (6.8, 0.45, 0.50, 3.0), \"river_intake\": (7.3, 0.55, 0.60, 2.0)} # pH, ํƒ๋„(NTU), ์ž”๋ฅ˜์—ผ์†Œ(mg/L), ์ค‘๊ธˆ์†(ug/L)\n", "LIMITS = {\"ph\": (5.8, 8.5), \"turbidity\": (1.0, 2.0), \"chlorine\": (0.2, 0.1), \"metal\": (5.0, 10.0), \"ecoli\": (1, 1)} # (์ฃผ์˜, ์œ„ํ—˜) ๊ฐ€์ • ๊ธฐ์ค€\n", "EVENT_TYPES = { # ์˜ค์—ผ ์‚ฌ๊ฑด: ๋ณ€์ˆ˜๋ณ„ ์ตœ๋Œ€ ๋ณ€ํ™”๋Ÿ‰, ์ตœ๋Œ€์น˜ ๋„๋‹ฌ ์‹œ๊ฐ„(h)\n", " \"sewage\": ({\"turbidity\": 2.8, \"chlorine\": -0.5, \"ecoli\": 30, \"ph\": -0.3}, 4),\n", " \"metal\": ({\"metal\": 22.0, \"ph\": -0.5}, 4),\n", " \"disinfect_fail\": ({\"chlorine\": -0.8, \"ecoli\": 6}, 6),\n", " \"chemical\": ({\"ph\": 1.7, \"turbidity\": 1.0}, 3),\n", " \"creeping_metal\": ({\"metal\": 13.0}, 18),\n", "}\n", "FAULT_TYPES = [\"stuck\", \"spike\", \"drift\", \"dropout\"]\n", "NOISE = {\"ph\": 0.04, \"turbidity\": 0.06, \"chlorine\": 0.03, \"metal\": 0.35}\n", "DIGITS = {\"ph\": 2, \"turbidity\": 2, \"chlorine\": 2, \"metal\": 1}\n", "\n", "\n", "def make_dataset(days=14, seed=7, n_events=6, n_faults=10, events=None, faults=None, sources=None):\n", " \"\"\"events: [(๊ธ‰์ˆ˜์› ๋ฒˆํ˜ธ, ์‚ฌ๊ฑด ์œ ํ˜•, ์‹œ์ž‘ ์‹œ๊ฐ h, ์ง€์† h, ๊ฐ•๋„)], faults: [(๊ธ‰์ˆ˜์› ๋ฒˆํ˜ธ, ๊ณ ์žฅ ์œ ํ˜•, ํ•ญ๋ชฉ, ์‹œ์ž‘ h, ์ง€์† h)]\"\"\"\n", " rng = np.random.default_rng(seed)\n", " sources = sources or SOURCES\n", " T = days * 24\n", " tt = np.arange(T)\n", " hour = tt % 24\n", " night = (hour < 6) | (hour >= 20)\n", " rain = np.zeros(T)\n", " for _ in range(max(1, days // 4)):\n", " s0, d = int(rng.integers(0, T - 14)), int(rng.integers(6, 14))\n", " rain[s0:s0 + d] = np.maximum(rain[s0:s0 + d], rng.uniform(0.6, 1.0))\n", " if events is None:\n", " events = [(int(rng.integers(len(sources))), str(rng.choice(list(EVENT_TYPES))), int(rng.integers(min(24, T // 4), max(T // 4 + 1, T - 48))),\n", " int(rng.integers(10, 30)), 1.0) for _ in range(n_events)]\n", " if faults is None:\n", " faults = [(int(rng.integers(len(sources))), str(rng.choice(FAULT_TYPES)), str(rng.choice([\"ph\", \"turbidity\", \"chlorine\", \"metal\"])),\n", " int(rng.integers(12, max(13, T - 30))), int(rng.integers(6, 24))) for _ in range(n_faults)]\n", " out = []\n", " for si, (sid, stype, x, y) in enumerate(sources):\n", " ph0, tb0, cl0, mt0 = BASE[stype]\n", " tr = {\"ph\": ph0 + 0.08 * np.sin(2 * np.pi * hour / 24) + rng.normal(0, 0.03, T),\n", " \"turbidity\": tb0 * (1 + 0.1 * rng.normal(0, 1, T)),\n", " \"chlorine\": (cl0 * np.exp(-0.022 * hour) if stype == \"water_truck\" else cl0 + rng.normal(0, 0.03, T)),\n", " \"metal\": mt0 + rng.normal(0, 0.2, T), \"ecoli\": np.zeros(T)}\n", " if stype in (\"river_intake\", \"well\"):\n", " tr[\"turbidity\"] = tr[\"turbidity\"] + rain * (0.9 if stype == \"river_intake\" else 0.45)\n", " f_all, etype = np.zeros(T), np.array([\"none\"] * T, dtype=object)\n", " for (s_i, et, onset, dur, scale) in events:\n", " if s_i != si:\n", " continue\n", " delta, ramp = EVENT_TYPES[et]\n", " f = np.clip((tt - onset + 1) / ramp, 0, 1) * ((tt >= onset) & (tt < onset + dur + ramp))\n", " for k, v in delta.items():\n", " tr[k] = tr[k] + v * f * scale\n", " etype[f > 0] = et\n", " f_all = np.maximum(f_all, f)\n", " tr[\"chlorine\"] = np.clip(tr[\"chlorine\"], 0.02, None)\n", " tr[\"turbidity\"] = np.clip(tr[\"turbidity\"], 0.05, None)\n", " tr[\"metal\"] = np.clip(tr[\"metal\"], 0.1, None)\n", " grade = np.where(f_all >= 0.2, 2, 0)\n", " mild = (tr[\"turbidity\"] > LIMITS[\"turbidity\"][0]) | (tr[\"chlorine\"] < LIMITS[\"chlorine\"][0]) | (tr[\"metal\"] > LIMITS[\"metal\"][0])\n", " grade = np.where((grade == 0) & mild, 1, grade)\n", " ms = {}\n", " for p in [\"ph\", \"turbidity\", \"chlorine\", \"metal\"]:\n", " sd = NOISE[p] * np.where(night, 1.5, 1.0) * (tr[p] if p == \"turbidity\" else 1)\n", " ms[p] = np.round(np.clip(tr[p] + rng.normal(0, 1, T) * sd, 0, None), DIGITS[p])\n", " ms[\"ecoli\"] = (rng.poisson(tr[\"ecoli\"]) + (rng.random(T) < 0.004)).astype(float)\n", " fault, fparam = np.array([\"none\"] * T, dtype=object), np.array([\"-\"] * T, dtype=object)\n", " for (s_i, ft, p, s0, d) in faults:\n", " if s_i != si:\n", " continue\n", " seg = slice(s0, min(T, s0 + d))\n", " if ft == \"stuck\":\n", " ms[p][seg] = ms[p][s0]\n", " elif ft == \"spike\":\n", " seg = slice(s0, s0 + 1)\n", " ms[p][s0] = {\"ph\": 11.8, \"turbidity\": 38.0, \"chlorine\": 0.0, \"metal\": 75.0}[p]\n", " elif ft == \"drift\":\n", " ramp = np.linspace(0, 1, seg.stop - s0)\n", " ms[p][seg] = np.clip(ms[p][seg] + ramp * {\"ph\": -1.6, \"turbidity\": 2.6, \"chlorine\": -0.75, \"metal\": 12.0}[p], 0, None).round(DIGITS[p])\n", " elif ft == \"dropout\":\n", " for q in PARAMS:\n", " ms[q][seg] = np.nan\n", " p = \"all\"\n", " fault[seg], fparam[seg] = ft, p\n", " df = pd.DataFrame({\"source_id\": sid, \"source_type\": stype, \"x_km\": x, \"y_km\": y, \"t\": tt,\n", " \"cond\": np.where(rain > 0, \"storm\", np.where(night, \"night\", \"day\")),\n", " **{p: ms[p] for p in PARAMS}, **{f\"true_{p}\": tr[p] for p in PARAMS},\n", " \"event\": etype, \"fault\": fault, \"fault_param\": fparam, \"grade\": grade})\n", " out.append(df)\n", " return pd.concat(out, ignore_index=True)\n", "# ---------------------------------------------------------------------------\n", "\n", "# ---- ์ „์ฒ˜๋ฆฌ ๊ณตํ†ต ํ•จ์ˆ˜: ๋ฌผ๋ฆฌ ๋ฒ”์œ„ ์ ๊ฒ€ -> ๊ณ ์ •๊ฐ’(stuck) ํ‘œ์‹œ -> ๋‹จ๋ฐœ ์ŠคํŒŒ์ดํฌ ์ œ๊ฑฐ -> ๊ฒฐ์ธก ๋ณด์ • ----\n", "PHYS = {\"ph\": (0, 14), \"turbidity\": (0, 1000), \"chlorine\": (0, 10), \"metal\": (0, 1000), \"ecoli\": (0, 100000)}\n", "SPIKE_K = {\"day\": 6.0, \"night\": 7.0, \"storm\": 9.0} # ์ƒํ™ฉ๋ณ„(์ฃผ๊ฐ„/์•ผ๊ฐ„/์•…์ฒœํ›„) ์ŠคํŒŒ์ดํฌ ํŒ์ • ๋ฐฐ์ˆ˜\n", "MIN_JUMP = {\"ph\": 0.8, \"turbidity\": 1.5, \"chlorine\": 0.3, \"metal\": 6.0}\n", "STUCK_RUN, SHORT_GAP = 6, 3\n", "\n", "\n", "def clean(df, spike_k=None, min_jump=None):\n", " \"\"\"๊ธ‰์ˆ˜์›๋ณ„ ์‹œ๊ณ„์—ด์„ ์ •์ œํ•˜๊ณ  ํ•ญ๋ชฉ๋ณ„ ์ฒ˜๋ฆฌ ๋‚ด์—ญ(<ํ•ญ๋ชฉ>_flag: range/stuck/spike/interp/long_gap)์„ ๋‚จ๊ธด๋‹ค.\"\"\"\n", " spike_k = spike_k or SPIKE_K\n", " min_jump = MIN_JUMP if min_jump is None else min_jump\n", " out = df.sort_values([\"source_id\", \"t\"]).reset_index(drop=True)\n", " for sid, idx in out.groupby(\"source_id\").groups.items():\n", " idx = np.asarray(idx)\n", " kk = out.loc[idx, \"cond\"].map(spike_k).to_numpy(float)\n", " for p in PARAMS:\n", " v = out.loc[idx, p].to_numpy(float).copy()\n", " fl = np.array([\"\"] * len(v), dtype=object)\n", " lo, hi = PHYS[p]\n", " bad = (v < lo) | (v > hi)\n", " v[bad], fl[bad] = np.nan, \"range\"\n", " if p != \"ecoli\":\n", " chg = np.r_[True, v[1:] != v[:-1]] # ๊ฐ™์€ ๊ฐ’์ด STUCK_RUN์‹œ๊ฐ„ ์ด์ƒ ๋ฐ˜๋ณต = ๊ณ ์ •๊ฐ’ ๊ณ ์žฅ ์˜์‹ฌ\n", " rid = np.cumsum(chg)\n", " rlen = np.bincount(rid)[rid]\n", " fl[(rlen >= STUCK_RUN) & ~chg & ~np.isnan(v)] = \"stuck\"\n", " d = np.abs(np.diff(v[~np.isnan(v)]))\n", " sig = 1.4826 * np.median(d) + 1e-6 if len(d) else 1.0\n", " up, dn = v - np.r_[np.nan, v[:-1]], v - np.r_[v[1:], np.nan]\n", " thr = np.maximum(kk * sig, min_jump[p])\n", " spike = (np.abs(up) > thr) & (np.abs(dn) > thr) & (np.sign(up) == np.sign(dn)) # ํŠ€์—ˆ๋‹ค๊ฐ€ ๋ฐ”๋กœ ๋Œ์•„์˜จ ๊ฐ’๋งŒ\n", " v[spike], fl[spike] = np.nan, \"spike\"\n", " s = pd.Series(v)\n", " na = s.isna().to_numpy()\n", " grp = np.cumsum(np.r_[True, na[1:] != na[:-1]])\n", " glen = pd.Series(na.astype(int)).groupby(grp).transform(\"sum\").to_numpy()\n", " lin, ff = s.interpolate(limit_area=\"inside\").to_numpy(), s.ffill().bfill().to_numpy()\n", " short, longg = na & (glen <= SHORT_GAP), na & (glen > SHORT_GAP)\n", " v = np.where(short, np.where(np.isnan(lin), ff, lin), np.where(longg, ff, v))\n", " fl[short & (fl == \"\")] = \"interp\"\n", " fl[longg] = \"long_gap\"\n", " out.loc[idx, p] = v\n", " out.loc[idx, f\"{p}_flag\"] = fl\n", " return out\n", "\n", "\n", "data = make_dataset(days=14, seed=7, n_events=8, n_faults=16)\n", "\n", "# 1) ์ฒ˜๋ฆฌ ์†๋„์™€ ์˜ค๋ฅ˜์œจ\n", "t0 = time.perf_counter()\n", "c = clean(data)\n", "dt = time.perf_counter() - t0\n", "remain = int(c[PARAMS].isna().sum().sum())\n", "print(f\"์ „์ฒ˜๋ฆฌ {len(data):,}๊ฑด, {len(data) / dt:,.0f}๊ฑด/์ดˆ, ์ฒ˜๋ฆฌ ํ›„ ๋‚จ์€ ๊ฒฐ์ธก {remain}๊ฑด, ์˜ค๋ฅ˜์œจ {remain / len(data):.1%}\")\n", "\n", "# 2) ์ „์ฒ˜๋ฆฌ ์ „ํ›„ ๋น„๊ต: ์„ผ์„œ ๊ณ ์žฅ ์œ ํ˜•๋ณ„๋กœ ๋ฌด์—‡์ด ์ฒ˜๋ฆฌ๋˜์—ˆ๋Š”๊ฐ€\n", "flag_cols = [f\"{p}_flag\" for p in PARAMS]\n", "c[\"์ฒ˜๋ฆฌ๋‚ด์—ญ\"] = c[flag_cols].apply(lambda r: \",\".join(sorted({x for x in r if x})) or \"-\", axis=1)\n", "print(\"\\n[์‹ค์ œ ๊ณ ์žฅ ์œ ํ˜• x ์ „์ฒ˜๋ฆฌ ์ฒ˜๋ฆฌ ๋‚ด์—ญ] (ํ–‰ ์ˆ˜)\")\n", "print(pd.crosstab(c[\"fault\"], c[\"์ฒ˜๋ฆฌ๋‚ด์—ญ\"]).to_string())\n", "print(f\"๊ฒฐ์ธก ์…€ ์ˆ˜: ์ „์ฒ˜๋ฆฌ ์ „ {int(data[PARAMS].isna().sum().sum())} -> ํ›„ {remain}\")\n", "\n", "# 3) ๊ฒฐ์ธก ๋ณด์ • ์ •ํ™•๋„: ์ •์ƒ ๊ตฌ๊ฐ„์—์„œ ๊ฐ’์„ ์ผ๋ถ€ ์ง€์›Œ ๋ณด๊ณ , ์‹ค์ œ ๊ฐ’๊ณผ ๋น„๊ต\n", "rng = np.random.default_rng(3)\n", "test = data.copy()\n", "normal_idx = test.index[(test[\"fault\"] == \"none\") & (test[\"event\"] == \"none\")]\n", "holes = rng.choice(normal_idx, 120, replace=False)\n", "test.loc[holes, \"chlorine\"] = np.nan\n", "filled = clean(test).set_index([\"source_id\", \"t\"])\n", "truth = data.loc[holes].set_index([\"source_id\", \"t\"])[\"true_chlorine\"]\n", "mae = (filled.loc[truth.index, \"chlorine\"] - truth).abs().mean()\n", "mean_fill = (data[\"chlorine\"].mean() - truth).abs().mean()\n", "print(f\"\\n์ž”๋ฅ˜์—ผ์†Œ ๊ฒฐ์ธก 120๊ฑด ๋ณด์ • ์˜ค์ฐจ(MAE): ์„ ํ˜• ๋ณด๊ฐ„ {mae:.3f} mg/L vs ์ „์ฒด ํ‰๊ท  ๋Œ€์ฒด {mean_fill:.3f} mg/L\")\n", "\n", "# 4) ๋‹จ์œ„ ํ…Œ์ŠคํŠธ 5์ข…\n", "def unit_tests():\n", " base = make_dataset(days=3, seed=5, n_events=0, n_faults=0, events=[], faults=[])\n", " res = {}\n", " t = base.copy(); t.loc[(t.source_id == \"W01\") & (t.t == 30), \"ph\"] = 15.0\n", " r = clean(t).query(\"source_id == 'W01' and t == 30\").iloc[0]\n", " res[\"T1 ๋ฌผ๋ฆฌ ๋ฒ”์œ„ ๋ฐ– pH 15.0 ์ œ๊ฑฐ ํ›„ ๋ณด์ •\"] = r[\"ph_flag\"] == \"range\" and 6.0 < r[\"ph\"] < 8.0\n", " t = base.copy(); t.loc[(t.source_id == \"W03\") & (t.t == 40), \"turbidity\"] = 38.0\n", " r = clean(t).query(\"source_id == 'W03' and t == 40\").iloc[0]\n", " res[\"T2 ๋‹จ๋ฐœ ์ŠคํŒŒ์ดํฌ(ํƒ๋„ 38) ์ œ๊ฑฐ\"] = r[\"turbidity_flag\"] == \"spike\" and r[\"turbidity\"] < 1.0\n", " ev = make_dataset(days=3, seed=5, events=[(2, \"sewage\", 30, 12, 1.0)], faults=[])\n", " r = clean(ev).query(\"source_id == 'W03' and 30 <= t <= 45\")\n", " res[\"T3 ์ง€์† ์ƒ์Šน(ํ•˜์ˆ˜ ์œ ์ž…)์€ ๋ณด์กด\"] = (r[\"turbidity_flag\"] != \"spike\").all() and r[\"turbidity\"].max() > 2.0\n", " fz = make_dataset(days=3, seed=5, events=[], faults=[(4, \"stuck\", \"chlorine\", 20, 10)])\n", " r = clean(fz).query(\"source_id == 'W05'\")\n", " res[\"T4 10์‹œ๊ฐ„ ๊ณ ์ •๊ฐ’ -> stuck ํ‘œ์‹œ\"] = (r[\"chlorine_flag\"] == \"stuck\").sum() == 9\n", " t = base.copy(); t.loc[(t.source_id == \"W07\") & t.t.isin([10, 11]), PARAMS] = np.nan\n", " t.loc[(t.source_id == \"W07\") & t.t.between(40, 47), PARAMS] = np.nan\n", " r = clean(t).query(\"source_id == 'W07'\").set_index(\"t\")\n", " res[\"T5 2์‹œ๊ฐ„ ๊ฒฐ์ธก ๋ณด๊ฐ„ / 8์‹œ๊ฐ„ ๊ฒฐ์ธก ํ‘œ์‹œ\"] = (r.loc[[10, 11], \"ph_flag\"] == \"interp\").all() and (r.loc[40:47, \"ph_flag\"] == \"long_gap\").all()\n", " return res\n", "\n", "\n", "res = unit_tests()\n", "print(\"\\n[๋‹จ์œ„ ํ…Œ์ŠคํŠธ]\")\n", "for k, v in res.items():\n", " print(f\" [{'PASS' if v else 'FAIL'}] {k}\")\n", "print(f\"๋‹จ์œ„ ํ…Œ์ŠคํŠธ {sum(res.values())}/5 ํ†ต๊ณผ\")\n", "\n", "# 5) ์ƒํ™ฉ๋ณ„(์ฃผ๊ฐ„/์•ผ๊ฐ„/์•…์ฒœํ›„) ์ž„๊ณ„๊ฐ’ ๋™์  ์กฐ์ • ํšจ๊ณผ: ์ตœ์†Œ ๋ณ€ํ™”ํญ ํ•˜ํ•œ์„ ๋„๊ณ  ๋ฐฐ์ˆ˜๋งŒ ๋น„๊ต\n", "# (๊ณ ์žฅ์ด ์—†๋Š” ํ–‰์ด ์ŠคํŒŒ์ดํฌ๋กœ ์ž˜๋ชป ์ œ๊ฑฐ๋œ ๊ฑด์ˆ˜ / ์‹ค์ œ ์ŠคํŒŒ์ดํฌ ๊ณ ์žฅ ๊ฒ€์ถœ ๊ฑด์ˆ˜)\n", "no_floor = {p: 0.0 for p in MIN_JUMP}\n", "for name, k in [(\"๊ณ ์ • ์ž„๊ณ„(๋ชจ๋“  ์ƒํ™ฉ 3๋ฐฐ)\", {\"day\": 3.0, \"night\": 3.0, \"storm\": 3.0}), (\"์ƒํ™ฉ๋ณ„ ๋™์  ์ž„๊ณ„\", SPIKE_K)]:\n", " cc = clean(data, spike_k=k, min_jump=no_floor)\n", " sp = (cc[flag_cols] == \"spike\").any(axis=1)\n", " wrong, caught = int((sp & (cc[\"fault\"] != \"spike\")).sum()), int((sp & (cc[\"fault\"] == \"spike\")).sum())\n", " print(f\"{name}: ์ •์ƒ ๋ณ€๋™ ์˜ค์ œ๊ฑฐ {wrong}๊ฑด(์•ผ๊ฐ„ยท์•…์ฒœํ›„ {int((sp & (cc['fault'] != 'spike') & (cc['cond'] != 'day')).sum())}๊ฑด), ์ŠคํŒŒ์ดํฌ ๊ฒ€์ถœ {caught}๊ฑด\")\n", "print(\"์šด์šฉ ์„ค์ •์€ ๋™์  ์ž„๊ณ„ + ํ•ญ๋ชฉ๋ณ„ ์ตœ์†Œ ๋ณ€ํ™”ํญ ํ•˜ํ•œ(MIN_JUMP)์„ ํ•จ๊ป˜ ์“ด๋‹ค.\")\n", "\n", "# 6) ์ „ํ›„ ๋น„๊ต ๊ทธ๋ฆผ\n", "sid = data.loc[data[\"fault\"].isin([\"spike\", \"dropout\"]), \"source_id\"].iloc[0]\n", "raw, cl = data[data.source_id == sid], c[c.source_id == sid]\n", "fig, ax = plt.subplots(2, 1, figsize=(10, 6), sharex=True)\n", "for a, p in zip(ax, [\"turbidity\", \"chlorine\"]):\n", " a.plot(raw[\"t\"], raw[p], color=\"lightgray\", lw=3, label=\"raw sensor\")\n", " a.plot(cl[\"t\"], cl[p], color=\"tab:blue\", lw=1, label=\"cleaned\")\n", " a.set_ylabel(p)\n", " a.legend(loc=\"upper right\")\n", "ax[1].set_xlabel(\"hour\")\n", "plt.suptitle(f\"{sid} before / after preprocessing (synthetic)\")\n", "plt.tight_layout()\n", "plt.show()\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "
\n", "๐Ÿ’ก [์ฐธ๊ณ ] ๋ฏธ์…˜ 02 ์˜ˆ์ƒ ์‹คํ–‰ ๊ฒฐ๊ณผ (Output)\n", "\n", "```text\n", "์ „์ฒ˜๋ฆฌ 2,688๊ฑด, 22,643๊ฑด/์ดˆ, ์ฒ˜๋ฆฌ ํ›„ ๋‚จ์€ ๊ฒฐ์ธก 0๊ฑด, ์˜ค๋ฅ˜์œจ 0.0%\n", "\n", "[์‹ค์ œ ๊ณ ์žฅ ์œ ํ˜• x ์ „์ฒ˜๋ฆฌ ์ฒ˜๋ฆฌ ๋‚ด์—ญ] (ํ–‰ ์ˆ˜)\n", "์ฒ˜๋ฆฌ๋‚ด์—ญ - long_gap spike stuck\n", "fault \n", "drift 78 8 0 0\n", "dropout 0 17 0 0\n", "none 2485 0 0 0\n", "spike 1 0 2 0\n", "stuck 6 0 0 91\n", "๊ฒฐ์ธก ์…€ ์ˆ˜: ์ „์ฒ˜๋ฆฌ ์ „ 125 -> ํ›„ 0\n", "\n", "์ž”๋ฅ˜์—ผ์†Œ ๊ฒฐ์ธก 120๊ฑด ๋ณด์ • ์˜ค์ฐจ(MAE): ์„ ํ˜• ๋ณด๊ฐ„ 0.042 mg/L vs ์ „์ฒด ํ‰๊ท  ๋Œ€์ฒด 0.122 mg/L\n", "\n", "[๋‹จ์œ„ ํ…Œ์ŠคํŠธ]\n", " [PASS] T1 ๋ฌผ๋ฆฌ ๋ฒ”์œ„ ๋ฐ– pH 15.0 ์ œ๊ฑฐ ํ›„ ๋ณด์ •\n", " [PASS] T2 ๋‹จ๋ฐœ ์ŠคํŒŒ์ดํฌ(ํƒ๋„ 38) ์ œ๊ฑฐ\n", " [PASS] T3 ์ง€์† ์ƒ์Šน(ํ•˜์ˆ˜ ์œ ์ž…)์€ ๋ณด์กด\n", " [PASS] T4 10์‹œ๊ฐ„ ๊ณ ์ •๊ฐ’ -> stuck ํ‘œ์‹œ\n", " [PASS] T5 2์‹œ๊ฐ„ ๊ฒฐ์ธก ๋ณด๊ฐ„ / 8์‹œ๊ฐ„ ๊ฒฐ์ธก ํ‘œ์‹œ\n", "๋‹จ์œ„ ํ…Œ์ŠคํŠธ 5/5 ํ†ต๊ณผ\n", "๊ณ ์ • ์ž„๊ณ„(๋ชจ๋“  ์ƒํ™ฉ 3๋ฐฐ): ์ •์ƒ ๋ณ€๋™ ์˜ค์ œ๊ฑฐ 7๊ฑด(์•ผ๊ฐ„ยท์•…์ฒœํ›„ 6๊ฑด), ์ŠคํŒŒ์ดํฌ ๊ฒ€์ถœ 3๊ฑด\n", "์ƒํ™ฉ๋ณ„ ๋™์  ์ž„๊ณ„: ์ •์ƒ ๋ณ€๋™ ์˜ค์ œ๊ฑฐ 1๊ฑด(์•ผ๊ฐ„ยท์•…์ฒœํ›„ 0๊ฑด), ์ŠคํŒŒ์ดํฌ ๊ฒ€์ถœ 2๊ฑด\n", "์šด์šฉ ์„ค์ •์€ ๋™์  ์ž„๊ณ„ + ํ•ญ๋ชฉ๋ณ„ ์ตœ์†Œ ๋ณ€ํ™”ํญ ํ•˜ํ•œ(MIN_JUMP)์„ ํ•จ๊ป˜ ์“ด๋‹ค.\n", "```\n", "
\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "---\n", "\n", "## ๐ŸŽฏ ๋ฏธ์…˜ 03. ์Œ์šฉ ์ ํ•ฉ/๋ถ€์ ํ•ฉ ํŒ์ • 1์ฐจ ๊ธฐ์ค€์น˜ ํ•„ํ„ฐ๋ง Baseline ์•Œ๊ณ ๋ฆฌ์ฆ˜ ๊ตฌํ˜„\n", "**์†Œ์š”์‹œ๊ฐ„**: ์•ฝ 2.5์‹œ๊ฐ„\n", "\n", "### ๐Ÿ“‹ ๋ฏธ์…˜ ๋ฐฐ๊ฒฝ ๋ฐ ์ˆ˜ํ–‰ ์ง€์นจ\n", "#### ๋ฏธ์…˜ ๋ฐฐ๊ฒฝ ๋ฐ ๋‹น์œ„์„ฑ\n", "\n", "์Œ์šฉ ์ ํ•ฉ/๋ถ€์ ํ•ฉ ํŒ์ • 1์ฐจ ๊ธฐ์ค€์น˜ ํ•„ํ„ฐ๋ง Baseline ์•Œ๊ณ ๋ฆฌ์ฆ˜์„ ๊ตฌํ˜„ํ•œ๋‹ค. ์‹ค์ œ ์ „์ˆ  ํ˜„์žฅ์—์„œ๋Š” ๋‹จ์ˆœ ํƒ์ง€์— ๊ทธ์น˜์ง€ ์•Š๊ณ  ์‹ ์†ํ•œ ์œ„ํ˜‘ ํŒ๋‹จ๊ณผ ์‹ ๋ขฐ๋„ ๋†’์€ ์˜์‚ฌ๊ฒฐ์ •์ด ํ•„์ˆ˜์ ์ด๋ฏ€๋กœ, **์Œ์šฉ ์ ํ•ฉยท๋ถ€์ ํ•ฉ ํŒ์ •์˜ 1์ฐจ ๊ธฐ์ค€์น˜ ํ•„ํ„ฐ๋ง Baseline ์•Œ๊ณ ๋ฆฌ์ฆ˜์„ ๊ตฌํ˜„ํ•˜๊ณ  ์ •ํ™•๋„๋ฅผ ์ธก์ •ํ•œ๋‹ค.** ๊ณผ์—…์„ ์™„์ˆ˜ํ•˜์—ฌ ์˜คํŒ์„ ๋ฐฉ์ง€ํ•˜๊ณ  ์ž‘์ „ ๋Œ€์‘๋ ฅ์„ ๊ทน๋Œ€ํ™”ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.\n", "\n", "์ด ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด ๋ณธ ๋ฏธ์…˜์—์„œ๋Š” **์ „์ฒ˜๋ฆฌํ•œ ์ž๋ฃŒ์— pH, ํƒ๋„, ์ž”๋ฅ˜์—ผ์†Œ, ์ค‘๊ธˆ์†, ๋Œ€์žฅ๊ท ์˜ ์ฃผ์˜ยท์œ„ํ—˜ ๊ฐ€์ • ๊ธฐ์ค€์„ ์ ์šฉํ•ด ์•ˆ์ „ยท์ฃผ์˜ยท์œ„ํ—˜ 3๋“ฑ๊ธ‰ ํŒ์ •ํ•˜๊ณ  ์ •ํ™•๋„๋ฅผ ์ธก์ •ํ•˜๋Š”** ์‹ค์Šต์„ ์ฒด๊ณ„์ ์œผ๋กœ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค.\n", "\n", "ํ•™์Šต์ž๋Š” ์‹ค์Šต๊ฐ€์ด๋“œ์— ์ œ์‹œ๋œ ์ด 4๊ฐœ ์„ธ๋ถ€ ํ™œ๋™(๊ธฐ๋ณธ ๊ธฐ๋Šฅ ๊ตฌํ˜„ โž” ๊ฒฐ๊ณผ ์ง€ํ‘œ ๊ฒ€์ฆ โž” ํŒŒ๋ผ๋ฏธํ„ฐ ํŠœ๋‹ ๋ฐ ๋„์ „๊ณผ์ œ)์„ ์ˆœ์ฐจ์ ์œผ๋กœ ์ง„ํ–‰ํ•˜๋ฉฐ ํ•ต์‹ฌ ํŒ์ • ๊ธฐ์ค€์„ ๋‹ฌ์„ฑํ•ฉ๋‹ˆ๋‹ค.\n", "\n", "**์ด๋ฒˆ ๊ฒฐ๊ณผ๋ฌผ:** `src/models/wel03_baseline_model.py ๋ฐ ๊ธฐ๋ณธ ์ถ”๋ก  ๊ฒฐ๊ณผ ๋ณด๊ณ ์„œ` ํ˜•ํƒœ์˜ ๊ฒฐ๊ณผ๋ฌผ์„ ์™„์„ฑํ•œ๋‹ค.\n", "\n", "### ๐Ÿ” ํ‰๊ฐ€ ๋ฐ ๊ฒ€์ฆ ๊ธฐ์ค€ (PASS ์กฐ๊ฑด)\n", "#### ๋ฏธ์…˜ ํŒ์ •\n", "\n", "**ํ‰๊ฐ€ ๊ตฌ๋ถ„:** ํ•„์ˆ˜\n", "\n", "**ํ•ต์‹ฌ ํ‰๊ฐ€์˜์—ญ:** ์•Œ๊ณ ๋ฆฌ์ฆ˜์˜ ๋…ผ๋ฆฌ์  ํ๋ฆ„ ๋ฐ ๊ตฐ์‚ฌ์  ํŒ๋‹จ ๊ธฐ์ค€์˜ ํˆฌ๋ช…์„ฑ\n", "\n", "#### ๋ฏธ์…˜ PASS ์กฐ๊ฑด\n", "\n", "- ์ •๋Ÿ‰ ๊ธฐ์ค€: ๊ธฐ๋ณธ ๋ฐ์ดํ„ฐ์…‹ ๋Œ€์ƒ ํƒ์ง€/์ฒ˜๋ฆฌ ์ •ํ™•๋„ 75% ์ด์ƒ ๋‹ฌ์„ฑ\n", "\n", "\n", "- ์ง„ํ–‰ ๊ธฐ์ค€: ๋‹จ์œ„ ๊ธฐ๋Šฅ ๋™์ž‘ ํ™•์ธ ๋ฐ ๊ธฐ๋ณธ ์‹œ๋‚˜๋ฆฌ์˜ค ์‹คํ–‰ ๋กœ๊ทธ ์ƒ์„ฑ\n", "\n", "\n", "- ๊ฒ€์ฆ ๋ฐฉ๋ฒ•: ๊ธฐ์ค€ ๋ฒค์น˜๋งˆํฌ ๋ฐ์ดํ„ฐ์…‹ ๋Œ€์ƒ ์ถ”๋ก  ๊ฒฐ๊ณผ ์˜ค์ฐจ์œจ ๋น„๊ต\n", "\n", "\n", "#### ์ฑ„์ ์ž ํŒ์ • ๋ฉ”๋ชจ\n", "\n", "> ๋‹จ์ˆœ ์ฝ”๋“œ ์‹คํ–‰ ์—ฌ๋ถ€๋ฟ๋งŒ ์•„๋‹ˆ๋ผ ์ถœ๋ ฅ๋œ ์ˆ˜์น˜์™€ ๊ทผ๊ฑฐ ์ž๋ฃŒ๊ฐ€ ๊ตฐ ์‹ค๋ฌด ์š”๊ตฌ์‚ฌํ•ญ์— ๋ถ€ํ•ฉํ•˜๋Š”์ง€ ๋ฉด๋ฐ€ํžˆ ํ™•์ธํ•œ๋‹ค.\n", "\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "\"\"\"๊ฐ€์ • ๊ธฐ์ค€์น˜(์ฃผ์˜ยท์œ„ํ—˜)๋ฅผ ๋„˜๋Š” ํ•ญ๋ชฉ์ด ์žˆ์œผ๋ฉด ์Œ์šฉ ๋ถ€์ ํ•ฉ ์˜์‹ฌ์œผ๋กœ ํŒ์ •ํ•˜๋Š” 1์ฐจ ํ•„ํ„ฐ๋ง Baseline ์•Œ๊ณ ๋ฆฌ์ฆ˜์„ ๊ตฌํ˜„ํ•˜๊ณ , ๊ธฐ์ค€ ๋ฒค์น˜๋งˆํฌ ๋ฐ์ดํ„ฐ์—์„œ ์ •ํ™•๋„ยท์˜ค์ฐจ ์œ ํ˜•ยทํŒ์ • ๊ทผ๊ฑฐยท์ฒ˜๋ฆฌ ์ง€์—ฐ์„ ํ™•์ธํ•œ๋‹ค.\"\"\"\n", "import time\n", "import numpy as np\n", "import pandas as pd\n", "import matplotlib.pyplot as plt\n", "\n", "# ---------------------------------------------------------------------------\n", "# [๊ต์œก์šฉ๊ฐ€์ƒ์ž๋ฃŒ] ์•ผ์ „ ๊ธ‰์ˆ˜์› ๊ฐ€์ƒ ์ˆ˜์งˆ ์„ผ์„œ ๋ฐ์ดํ„ฐ ์ƒ์„ฑ๊ธฐ (์‹ค์ œ ๋ถ€๋Œ€ยท์œ„์น˜ยท๊ฒ€์‚ฌ ๊ฒฐ๊ณผ ์—†์Œ)\n", "# ๊ฐ€์ƒ ๊ธ‰์ˆ˜์› 8๊ณณ x 14์ผ x 1์‹œ๊ฐ„ ๊ฐ„๊ฒฉ. ์‹ค์ œ ์ˆ˜์งˆ(true_*)ยท์„ผ์„œ ์ธก์ •๊ฐ’ยท์˜ค์—ผ ์‚ฌ๊ฑดยท์„ผ์„œ ๊ณ ์žฅ ์ •๋‹ต์„ ํ•จ๊ป˜ ๋งŒ๋“ ๋‹ค.\n", "# ๊ธฐ์ค€์น˜๋Š” ๊ต์œก์šฉ ๊ฐ€์ •๊ฐ’์ด๋ฉฐ, ์‹ค์ œ ์Œ์šฉ ํŒ์ •์€ ๊ณต์ธ ๊ฒ€์‚ฌ ๊ฒฐ๊ณผ์™€ ์˜๋ฌด(์˜ˆ๋ฐฉ์˜ํ•™) ๋‹ด๋‹น ๊ธฐ์ค€์— ๋”ฐ๋ฅธ๋‹ค.\n", "# ---------------------------------------------------------------------------\n", "PARAMS = [\"ph\", \"turbidity\", \"chlorine\", \"metal\", \"ecoli\"]\n", "SOURCES = [(\"W01\", \"water_truck\", 1.0, 2.0), (\"W02\", \"water_truck\", 3.5, 1.2), (\"W03\", \"purification_unit\", 2.2, 4.1),\n", " (\"W04\", \"purification_unit\", 5.0, 3.3), (\"W05\", \"well\", 0.8, 5.6), (\"W06\", \"well\", 4.4, 6.0),\n", " (\"W07\", \"river_intake\", 6.3, 1.8), (\"W08\", \"river_intake\", 6.9, 4.9)]\n", "BASE = {\"water_truck\": (7.2, 0.25, 0.75, 1.5), \"purification_unit\": (7.0, 0.20, 0.85, 1.2),\n", " \"well\": (6.8, 0.45, 0.50, 3.0), \"river_intake\": (7.3, 0.55, 0.60, 2.0)} # pH, ํƒ๋„(NTU), ์ž”๋ฅ˜์—ผ์†Œ(mg/L), ์ค‘๊ธˆ์†(ug/L)\n", "LIMITS = {\"ph\": (5.8, 8.5), \"turbidity\": (1.0, 2.0), \"chlorine\": (0.2, 0.1), \"metal\": (5.0, 10.0), \"ecoli\": (1, 1)} # (์ฃผ์˜, ์œ„ํ—˜) ๊ฐ€์ • ๊ธฐ์ค€\n", "EVENT_TYPES = { # ์˜ค์—ผ ์‚ฌ๊ฑด: ๋ณ€์ˆ˜๋ณ„ ์ตœ๋Œ€ ๋ณ€ํ™”๋Ÿ‰, ์ตœ๋Œ€์น˜ ๋„๋‹ฌ ์‹œ๊ฐ„(h)\n", " \"sewage\": ({\"turbidity\": 2.8, \"chlorine\": -0.5, \"ecoli\": 30, \"ph\": -0.3}, 4),\n", " \"metal\": ({\"metal\": 22.0, \"ph\": -0.5}, 4),\n", " \"disinfect_fail\": ({\"chlorine\": -0.8, \"ecoli\": 6}, 6),\n", " \"chemical\": ({\"ph\": 1.7, \"turbidity\": 1.0}, 3),\n", " \"creeping_metal\": ({\"metal\": 13.0}, 18),\n", "}\n", "FAULT_TYPES = [\"stuck\", \"spike\", \"drift\", \"dropout\"]\n", "NOISE = {\"ph\": 0.04, \"turbidity\": 0.06, \"chlorine\": 0.03, \"metal\": 0.35}\n", "DIGITS = {\"ph\": 2, \"turbidity\": 2, \"chlorine\": 2, \"metal\": 1}\n", "\n", "\n", "def make_dataset(days=14, seed=7, n_events=6, n_faults=10, events=None, faults=None, sources=None):\n", " \"\"\"events: [(๊ธ‰์ˆ˜์› ๋ฒˆํ˜ธ, ์‚ฌ๊ฑด ์œ ํ˜•, ์‹œ์ž‘ ์‹œ๊ฐ h, ์ง€์† h, ๊ฐ•๋„)], faults: [(๊ธ‰์ˆ˜์› ๋ฒˆํ˜ธ, ๊ณ ์žฅ ์œ ํ˜•, ํ•ญ๋ชฉ, ์‹œ์ž‘ h, ์ง€์† h)]\"\"\"\n", " rng = np.random.default_rng(seed)\n", " sources = sources or SOURCES\n", " T = days * 24\n", " tt = np.arange(T)\n", " hour = tt % 24\n", " night = (hour < 6) | (hour >= 20)\n", " rain = np.zeros(T)\n", " for _ in range(max(1, days // 4)):\n", " s0, d = int(rng.integers(0, T - 14)), int(rng.integers(6, 14))\n", " rain[s0:s0 + d] = np.maximum(rain[s0:s0 + d], rng.uniform(0.6, 1.0))\n", " if events is None:\n", " events = [(int(rng.integers(len(sources))), str(rng.choice(list(EVENT_TYPES))), int(rng.integers(min(24, T // 4), max(T // 4 + 1, T - 48))),\n", " int(rng.integers(10, 30)), 1.0) for _ in range(n_events)]\n", " if faults is None:\n", " faults = [(int(rng.integers(len(sources))), str(rng.choice(FAULT_TYPES)), str(rng.choice([\"ph\", \"turbidity\", \"chlorine\", \"metal\"])),\n", " int(rng.integers(12, max(13, T - 30))), int(rng.integers(6, 24))) for _ in range(n_faults)]\n", " out = []\n", " for si, (sid, stype, x, y) in enumerate(sources):\n", " ph0, tb0, cl0, mt0 = BASE[stype]\n", " tr = {\"ph\": ph0 + 0.08 * np.sin(2 * np.pi * hour / 24) + rng.normal(0, 0.03, T),\n", " \"turbidity\": tb0 * (1 + 0.1 * rng.normal(0, 1, T)),\n", " \"chlorine\": (cl0 * np.exp(-0.022 * hour) if stype == \"water_truck\" else cl0 + rng.normal(0, 0.03, T)),\n", " \"metal\": mt0 + rng.normal(0, 0.2, T), \"ecoli\": np.zeros(T)}\n", " if stype in (\"river_intake\", \"well\"):\n", " tr[\"turbidity\"] = tr[\"turbidity\"] + rain * (0.9 if stype == \"river_intake\" else 0.45)\n", " f_all, etype = np.zeros(T), np.array([\"none\"] * T, dtype=object)\n", " for (s_i, et, onset, dur, scale) in events:\n", " if s_i != si:\n", " continue\n", " delta, ramp = EVENT_TYPES[et]\n", " f = np.clip((tt - onset + 1) / ramp, 0, 1) * ((tt >= onset) & (tt < onset + dur + ramp))\n", " for k, v in delta.items():\n", " tr[k] = tr[k] + v * f * scale\n", " etype[f > 0] = et\n", " f_all = np.maximum(f_all, f)\n", " tr[\"chlorine\"] = np.clip(tr[\"chlorine\"], 0.02, None)\n", " tr[\"turbidity\"] = np.clip(tr[\"turbidity\"], 0.05, None)\n", " tr[\"metal\"] = np.clip(tr[\"metal\"], 0.1, None)\n", " grade = np.where(f_all >= 0.2, 2, 0)\n", " mild = (tr[\"turbidity\"] > LIMITS[\"turbidity\"][0]) | (tr[\"chlorine\"] < LIMITS[\"chlorine\"][0]) | (tr[\"metal\"] > LIMITS[\"metal\"][0])\n", " grade = np.where((grade == 0) & mild, 1, grade)\n", " ms = {}\n", " for p in [\"ph\", \"turbidity\", \"chlorine\", \"metal\"]:\n", " sd = NOISE[p] * np.where(night, 1.5, 1.0) * (tr[p] if p == \"turbidity\" else 1)\n", " ms[p] = np.round(np.clip(tr[p] + rng.normal(0, 1, T) * sd, 0, None), DIGITS[p])\n", " ms[\"ecoli\"] = (rng.poisson(tr[\"ecoli\"]) + (rng.random(T) < 0.004)).astype(float)\n", " fault, fparam = np.array([\"none\"] * T, dtype=object), np.array([\"-\"] * T, dtype=object)\n", " for (s_i, ft, p, s0, d) in faults:\n", " if s_i != si:\n", " continue\n", " seg = slice(s0, min(T, s0 + d))\n", " if ft == \"stuck\":\n", " ms[p][seg] = ms[p][s0]\n", " elif ft == \"spike\":\n", " seg = slice(s0, s0 + 1)\n", " ms[p][s0] = {\"ph\": 11.8, \"turbidity\": 38.0, \"chlorine\": 0.0, \"metal\": 75.0}[p]\n", " elif ft == \"drift\":\n", " ramp = np.linspace(0, 1, seg.stop - s0)\n", " ms[p][seg] = np.clip(ms[p][seg] + ramp * {\"ph\": -1.6, \"turbidity\": 2.6, \"chlorine\": -0.75, \"metal\": 12.0}[p], 0, None).round(DIGITS[p])\n", " elif ft == \"dropout\":\n", " for q in PARAMS:\n", " ms[q][seg] = np.nan\n", " p = \"all\"\n", " fault[seg], fparam[seg] = ft, p\n", " df = pd.DataFrame({\"source_id\": sid, \"source_type\": stype, \"x_km\": x, \"y_km\": y, \"t\": tt,\n", " \"cond\": np.where(rain > 0, \"storm\", np.where(night, \"night\", \"day\")),\n", " **{p: ms[p] for p in PARAMS}, **{f\"true_{p}\": tr[p] for p in PARAMS},\n", " \"event\": etype, \"fault\": fault, \"fault_param\": fparam, \"grade\": grade})\n", " out.append(df)\n", " return pd.concat(out, ignore_index=True)\n", "# ---------------------------------------------------------------------------\n", "\n", "# ---- ์ „์ฒ˜๋ฆฌ ๊ณตํ†ต ํ•จ์ˆ˜: ๋ฌผ๋ฆฌ ๋ฒ”์œ„ ์ ๊ฒ€ -> ๊ณ ์ •๊ฐ’(stuck) ํ‘œ์‹œ -> ๋‹จ๋ฐœ ์ŠคํŒŒ์ดํฌ ์ œ๊ฑฐ -> ๊ฒฐ์ธก ๋ณด์ • ----\n", "PHYS = {\"ph\": (0, 14), \"turbidity\": (0, 1000), \"chlorine\": (0, 10), \"metal\": (0, 1000), \"ecoli\": (0, 100000)}\n", "SPIKE_K = {\"day\": 6.0, \"night\": 7.0, \"storm\": 9.0} # ์ƒํ™ฉ๋ณ„(์ฃผ๊ฐ„/์•ผ๊ฐ„/์•…์ฒœํ›„) ์ŠคํŒŒ์ดํฌ ํŒ์ • ๋ฐฐ์ˆ˜\n", "MIN_JUMP = {\"ph\": 0.8, \"turbidity\": 1.5, \"chlorine\": 0.3, \"metal\": 6.0}\n", "STUCK_RUN, SHORT_GAP = 6, 3\n", "\n", "\n", "def clean(df, spike_k=None, min_jump=None):\n", " \"\"\"๊ธ‰์ˆ˜์›๋ณ„ ์‹œ๊ณ„์—ด์„ ์ •์ œํ•˜๊ณ  ํ•ญ๋ชฉ๋ณ„ ์ฒ˜๋ฆฌ ๋‚ด์—ญ(<ํ•ญ๋ชฉ>_flag: range/stuck/spike/interp/long_gap)์„ ๋‚จ๊ธด๋‹ค.\"\"\"\n", " spike_k = spike_k or SPIKE_K\n", " min_jump = MIN_JUMP if min_jump is None else min_jump\n", " out = df.sort_values([\"source_id\", \"t\"]).reset_index(drop=True)\n", " for sid, idx in out.groupby(\"source_id\").groups.items():\n", " idx = np.asarray(idx)\n", " kk = out.loc[idx, \"cond\"].map(spike_k).to_numpy(float)\n", " for p in PARAMS:\n", " v = out.loc[idx, p].to_numpy(float).copy()\n", " fl = np.array([\"\"] * len(v), dtype=object)\n", " lo, hi = PHYS[p]\n", " bad = (v < lo) | (v > hi)\n", " v[bad], fl[bad] = np.nan, \"range\"\n", " if p != \"ecoli\":\n", " chg = np.r_[True, v[1:] != v[:-1]] # ๊ฐ™์€ ๊ฐ’์ด STUCK_RUN์‹œ๊ฐ„ ์ด์ƒ ๋ฐ˜๋ณต = ๊ณ ์ •๊ฐ’ ๊ณ ์žฅ ์˜์‹ฌ\n", " rid = np.cumsum(chg)\n", " rlen = np.bincount(rid)[rid]\n", " fl[(rlen >= STUCK_RUN) & ~chg & ~np.isnan(v)] = \"stuck\"\n", " d = np.abs(np.diff(v[~np.isnan(v)]))\n", " sig = 1.4826 * np.median(d) + 1e-6 if len(d) else 1.0\n", " up, dn = v - np.r_[np.nan, v[:-1]], v - np.r_[v[1:], np.nan]\n", " thr = np.maximum(kk * sig, min_jump[p])\n", " spike = (np.abs(up) > thr) & (np.abs(dn) > thr) & (np.sign(up) == np.sign(dn)) # ํŠ€์—ˆ๋‹ค๊ฐ€ ๋ฐ”๋กœ ๋Œ์•„์˜จ ๊ฐ’๋งŒ\n", " v[spike], fl[spike] = np.nan, \"spike\"\n", " s = pd.Series(v)\n", " na = s.isna().to_numpy()\n", " grp = np.cumsum(np.r_[True, na[1:] != na[:-1]])\n", " glen = pd.Series(na.astype(int)).groupby(grp).transform(\"sum\").to_numpy()\n", " lin, ff = s.interpolate(limit_area=\"inside\").to_numpy(), s.ffill().bfill().to_numpy()\n", " short, longg = na & (glen <= SHORT_GAP), na & (glen > SHORT_GAP)\n", " v = np.where(short, np.where(np.isnan(lin), ff, lin), np.where(longg, ff, v))\n", " fl[short & (fl == \"\")] = \"interp\"\n", " fl[longg] = \"long_gap\"\n", " out.loc[idx, p] = v\n", " out.loc[idx, f\"{p}_flag\"] = fl\n", " return out\n", "\n", "# ---- ๊ฐ€์ • ๊ธฐ์ค€์น˜ ํŒ์ •(Baseline)๊ณผ ์‹œ๊ณ„์—ด ํŠน์ง• ๊ณตํ†ต ํ•จ์ˆ˜ ----\n", "GRADE_NAME = {0: \"์•ˆ์ „\", 1: \"์ฃผ์˜\", 2: \"์œ„ํ—˜\"}\n", "\n", "\n", "def rule_grade(df):\n", " \"\"\"๊ธฐ์ค€์น˜ 1์ฐจ ํ•„ํ„ฐ: ์œ„ํ—˜(์Œ์šฉ ๋ถ€์ ํ•ฉ ์˜์‹ฌ) = ํ•œ ํ•ญ๋ชฉ์ด๋ผ๋„ ์œ„ํ—˜ ๊ธฐ์ค€ ์ดˆ๊ณผ, ์ฃผ์˜ = ์ฃผ์˜ ๊ธฐ์ค€ ์ดˆ๊ณผ.\"\"\"\n", " ph, tb, cl, mt, ec = (df[p].to_numpy(float) for p in PARAMS)\n", " danger = (ph < LIMITS[\"ph\"][0]) | (ph > LIMITS[\"ph\"][1]) | (tb > LIMITS[\"turbidity\"][1]) | (cl < LIMITS[\"chlorine\"][1]) \\\n", " | (mt > LIMITS[\"metal\"][1]) | (ec >= LIMITS[\"ecoli\"][1])\n", " caution = (tb > LIMITS[\"turbidity\"][0]) | (cl < LIMITS[\"chlorine\"][0]) | (mt > LIMITS[\"metal\"][0])\n", " return np.where(danger, 2, np.where(caution, 1, 0))\n", "\n", "\n", "def reasons(row):\n", " r = []\n", " if not LIMITS[\"ph\"][0] <= row[\"ph\"] <= LIMITS[\"ph\"][1]:\n", " r.append(f\"pH {row['ph']:.2f}\")\n", " if row[\"turbidity\"] > LIMITS[\"turbidity\"][0]:\n", " r.append(f\"ํƒ๋„ {row['turbidity']:.2f}NTU\")\n", " if row[\"chlorine\"] < LIMITS[\"chlorine\"][0]:\n", " r.append(f\"์ž”๋ฅ˜์—ผ์†Œ {row['chlorine']:.2f}mg/L\")\n", " if row[\"metal\"] > LIMITS[\"metal\"][0]:\n", " r.append(f\"์ค‘๊ธˆ์† {row['metal']:.1f}ug/L\")\n", " if row[\"ecoli\"] >= 1:\n", " r.append(f\"๋Œ€์žฅ๊ท  {row['ecoli']:.0f}\")\n", " return \", \".join(r) or \"-\"\n", "\n", "\n", "FEATS = [f\"{p}{s}\" for p in [\"ph\", \"turbidity\", \"chlorine\", \"metal\"] for s in [\"\", \"_d1\", \"_d3\", \"_dev\", \"_sd6\"]] + \\\n", " [\"ecoli\", \"ecoli_3h\", \"storm\", \"night\", \"n_flag\", \"truck\", \"purif\", \"well\", \"river\"]\n", "\n", "\n", "def add_features(c):\n", " \"\"\"์‹œ๊ฐ„ ์ˆœ์„œ๋ฅผ ์ง€ํ‚ค๋Š”(๊ณผ๊ฑฐ ๊ฐ’๋งŒ ์“ฐ๋Š”) ๊ธ‰์ˆ˜์›๋ณ„ ํŠน์ง•: ๋ณ€ํ™”๋Ÿ‰, 24์‹œ๊ฐ„ ๊ธฐ์ค€์„  ๋Œ€๋น„ ํŽธ์ฐจ, 6์‹œ๊ฐ„ ๋ณ€๋™์„ฑ, ๋Œ€์žฅ๊ท  ๋ˆ„์  ๋“ฑ.\"\"\"\n", " c = c.sort_values([\"source_id\", \"t\"]).reset_index(drop=True)\n", " g = c.groupby(\"source_id\")\n", " for p in [\"ph\", \"turbidity\", \"chlorine\", \"metal\"]:\n", " c[f\"{p}_d1\"] = g[p].diff().fillna(0)\n", " c[f\"{p}_d3\"] = g[p].diff(3).fillna(0)\n", " base = g[p].transform(lambda s: s.shift(1).rolling(24, min_periods=4).median())\n", " c[f\"{p}_dev\"] = (c[p] - base).fillna(0)\n", " c[f\"{p}_sd6\"] = g[p].transform(lambda s: s.rolling(6, min_periods=2).std()).fillna(0)\n", " c[\"ecoli_3h\"] = g[\"ecoli\"].transform(lambda s: s.rolling(3, min_periods=1).sum())\n", " c[\"storm\"], c[\"night\"] = (c[\"cond\"] == \"storm\").astype(int), (c[\"cond\"] == \"night\").astype(int)\n", " flags = [f\"{p}_flag\" for p in PARAMS if f\"{p}_flag\" in c]\n", " c[\"n_flag\"] = sum(c[f].isin([\"stuck\", \"spike\", \"long_gap\", \"range\"]).astype(int) for f in flags) if flags else 0\n", " for k, name in [(\"truck\", \"water_truck\"), (\"purif\", \"purification_unit\"), (\"well\", \"well\"), (\"river\", \"river_intake\")]:\n", " c[k] = (c[\"source_type\"] == name).astype(int)\n", " return c\n", "\n", "\n", "# ๊ธฐ์ค€ ๋ฒค์น˜๋งˆํฌ ๋ฐ์ดํ„ฐ์…‹: ๊ฐ€์ƒ ๊ธ‰์ˆ˜์› 8๊ณณ x 14์ผ, ์˜ค์—ผ ์‚ฌ๊ฑด 10๊ฑด, ์„ผ์„œ ๊ณ ์žฅ 14๊ฑด (์ •๋‹ต ๋“ฑ๊ธ‰ grade ํฌํ•จ)\n", "bench = make_dataset(days=14, seed=11, n_events=10, n_faults=14)\n", "truth_raw = bench.sort_values([\"source_id\", \"t\"])[\"grade\"].to_numpy()\n", "\n", "# 1) ์ „์ฒ˜๋ฆฌ ์—†์ด(๊ฒฐ์ธก์€ 0์œผ๋กœ) vs ์ „์ฒ˜๋ฆฌ ํ›„ Baseline ํŒ์ •\n", "t0 = time.perf_counter()\n", "c = clean(bench)\n", "c[\"pred\"] = rule_grade(c)\n", "dt = time.perf_counter() - t0\n", "raw = bench.sort_values([\"source_id\", \"t\"]).fillna({p: 0 for p in PARAMS})\n", "acc_raw = (rule_grade(raw) == truth_raw).mean()\n", "acc = (c[\"pred\"] == c[\"grade\"]).mean()\n", "print(f\"[๊ต์œก์šฉ๊ฐ€์ƒ์ž๋ฃŒ] ๋ฒค์น˜๋งˆํฌ {len(c):,}๊ฑด(๊ธ‰์ˆ˜์› 8๊ณณ x 336์‹œ๊ฐ„)\")\n", "print(f\"์ •ํ™•๋„: ์ „์ฒ˜๋ฆฌ ์—†์Œ {acc_raw:.1%} -> ์ „์ฒ˜๋ฆฌ + ๊ธฐ์ค€์น˜ ํ•„ํ„ฐ {acc:.1%} (๋ชฉํ‘œ 75% ์ด์ƒ: {'๋‹ฌ์„ฑ' if acc >= 0.75 else '๋ฏธ๋‹ฌ'})\")\n", "print(f\"์ฒ˜๋ฆฌ ์ง€์—ฐ: ์ „์ฒด {dt * 1000:.0f}ms, 1๊ฑด๋‹น {dt / len(c) * 1000:.3f}ms\")\n", "\n", "# 2) ํ˜ผ๋™ํ–‰๋ ฌ๊ณผ ์˜ค์ฐจ ์œ ํ˜•\n", "cm = pd.crosstab(c[\"grade\"].map(GRADE_NAME), c[\"pred\"].map(GRADE_NAME), rownames=[\"์‹ค์ œ\"], colnames=[\"ํŒ์ •\"])\n", "cm = cm.reindex(index=[\"์•ˆ์ „\", \"์ฃผ์˜\", \"์œ„ํ—˜\"], columns=[\"์•ˆ์ „\", \"์ฃผ์˜\", \"์œ„ํ—˜\"], fill_value=0)\n", "print(\"\\n[ํ˜ผ๋™ํ–‰๋ ฌ]\")\n", "print(cm.to_string())\n", "danger = c[\"grade\"] == 2\n", "rec = ((c[\"pred\"] == 2) & danger).sum() / danger.sum()\n", "far = ((c[\"pred\"] == 2) & ~danger).sum() / max((c[\"pred\"] == 2).sum(), 1)\n", "print(f\"์œ„ํ—˜ ์žฌํ˜„์œจ {rec:.1%}, ์œ„ํ—˜ ๊ฒฝ๋ณด ์ค‘ ์˜ค๊ฒฝ๋ณด ๋น„์œจ {far:.1%}\")\n", "miss = c[danger & (c[\"pred\"] != 2)]\n", "fa = c[~danger & (c[\"pred\"] == 2)]\n", "print(\"๋†“์นœ ์œ„ํ—˜(์‚ฌ๊ฑด ์œ ํ˜•๋ณ„):\", miss[\"event\"].value_counts().to_dict())\n", "print(\"์˜ค๊ฒฝ๋ณด(์„ผ์„œ ๊ณ ์žฅ ์œ ํ˜•๋ณ„):\", fa[\"fault\"].value_counts().to_dict())\n", "\n", "# 3) ํŒ์ • ๊ทผ๊ฑฐ(ํˆฌ๋ช…์„ฑ): ์œ„ํ—˜ ํŒ์ • ์‚ฌ๋ก€ ์ผ๋ถ€์˜ ์ดˆ๊ณผ ํ•ญ๋ชฉ\n", "print(\"\\n[์œ„ํ—˜ ํŒ์ • ๊ทผ๊ฑฐ ์˜ˆ์‹œ]\")\n", "show = c[c[\"pred\"] == 2].groupby(\"source_id\").head(1).head(5)\n", "for _, r in show.iterrows():\n", " print(f\" {r['source_id']} D+{int(r['t']) // 24:02d} {int(r['t']) % 24:02d}์‹œ: {reasons(r)} -> ์‹ค์ œ {GRADE_NAME[r['grade']]}({r['event']})\")\n", "\n", "# 4) ํœด๋ฆฌ์Šคํ‹ฑ 1์ฐจ ํŠœ๋‹: ์œ„ํ—˜ ํŒ์ •์„ '2์‹œ๊ฐ„ ์—ฐ์† ์ดˆ๊ณผ'๋กœ ๋ฐ”๊พธ๋ฉด ์˜ค๊ฒฝ๋ณด๋Š” ์ค„์ง€๋งŒ ๋†“์นจ์ด ๋Š˜์–ด๋‚œ๋‹ค\n", "c[\"pred2\"] = c[\"pred\"]\n", "prev = c.groupby(\"source_id\")[\"pred\"].shift(1).fillna(0)\n", "c.loc[(c[\"pred\"] == 2) & (prev < 2), \"pred2\"] = 1\n", "for name, col in [(\"1์‹œ๊ฐ„ ์ฆ‰์‹œ ํŒ์ •\", \"pred\"), (\"2์‹œ๊ฐ„ ์—ฐ์† ํ™•์ธ\", \"pred2\")]:\n", " p = c[col]\n", " print(f\"{name}: ์ •ํ™•๋„ {(p == c['grade']).mean():.1%}, ์œ„ํ—˜ ์žฌํ˜„์œจ {((p == 2) & danger).sum() / danger.sum():.1%}, \"\n", " f\"์˜ค๊ฒฝ๋ณด {int(((p == 2) & ~danger).sum())}๊ฑด\")\n", "print(\"-> ์Œ์šฉ์ˆ˜๋Š” ๋†“์นจ์ด ๋” ์œ„ํ—˜ํ•˜๋ฏ€๋กœ Baseline์€ ์ฆ‰์‹œ ํŒ์ •์„ ์œ ์ง€ํ•˜๊ณ , ์˜ค๊ฒฝ๋ณด๋Š” ๋ฏธ์…˜ 5 ์‹ ๋ขฐ๋„ ์ง€์ˆ˜๋กœ ๋‹ค๋ฃฌ๋‹ค.\")\n", "print(\"โ€ป ๊ธฐ์ค€์น˜๋Š” ๊ต์œก์šฉ ๊ฐ€์ •๊ฐ’์ด๋‹ค. ์‹ค์ œ ์Œ์šฉ ์ ํ•ฉ ํŒ์ •์€ ๊ณต์‹ ์ˆ˜์งˆ ๊ธฐ์ค€๊ณผ ์˜๋ฌด(์˜ˆ๋ฐฉ์˜ํ•™)ยทํ™˜๊ฒฝ ๋‹ด๋‹น ํ™•์ธ์— ๋”ฐ๋ฅธ๋‹ค.\")\n", "\n", "sid = miss[\"source_id\"].mode().iloc[0] if len(miss) else \"W01\"\n", "s = c[c[\"source_id\"] == sid]\n", "fig, ax = plt.subplots(figsize=(10, 3.5))\n", "ax.plot(s[\"t\"], s[\"grade\"], color=\"black\", lw=2, label=\"true grade\")\n", "ax.plot(s[\"t\"], s[\"pred\"] + 0.05, color=\"tab:red\", lw=1, label=\"baseline grade\")\n", "ax.set_yticks([0, 1, 2], [\"safe\", \"caution\", \"danger\"]); ax.set_xlabel(\"hour\"); ax.legend()\n", "ax.set_title(f\"{sid} baseline vs true grade (synthetic)\")\n", "plt.tight_layout(); plt.show()\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "
\n", "๐Ÿ’ก [์ฐธ๊ณ ] ๋ฏธ์…˜ 03 ์˜ˆ์ƒ ์‹คํ–‰ ๊ฒฐ๊ณผ (Output)\n", "\n", "```text\n", "[๊ต์œก์šฉ๊ฐ€์ƒ์ž๋ฃŒ] ๋ฒค์น˜๋งˆํฌ 2,688๊ฑด(๊ธ‰์ˆ˜์› 8๊ณณ x 336์‹œ๊ฐ„)\n", "์ •ํ™•๋„: ์ „์ฒ˜๋ฆฌ ์—†์Œ 96.8% -> ์ „์ฒ˜๋ฆฌ + ๊ธฐ์ค€์น˜ ํ•„ํ„ฐ 97.9% (๋ชฉํ‘œ 75% ์ด์ƒ: ๋‹ฌ์„ฑ)\n", "์ฒ˜๋ฆฌ ์ง€์—ฐ: ์ „์ฒด 158ms, 1๊ฑด๋‹น 0.059ms\n", "\n", "[ํ˜ผ๋™ํ–‰๋ ฌ]\n", "ํŒ์ • ์•ˆ์ „ ์ฃผ์˜ ์œ„ํ—˜\n", "์‹ค์ œ \n", "์•ˆ์ „ 2369 8 17\n", "์ฃผ์˜ 3 45 0\n", "์œ„ํ—˜ 9 19 218\n", "์œ„ํ—˜ ์žฌํ˜„์œจ 88.6%, ์œ„ํ—˜ ๊ฒฝ๋ณด ์ค‘ ์˜ค๊ฒฝ๋ณด ๋น„์œจ 7.2%\n", "๋†“์นœ ์œ„ํ—˜(์‚ฌ๊ฑด ์œ ํ˜•๋ณ„): {'creeping_metal': 22, 'chemical': 4, 'disinfect_fail': 1, 'metal': 1}\n", "์˜ค๊ฒฝ๋ณด(์„ผ์„œ ๊ณ ์žฅ ์œ ํ˜•๋ณ„): {'none': 11, 'drift': 6}\n", "\n", "[์œ„ํ—˜ ํŒ์ • ๊ทผ๊ฑฐ ์˜ˆ์‹œ]\n", " W01 D+13 20์‹œ: ๋Œ€์žฅ๊ท  1 -> ์‹ค์ œ ์•ˆ์ „(none)\n", " W02 D+06 03์‹œ: pH 9.04, ํƒ๋„ 1.24NTU -> ์‹ค์ œ ์œ„ํ—˜(chemical)\n", " W03 D+02 03์‹œ: ๋Œ€์žฅ๊ท  1 -> ์‹ค์ œ ์•ˆ์ „(none)\n", " W04 D+12 00์‹œ: pH 8.65, ํƒ๋„ 1.16NTU -> ์‹ค์ œ ์œ„ํ—˜(chemical)\n", " W05 D+00 09์‹œ: ๋Œ€์žฅ๊ท  1 -> ์‹ค์ œ ์•ˆ์ „(none)\n", "1์‹œ๊ฐ„ ์ฆ‰์‹œ ํŒ์ •: ์ •ํ™•๋„ 97.9%, ์œ„ํ—˜ ์žฌํ˜„์œจ 88.6%, ์˜ค๊ฒฝ๋ณด 17๊ฑด\n", "2์‹œ๊ฐ„ ์—ฐ์† ํ™•์ธ: ์ •ํ™•๋„ 97.6%, ์œ„ํ—˜ ์žฌํ˜„์œจ 85.0%, ์˜ค๊ฒฝ๋ณด 5๊ฑด\n", "-> ์Œ์šฉ์ˆ˜๋Š” ๋†“์นจ์ด ๋” ์œ„ํ—˜ํ•˜๋ฏ€๋กœ Baseline์€ ์ฆ‰์‹œ ํŒ์ •์„ ์œ ์ง€ํ•˜๊ณ , ์˜ค๊ฒฝ๋ณด๋Š” ๋ฏธ์…˜ 5 ์‹ ๋ขฐ๋„ ์ง€์ˆ˜๋กœ ๋‹ค๋ฃฌ๋‹ค.\n", "โ€ป ๊ธฐ์ค€์น˜๋Š” ๊ต์œก์šฉ ๊ฐ€์ •๊ฐ’์ด๋‹ค. ์‹ค์ œ ์Œ์šฉ ์ ํ•ฉ ํŒ์ •์€ ๊ณต์‹ ์ˆ˜์งˆ ๊ธฐ์ค€๊ณผ ์˜๋ฌด(์˜ˆ๋ฐฉ์˜ํ•™)ยทํ™˜๊ฒฝ ๋‹ด๋‹น ํ™•์ธ์— ๋”ฐ๋ฅธ๋‹ค.\n", "```\n", "
\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "---\n", "\n", "## ๐ŸŽฏ ๋ฏธ์…˜ 04. ๋ณตํ•ฉ ์˜ค์—ผ์› ์œ ์ž… ์ง•ํ›„ ๋ฐ ๊ธ‰์ˆ˜ ์•ˆ์ „๋„ ์œ„ํ—˜ ๋“ฑ๊ธ‰ ๋ถ„๋ฅ˜ ๋จธ์‹ ๋Ÿฌ๋‹ ๋ชจ๋ธ ๊ณ ๋„ํ™”\n", "**์†Œ์š”์‹œ๊ฐ„**: ์•ฝ 3.0์‹œ๊ฐ„\n", "\n", "### ๐Ÿ“‹ ๋ฏธ์…˜ ๋ฐฐ๊ฒฝ ๋ฐ ์ˆ˜ํ–‰ ์ง€์นจ\n", "#### ๋ฏธ์…˜ ๋ฐฐ๊ฒฝ ๋ฐ ๋‹น์œ„์„ฑ\n", "\n", "๋ณตํ•ฉ ์˜ค์—ผ์› ์œ ์ž… ์ง•ํ›„ ๋ฐ ๊ธ‰์ˆ˜ ์œ„ํ—˜ ๋“ฑ๊ธ‰ ๋ถ„๋ฅ˜ ๋จธ์‹ ๋Ÿฌ๋‹ ๋ชจ๋ธ์„ ๊ณ ๋„ํ™”ํ•œ๋‹ค. ์‹ค์ œ ์ „์ˆ  ํ˜„์žฅ์—์„œ๋Š” ๋‹จ์ˆœ ํƒ์ง€์— ๊ทธ์น˜์ง€ ์•Š๊ณ  ์‹ ์†ํ•œ ์œ„ํ˜‘ ํŒ๋‹จ๊ณผ ์‹ ๋ขฐ๋„ ๋†’์€ ์˜์‚ฌ๊ฒฐ์ •์ด ํ•„์ˆ˜์ ์ด๋ฏ€๋กœ, **๋ณตํ•ฉ ์˜ค์—ผ์› ์œ ์ž… ์ง•ํ›„์™€ ๊ธ‰์ˆ˜ ์•ˆ์ „๋„ ์œ„ํ—˜ ๋“ฑ๊ธ‰์„ ๋ถ„๋ฅ˜ํ•˜๋Š” ๋จธ์‹ ๋Ÿฌ๋‹ ๋ชจ๋ธ์„ ๊ณ ๋„ํ™”ํ•˜๊ณ  ๋ฐฉ์–ด ๋กœ์ง์„ ๊ฒ€์ฆํ•œ๋‹ค.** ๊ณผ์—…์„ ์™„์ˆ˜ํ•˜์—ฌ ์˜คํŒ์„ ๋ฐฉ์ง€ํ•˜๊ณ  ์ž‘์ „ ๋Œ€์‘๋ ฅ์„ ๊ทน๋Œ€ํ™”ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.\n", "\n", "์ด ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด ๋ณธ ๋ฏธ์…˜์—์„œ๋Š” **๋ณ€ํ™”๋Ÿ‰, 24์‹œ๊ฐ„ ๊ธฐ์ค€์„  ๋Œ€๋น„ ํŽธ์ฐจ, 6์‹œ๊ฐ„ ๋ณ€๋™์„ฑ, ๋Œ€์žฅ๊ท  ๋ˆ„์ , ์„ผ์„œ ์ฒ˜๋ฆฌ ํ‘œ์‹œ ๋“ฑ 29๊ฐœ ํŠน์ง•์œผ๋กœ ๋žœ๋ค ํฌ๋ ˆ์ŠคํŠธ๋ฅผ ํ•™์Šตํ•˜๊ณ  Baseline๊ณผ ๋น„๊ต ์ˆ˜ํ–‰ํ•˜๊ณ  ๊ฒ€์ฆํ•˜๋Š”** ์‹ค์Šต์„ ์ฒด๊ณ„์ ์œผ๋กœ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค.\n", "\n", "ํ•™์Šต์ž๋Š” ์‹ค์Šต๊ฐ€์ด๋“œ์— ์ œ์‹œ๋œ ์ด 4๊ฐœ ์„ธ๋ถ€ ํ™œ๋™(๊ธฐ๋ณธ ๊ธฐ๋Šฅ ๊ตฌํ˜„ โž” ๊ฒฐ๊ณผ ์ง€ํ‘œ ๊ฒ€์ฆ โž” ํŒŒ๋ผ๋ฏธํ„ฐ ํŠœ๋‹ ๋ฐ ๋„์ „๊ณผ์ œ)์„ ์ˆœ์ฐจ์ ์œผ๋กœ ์ง„ํ–‰ํ•˜๋ฉฐ ํ•ต์‹ฌ ํŒ์ • ๊ธฐ์ค€์„ ๋‹ฌ์„ฑํ•ฉ๋‹ˆ๋‹ค.\n", "\n", "**์ด๋ฒˆ ๊ฒฐ๊ณผ๋ฌผ:** `src/models/wel03_advanced_detector.py ๋ฐ ์˜ˆ์™ธ ํƒ์ง€ ํ…Œ์ŠคํŠธ ๊ฒฐ๊ณผ` ํ˜•ํƒœ์˜ ๊ฒฐ๊ณผ๋ฌผ์„ ์™„์„ฑํ•œ๋‹ค.\n", "\n", "### ๐Ÿ” ํ‰๊ฐ€ ๋ฐ ๊ฒ€์ฆ ๊ธฐ์ค€ (PASS ์กฐ๊ฑด)\n", "#### ๋ฏธ์…˜ ํŒ์ •\n", "\n", "**ํ‰๊ฐ€ ๊ตฌ๋ถ„:** ํ•„์ˆ˜\n", "\n", "**ํ•ต์‹ฌ ํ‰๊ฐ€์˜์—ญ:** ๊ธฐ๋งŒ/๊ต๋ž€ ๊ณต๊ฒฉ์— ๋Œ€ํ•œ ๋ฐฉ์–ด ๋กœ์ง์˜ ํ˜„์‹ค์„ฑ ๋ฐ ์‹ ๋ขฐ์„ฑ\n", "\n", "#### ๋ฏธ์…˜ PASS ์กฐ๊ฑด\n", "\n", "- ์ •๋Ÿ‰ ๊ธฐ์ค€: ๋ณตํ•ฉ ์œ„ํ˜‘ ํƒ์ง€ ์žฌํ˜„์œจ(Recall) 85% ์ด์ƒ, ์ฒ˜๋ฆฌ ์ง€์—ฐ 200ms ์ดํ•˜\n", "\n", "\n", "- ์ง„ํ–‰ ๊ธฐ์ค€: ๋ณตํ•ฉ ์œ„ํ˜‘ ์‹œ๋‚˜๋ฆฌ์˜ค 10์ข…์— ๋Œ€ํ•œ ํƒ์ง€ ํ…Œ์ŠคํŠธ ํ†ต๊ณผ\n", "\n", "\n", "- ๊ฒ€์ฆ ๋ฐฉ๋ฒ•: ํ•ฉ์„ฑ ์œ„ํ˜‘ ๋ฐ์ดํ„ฐ์…‹ ์ฃผ์ž… ์‹œ ์ด์ƒ์น˜ ํƒ์ง€ ์ •ํ™•๋„ ๊ฒ€์ฆ\n", "\n", "\n", "#### ์ฑ„์ ์ž ํŒ์ • ๋ฉ”๋ชจ\n", "\n", "> ๋‹จ์ˆœ ์ฝ”๋“œ ์‹คํ–‰ ์—ฌ๋ถ€๋ฟ๋งŒ ์•„๋‹ˆ๋ผ ์ถœ๋ ฅ๋œ ์ˆ˜์น˜์™€ ๊ทผ๊ฑฐ ์ž๋ฃŒ๊ฐ€ ๊ตฐ ์‹ค๋ฌด ์š”๊ตฌ์‚ฌํ•ญ์— ๋ถ€ํ•ฉํ•˜๋Š”์ง€ ๋ฉด๋ฐ€ํžˆ ํ™•์ธํ•œ๋‹ค.\n", "\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "\"\"\"์‹œ๊ณ„์—ด ํŠน์ง•(๋ณ€ํ™”๋Ÿ‰ยท๊ธฐ์ค€์„  ํŽธ์ฐจยท๋ณ€๋™์„ฑยท๋Œ€์žฅ๊ท  ๋ˆ„์ ยท์„ผ์„œ ์ฒ˜๋ฆฌ ํ‘œ์‹œ)์œผ๋กœ ๊ธ‰์ˆ˜ ์•ˆ์ „๋„ ์œ„ํ—˜ ๋“ฑ๊ธ‰(์•ˆ์ „ยท์ฃผ์˜ยท์œ„ํ—˜)์„ ๋ถ„๋ฅ˜ํ•˜๋Š” ๋žœ๋ค ํฌ๋ ˆ์ŠคํŠธ ๋ชจ๋ธ์„ ํ•™์Šตํ•˜๊ณ , ๋ณตํ•ฉ ์œ„ํ˜‘ ์‹œ๋‚˜๋ฆฌ์˜ค 10์ข…(์˜ค์—ผ ์‚ฌ๊ฑด 7์ข…, ์„ผ์„œ ๊ต๋ž€ 3์ข…)์œผ๋กœ ์žฌํ˜„์œจยท์˜ค๊ฒฝ๋ณดยท์ฒ˜๋ฆฌ ์ง€์—ฐ์„ ๊ฒ€์ฆํ•œ๋‹ค.\"\"\"\n", "import time\n", "import numpy as np\n", "import pandas as pd\n", "import matplotlib.pyplot as plt\n", "from sklearn.ensemble import RandomForestClassifier\n", "\n", "# ---------------------------------------------------------------------------\n", "# [๊ต์œก์šฉ๊ฐ€์ƒ์ž๋ฃŒ] ์•ผ์ „ ๊ธ‰์ˆ˜์› ๊ฐ€์ƒ ์ˆ˜์งˆ ์„ผ์„œ ๋ฐ์ดํ„ฐ ์ƒ์„ฑ๊ธฐ (์‹ค์ œ ๋ถ€๋Œ€ยท์œ„์น˜ยท๊ฒ€์‚ฌ ๊ฒฐ๊ณผ ์—†์Œ)\n", "# ๊ฐ€์ƒ ๊ธ‰์ˆ˜์› 8๊ณณ x 14์ผ x 1์‹œ๊ฐ„ ๊ฐ„๊ฒฉ. ์‹ค์ œ ์ˆ˜์งˆ(true_*)ยท์„ผ์„œ ์ธก์ •๊ฐ’ยท์˜ค์—ผ ์‚ฌ๊ฑดยท์„ผ์„œ ๊ณ ์žฅ ์ •๋‹ต์„ ํ•จ๊ป˜ ๋งŒ๋“ ๋‹ค.\n", "# ๊ธฐ์ค€์น˜๋Š” ๊ต์œก์šฉ ๊ฐ€์ •๊ฐ’์ด๋ฉฐ, ์‹ค์ œ ์Œ์šฉ ํŒ์ •์€ ๊ณต์ธ ๊ฒ€์‚ฌ ๊ฒฐ๊ณผ์™€ ์˜๋ฌด(์˜ˆ๋ฐฉ์˜ํ•™) ๋‹ด๋‹น ๊ธฐ์ค€์— ๋”ฐ๋ฅธ๋‹ค.\n", "# ---------------------------------------------------------------------------\n", "PARAMS = [\"ph\", \"turbidity\", \"chlorine\", \"metal\", \"ecoli\"]\n", "SOURCES = [(\"W01\", \"water_truck\", 1.0, 2.0), (\"W02\", \"water_truck\", 3.5, 1.2), (\"W03\", \"purification_unit\", 2.2, 4.1),\n", " (\"W04\", \"purification_unit\", 5.0, 3.3), (\"W05\", \"well\", 0.8, 5.6), (\"W06\", \"well\", 4.4, 6.0),\n", " (\"W07\", \"river_intake\", 6.3, 1.8), (\"W08\", \"river_intake\", 6.9, 4.9)]\n", "BASE = {\"water_truck\": (7.2, 0.25, 0.75, 1.5), \"purification_unit\": (7.0, 0.20, 0.85, 1.2),\n", " \"well\": (6.8, 0.45, 0.50, 3.0), \"river_intake\": (7.3, 0.55, 0.60, 2.0)} # pH, ํƒ๋„(NTU), ์ž”๋ฅ˜์—ผ์†Œ(mg/L), ์ค‘๊ธˆ์†(ug/L)\n", "LIMITS = {\"ph\": (5.8, 8.5), \"turbidity\": (1.0, 2.0), \"chlorine\": (0.2, 0.1), \"metal\": (5.0, 10.0), \"ecoli\": (1, 1)} # (์ฃผ์˜, ์œ„ํ—˜) ๊ฐ€์ • ๊ธฐ์ค€\n", "EVENT_TYPES = { # ์˜ค์—ผ ์‚ฌ๊ฑด: ๋ณ€์ˆ˜๋ณ„ ์ตœ๋Œ€ ๋ณ€ํ™”๋Ÿ‰, ์ตœ๋Œ€์น˜ ๋„๋‹ฌ ์‹œ๊ฐ„(h)\n", " \"sewage\": ({\"turbidity\": 2.8, \"chlorine\": -0.5, \"ecoli\": 30, \"ph\": -0.3}, 4),\n", " \"metal\": ({\"metal\": 22.0, \"ph\": -0.5}, 4),\n", " \"disinfect_fail\": ({\"chlorine\": -0.8, \"ecoli\": 6}, 6),\n", " \"chemical\": ({\"ph\": 1.7, \"turbidity\": 1.0}, 3),\n", " \"creeping_metal\": ({\"metal\": 13.0}, 18),\n", "}\n", "FAULT_TYPES = [\"stuck\", \"spike\", \"drift\", \"dropout\"]\n", "NOISE = {\"ph\": 0.04, \"turbidity\": 0.06, \"chlorine\": 0.03, \"metal\": 0.35}\n", "DIGITS = {\"ph\": 2, \"turbidity\": 2, \"chlorine\": 2, \"metal\": 1}\n", "\n", "\n", "def make_dataset(days=14, seed=7, n_events=6, n_faults=10, events=None, faults=None, sources=None):\n", " \"\"\"events: [(๊ธ‰์ˆ˜์› ๋ฒˆํ˜ธ, ์‚ฌ๊ฑด ์œ ํ˜•, ์‹œ์ž‘ ์‹œ๊ฐ h, ์ง€์† h, ๊ฐ•๋„)], faults: [(๊ธ‰์ˆ˜์› ๋ฒˆํ˜ธ, ๊ณ ์žฅ ์œ ํ˜•, ํ•ญ๋ชฉ, ์‹œ์ž‘ h, ์ง€์† h)]\"\"\"\n", " rng = np.random.default_rng(seed)\n", " sources = sources or SOURCES\n", " T = days * 24\n", " tt = np.arange(T)\n", " hour = tt % 24\n", " night = (hour < 6) | (hour >= 20)\n", " rain = np.zeros(T)\n", " for _ in range(max(1, days // 4)):\n", " s0, d = int(rng.integers(0, T - 14)), int(rng.integers(6, 14))\n", " rain[s0:s0 + d] = np.maximum(rain[s0:s0 + d], rng.uniform(0.6, 1.0))\n", " if events is None:\n", " events = [(int(rng.integers(len(sources))), str(rng.choice(list(EVENT_TYPES))), int(rng.integers(min(24, T // 4), max(T // 4 + 1, T - 48))),\n", " int(rng.integers(10, 30)), 1.0) for _ in range(n_events)]\n", " if faults is None:\n", " faults = [(int(rng.integers(len(sources))), str(rng.choice(FAULT_TYPES)), str(rng.choice([\"ph\", \"turbidity\", \"chlorine\", \"metal\"])),\n", " int(rng.integers(12, max(13, T - 30))), int(rng.integers(6, 24))) for _ in range(n_faults)]\n", " out = []\n", " for si, (sid, stype, x, y) in enumerate(sources):\n", " ph0, tb0, cl0, mt0 = BASE[stype]\n", " tr = {\"ph\": ph0 + 0.08 * np.sin(2 * np.pi * hour / 24) + rng.normal(0, 0.03, T),\n", " \"turbidity\": tb0 * (1 + 0.1 * rng.normal(0, 1, T)),\n", " \"chlorine\": (cl0 * np.exp(-0.022 * hour) if stype == \"water_truck\" else cl0 + rng.normal(0, 0.03, T)),\n", " \"metal\": mt0 + rng.normal(0, 0.2, T), \"ecoli\": np.zeros(T)}\n", " if stype in (\"river_intake\", \"well\"):\n", " tr[\"turbidity\"] = tr[\"turbidity\"] + rain * (0.9 if stype == \"river_intake\" else 0.45)\n", " f_all, etype = np.zeros(T), np.array([\"none\"] * T, dtype=object)\n", " for (s_i, et, onset, dur, scale) in events:\n", " if s_i != si:\n", " continue\n", " delta, ramp = EVENT_TYPES[et]\n", " f = np.clip((tt - onset + 1) / ramp, 0, 1) * ((tt >= onset) & (tt < onset + dur + ramp))\n", " for k, v in delta.items():\n", " tr[k] = tr[k] + v * f * scale\n", " etype[f > 0] = et\n", " f_all = np.maximum(f_all, f)\n", " tr[\"chlorine\"] = np.clip(tr[\"chlorine\"], 0.02, None)\n", " tr[\"turbidity\"] = np.clip(tr[\"turbidity\"], 0.05, None)\n", " tr[\"metal\"] = np.clip(tr[\"metal\"], 0.1, None)\n", " grade = np.where(f_all >= 0.2, 2, 0)\n", " mild = (tr[\"turbidity\"] > LIMITS[\"turbidity\"][0]) | (tr[\"chlorine\"] < LIMITS[\"chlorine\"][0]) | (tr[\"metal\"] > LIMITS[\"metal\"][0])\n", " grade = np.where((grade == 0) & mild, 1, grade)\n", " ms = {}\n", " for p in [\"ph\", \"turbidity\", \"chlorine\", \"metal\"]:\n", " sd = NOISE[p] * np.where(night, 1.5, 1.0) * (tr[p] if p == \"turbidity\" else 1)\n", " ms[p] = np.round(np.clip(tr[p] + rng.normal(0, 1, T) * sd, 0, None), DIGITS[p])\n", " ms[\"ecoli\"] = (rng.poisson(tr[\"ecoli\"]) + (rng.random(T) < 0.004)).astype(float)\n", " fault, fparam = np.array([\"none\"] * T, dtype=object), np.array([\"-\"] * T, dtype=object)\n", " for (s_i, ft, p, s0, d) in faults:\n", " if s_i != si:\n", " continue\n", " seg = slice(s0, min(T, s0 + d))\n", " if ft == \"stuck\":\n", " ms[p][seg] = ms[p][s0]\n", " elif ft == \"spike\":\n", " seg = slice(s0, s0 + 1)\n", " ms[p][s0] = {\"ph\": 11.8, \"turbidity\": 38.0, \"chlorine\": 0.0, \"metal\": 75.0}[p]\n", " elif ft == \"drift\":\n", " ramp = np.linspace(0, 1, seg.stop - s0)\n", " ms[p][seg] = np.clip(ms[p][seg] + ramp * {\"ph\": -1.6, \"turbidity\": 2.6, \"chlorine\": -0.75, \"metal\": 12.0}[p], 0, None).round(DIGITS[p])\n", " elif ft == \"dropout\":\n", " for q in PARAMS:\n", " ms[q][seg] = np.nan\n", " p = \"all\"\n", " fault[seg], fparam[seg] = ft, p\n", " df = pd.DataFrame({\"source_id\": sid, \"source_type\": stype, \"x_km\": x, \"y_km\": y, \"t\": tt,\n", " \"cond\": np.where(rain > 0, \"storm\", np.where(night, \"night\", \"day\")),\n", " **{p: ms[p] for p in PARAMS}, **{f\"true_{p}\": tr[p] for p in PARAMS},\n", " \"event\": etype, \"fault\": fault, \"fault_param\": fparam, \"grade\": grade})\n", " out.append(df)\n", " return pd.concat(out, ignore_index=True)\n", "# ---------------------------------------------------------------------------\n", "\n", "# ---- ์ „์ฒ˜๋ฆฌ ๊ณตํ†ต ํ•จ์ˆ˜: ๋ฌผ๋ฆฌ ๋ฒ”์œ„ ์ ๊ฒ€ -> ๊ณ ์ •๊ฐ’(stuck) ํ‘œ์‹œ -> ๋‹จ๋ฐœ ์ŠคํŒŒ์ดํฌ ์ œ๊ฑฐ -> ๊ฒฐ์ธก ๋ณด์ • ----\n", "PHYS = {\"ph\": (0, 14), \"turbidity\": (0, 1000), \"chlorine\": (0, 10), \"metal\": (0, 1000), \"ecoli\": (0, 100000)}\n", "SPIKE_K = {\"day\": 6.0, \"night\": 7.0, \"storm\": 9.0} # ์ƒํ™ฉ๋ณ„(์ฃผ๊ฐ„/์•ผ๊ฐ„/์•…์ฒœํ›„) ์ŠคํŒŒ์ดํฌ ํŒ์ • ๋ฐฐ์ˆ˜\n", "MIN_JUMP = {\"ph\": 0.8, \"turbidity\": 1.5, \"chlorine\": 0.3, \"metal\": 6.0}\n", "STUCK_RUN, SHORT_GAP = 6, 3\n", "\n", "\n", "def clean(df, spike_k=None, min_jump=None):\n", " \"\"\"๊ธ‰์ˆ˜์›๋ณ„ ์‹œ๊ณ„์—ด์„ ์ •์ œํ•˜๊ณ  ํ•ญ๋ชฉ๋ณ„ ์ฒ˜๋ฆฌ ๋‚ด์—ญ(<ํ•ญ๋ชฉ>_flag: range/stuck/spike/interp/long_gap)์„ ๋‚จ๊ธด๋‹ค.\"\"\"\n", " spike_k = spike_k or SPIKE_K\n", " min_jump = MIN_JUMP if min_jump is None else min_jump\n", " out = df.sort_values([\"source_id\", \"t\"]).reset_index(drop=True)\n", " for sid, idx in out.groupby(\"source_id\").groups.items():\n", " idx = np.asarray(idx)\n", " kk = out.loc[idx, \"cond\"].map(spike_k).to_numpy(float)\n", " for p in PARAMS:\n", " v = out.loc[idx, p].to_numpy(float).copy()\n", " fl = np.array([\"\"] * len(v), dtype=object)\n", " lo, hi = PHYS[p]\n", " bad = (v < lo) | (v > hi)\n", " v[bad], fl[bad] = np.nan, \"range\"\n", " if p != \"ecoli\":\n", " chg = np.r_[True, v[1:] != v[:-1]] # ๊ฐ™์€ ๊ฐ’์ด STUCK_RUN์‹œ๊ฐ„ ์ด์ƒ ๋ฐ˜๋ณต = ๊ณ ์ •๊ฐ’ ๊ณ ์žฅ ์˜์‹ฌ\n", " rid = np.cumsum(chg)\n", " rlen = np.bincount(rid)[rid]\n", " fl[(rlen >= STUCK_RUN) & ~chg & ~np.isnan(v)] = \"stuck\"\n", " d = np.abs(np.diff(v[~np.isnan(v)]))\n", " sig = 1.4826 * np.median(d) + 1e-6 if len(d) else 1.0\n", " up, dn = v - np.r_[np.nan, v[:-1]], v - np.r_[v[1:], np.nan]\n", " thr = np.maximum(kk * sig, min_jump[p])\n", " spike = (np.abs(up) > thr) & (np.abs(dn) > thr) & (np.sign(up) == np.sign(dn)) # ํŠ€์—ˆ๋‹ค๊ฐ€ ๋ฐ”๋กœ ๋Œ์•„์˜จ ๊ฐ’๋งŒ\n", " v[spike], fl[spike] = np.nan, \"spike\"\n", " s = pd.Series(v)\n", " na = s.isna().to_numpy()\n", " grp = np.cumsum(np.r_[True, na[1:] != na[:-1]])\n", " glen = pd.Series(na.astype(int)).groupby(grp).transform(\"sum\").to_numpy()\n", " lin, ff = s.interpolate(limit_area=\"inside\").to_numpy(), s.ffill().bfill().to_numpy()\n", " short, longg = na & (glen <= SHORT_GAP), na & (glen > SHORT_GAP)\n", " v = np.where(short, np.where(np.isnan(lin), ff, lin), np.where(longg, ff, v))\n", " fl[short & (fl == \"\")] = \"interp\"\n", " fl[longg] = \"long_gap\"\n", " out.loc[idx, p] = v\n", " out.loc[idx, f\"{p}_flag\"] = fl\n", " return out\n", "\n", "# ---- ๊ฐ€์ • ๊ธฐ์ค€์น˜ ํŒ์ •(Baseline)๊ณผ ์‹œ๊ณ„์—ด ํŠน์ง• ๊ณตํ†ต ํ•จ์ˆ˜ ----\n", "GRADE_NAME = {0: \"์•ˆ์ „\", 1: \"์ฃผ์˜\", 2: \"์œ„ํ—˜\"}\n", "\n", "\n", "def rule_grade(df):\n", " \"\"\"๊ธฐ์ค€์น˜ 1์ฐจ ํ•„ํ„ฐ: ์œ„ํ—˜(์Œ์šฉ ๋ถ€์ ํ•ฉ ์˜์‹ฌ) = ํ•œ ํ•ญ๋ชฉ์ด๋ผ๋„ ์œ„ํ—˜ ๊ธฐ์ค€ ์ดˆ๊ณผ, ์ฃผ์˜ = ์ฃผ์˜ ๊ธฐ์ค€ ์ดˆ๊ณผ.\"\"\"\n", " ph, tb, cl, mt, ec = (df[p].to_numpy(float) for p in PARAMS)\n", " danger = (ph < LIMITS[\"ph\"][0]) | (ph > LIMITS[\"ph\"][1]) | (tb > LIMITS[\"turbidity\"][1]) | (cl < LIMITS[\"chlorine\"][1]) \\\n", " | (mt > LIMITS[\"metal\"][1]) | (ec >= LIMITS[\"ecoli\"][1])\n", " caution = (tb > LIMITS[\"turbidity\"][0]) | (cl < LIMITS[\"chlorine\"][0]) | (mt > LIMITS[\"metal\"][0])\n", " return np.where(danger, 2, np.where(caution, 1, 0))\n", "\n", "\n", "def reasons(row):\n", " r = []\n", " if not LIMITS[\"ph\"][0] <= row[\"ph\"] <= LIMITS[\"ph\"][1]:\n", " r.append(f\"pH {row['ph']:.2f}\")\n", " if row[\"turbidity\"] > LIMITS[\"turbidity\"][0]:\n", " r.append(f\"ํƒ๋„ {row['turbidity']:.2f}NTU\")\n", " if row[\"chlorine\"] < LIMITS[\"chlorine\"][0]:\n", " r.append(f\"์ž”๋ฅ˜์—ผ์†Œ {row['chlorine']:.2f}mg/L\")\n", " if row[\"metal\"] > LIMITS[\"metal\"][0]:\n", " r.append(f\"์ค‘๊ธˆ์† {row['metal']:.1f}ug/L\")\n", " if row[\"ecoli\"] >= 1:\n", " r.append(f\"๋Œ€์žฅ๊ท  {row['ecoli']:.0f}\")\n", " return \", \".join(r) or \"-\"\n", "\n", "\n", "FEATS = [f\"{p}{s}\" for p in [\"ph\", \"turbidity\", \"chlorine\", \"metal\"] for s in [\"\", \"_d1\", \"_d3\", \"_dev\", \"_sd6\"]] + \\\n", " [\"ecoli\", \"ecoli_3h\", \"storm\", \"night\", \"n_flag\", \"truck\", \"purif\", \"well\", \"river\"]\n", "\n", "\n", "def add_features(c):\n", " \"\"\"์‹œ๊ฐ„ ์ˆœ์„œ๋ฅผ ์ง€ํ‚ค๋Š”(๊ณผ๊ฑฐ ๊ฐ’๋งŒ ์“ฐ๋Š”) ๊ธ‰์ˆ˜์›๋ณ„ ํŠน์ง•: ๋ณ€ํ™”๋Ÿ‰, 24์‹œ๊ฐ„ ๊ธฐ์ค€์„  ๋Œ€๋น„ ํŽธ์ฐจ, 6์‹œ๊ฐ„ ๋ณ€๋™์„ฑ, ๋Œ€์žฅ๊ท  ๋ˆ„์  ๋“ฑ.\"\"\"\n", " c = c.sort_values([\"source_id\", \"t\"]).reset_index(drop=True)\n", " g = c.groupby(\"source_id\")\n", " for p in [\"ph\", \"turbidity\", \"chlorine\", \"metal\"]:\n", " c[f\"{p}_d1\"] = g[p].diff().fillna(0)\n", " c[f\"{p}_d3\"] = g[p].diff(3).fillna(0)\n", " base = g[p].transform(lambda s: s.shift(1).rolling(24, min_periods=4).median())\n", " c[f\"{p}_dev\"] = (c[p] - base).fillna(0)\n", " c[f\"{p}_sd6\"] = g[p].transform(lambda s: s.rolling(6, min_periods=2).std()).fillna(0)\n", " c[\"ecoli_3h\"] = g[\"ecoli\"].transform(lambda s: s.rolling(3, min_periods=1).sum())\n", " c[\"storm\"], c[\"night\"] = (c[\"cond\"] == \"storm\").astype(int), (c[\"cond\"] == \"night\").astype(int)\n", " flags = [f\"{p}_flag\" for p in PARAMS if f\"{p}_flag\" in c]\n", " c[\"n_flag\"] = sum(c[f].isin([\"stuck\", \"spike\", \"long_gap\", \"range\"]).astype(int) for f in flags) if flags else 0\n", " for k, name in [(\"truck\", \"water_truck\"), (\"purif\", \"purification_unit\"), (\"well\", \"well\"), (\"river\", \"river_intake\")]:\n", " c[k] = (c[\"source_type\"] == name).astype(int)\n", " return c\n", "\n", "\n", "def prepare(df):\n", " c = add_features(clean(df))\n", " c[\"rule\"] = rule_grade(c)\n", " return c\n", "\n", "\n", "# 1) ํ•™์Šต(๊ฐ€์ƒ 2์ฃผ x 2์„ธํŠธ)๊ณผ ํ‰๊ฐ€(๋ฒค์น˜๋งˆํฌ seed 11)\n", "train = pd.concat([prepare(make_dataset(seed=s, n_events=14, n_faults=14)) for s in (7, 8)], ignore_index=True)\n", "test = prepare(make_dataset(seed=11, n_events=10, n_faults=14))\n", "model = RandomForestClassifier(n_estimators=200, max_depth=10, class_weight=\"balanced_subsample\", random_state=0, n_jobs=1)\n", "model.fit(train[FEATS], train[\"grade\"])\n", "t0 = time.perf_counter()\n", "test[\"ml\"] = model.predict(test[FEATS])\n", "batch_ms = (time.perf_counter() - t0) * 1000\n", "t0 = time.perf_counter()\n", "for i in range(20):\n", " model.predict(test[FEATS].iloc[[i]])\n", "single_ms = (time.perf_counter() - t0) / 20 * 1000\n", "\n", "d = test[\"grade\"] == 2\n", "print(f\"[๊ต์œก์šฉ๊ฐ€์ƒ์ž๋ฃŒ] ํ•™์Šต {len(train):,}๊ฑด / ํ‰๊ฐ€ {len(test):,}๊ฑด, ํŠน์ง• {len(FEATS)}๊ฐœ\")\n", "for name, col in [(\"Baseline ๊ธฐ์ค€์น˜\", \"rule\"), (\"๋žœ๋ค ํฌ๋ ˆ์ŠคํŠธ\", \"ml\")]:\n", " p = test[col]\n", " print(f\"{name}: ์ •ํ™•๋„ {(p == test['grade']).mean():.1%}, ์œ„ํ—˜ ์žฌํ˜„์œจ {((p == 2) & d).sum() / d.sum():.1%}, \"\n", " f\"์œ„ํ—˜ ์˜ค๊ฒฝ๋ณด {int(((p == 2) & ~d).sum())}๊ฑด\")\n", "rec = ((test[\"ml\"] == 2) & d).sum() / d.sum()\n", "print(f\"๋ณตํ•ฉ ์œ„ํ˜‘ ์žฌํ˜„์œจ {rec:.1%} (๋ชฉํ‘œ 85% ์ด์ƒ: {'๋‹ฌ์„ฑ' if rec >= 0.85 else '๋ฏธ๋‹ฌ'}), \"\n", " f\"์ฒ˜๋ฆฌ ์ง€์—ฐ 1๊ฑด {single_ms:.1f}ms / ์ „์ฒด {batch_ms:.0f}ms (๋ชฉํ‘œ 200ms ์ดํ•˜: {'๋‹ฌ์„ฑ' if single_ms <= 200 else '๋ฏธ๋‹ฌ'})\")\n", "print(\"Baseline์ด ๋†“์นœ ์„œ์„œํžˆ ์˜ค๋ฅด๋Š” ์ค‘๊ธˆ์†(creeping_metal) ์ค‘ ๋ชจ๋ธ ๊ฒ€์ถœ:\",\n", " f\"{int(((test['ml'] == 2) & d & (test['rule'] != 2) & (test['event'] == 'creeping_metal')).sum())}\"\n", " f\"/{int((d & (test['rule'] != 2) & (test['event'] == 'creeping_metal')).sum())}์‹œ๊ฐ„\")\n", "\n", "# 2) ์˜ค๊ฒฝ๋ณด ์œ ๋ฐœ ํŒจํ„ด: ์œ„ํ—˜ ์˜ค๊ฒฝ๋ณด๊ฐ€ ์–ด๋–ค ์„ผ์„œ ๊ณ ์žฅ์—์„œ ๋‚˜์™”๋Š”๊ฐ€ -> ํŠน์ง• ๊ณต๊ฐ„ ์žฌ์„ค๊ณ„ ๋‹จ์„œ\n", "fa = test[(test[\"ml\"] == 2) & ~d]\n", "print(\"๋ชจ๋ธ ์œ„ํ—˜ ์˜ค๊ฒฝ๋ณด์˜ ์„ผ์„œ ๊ณ ์žฅ ์œ ํ˜•:\", fa[\"fault\"].value_counts().to_dict(), \"-> drift(์„œ์„œํžˆ ํ‹€์–ด์ง)๋Š” ๋ฏธ์…˜ 5 ๊ต์ฐจ ๊ฒ€์ฆ์œผ๋กœ ๋ณด์™„\")\n", "imp = pd.Series(model.feature_importances_, FEATS).sort_values(ascending=False).head(6)\n", "print(\"์ค‘์š” ํŠน์ง• Top6:\", \", \".join(f\"{k} {v:.2f}\" for k, v in imp.items()))\n", "\n", "# 3) ๋ณตํ•ฉ ์œ„ํ˜‘ ์‹œ๋‚˜๋ฆฌ์˜ค 10์ข…: (๊ธ‰์ˆ˜์›, ์‚ฌ๊ฑด) / ์„ผ์„œ ๊ต๋ž€์€ ์˜ค์—ผ์ด ์—†์œผ๋ฏ€๋กœ ์œ„ํ—˜ ๊ฒฝ๋ณด๊ฐ€ ๋‚˜์˜ค๋ฉด ์•ˆ ๋œ๋‹ค\n", "SCEN = [(\"ํ•˜์ˆ˜ ์œ ์ž…\", [(6, \"sewage\", 30, 14, 1.0)], []), (\"์ค‘๊ธˆ์† ๊ธ‰์ฆ\", [(4, \"metal\", 30, 14, 1.0)], []),\n", " (\"์†Œ๋… ์‹คํŒจ\", [(0, \"disinfect_fail\", 30, 14, 1.0)], []), (\"ํ™”ํ•™ ์˜ค์—ผ(pH ๊ธ‰๋ณ€)\", [(2, \"chemical\", 30, 14, 1.0)], []),\n", " (\"์ค‘๊ธˆ์† ์„œ์„œํžˆ ์ฆ๊ฐ€\", [(5, \"creeping_metal\", 20, 24, 1.0)], []),\n", " (\"ํ•˜์ˆ˜+์ค‘๊ธˆ์† ๋™์‹œ\", [(7, \"sewage\", 30, 14, 1.0), (7, \"metal\", 34, 10, 1.0)], []),\n", " (\"์•ฝํ•œ ํ•˜์ˆ˜ ์œ ์ž…(๊ฐ•๋„ 60%)\", [(3, \"sewage\", 30, 14, 0.6)], []),\n", " (\"๊ต๋ž€: ํƒ๋„ ์„ผ์„œ ๋“œ๋ฆฌํ”„ํŠธ\", [], [(1, \"drift\", \"turbidity\", 24, 18)]),\n", " (\"๊ต๋ž€: ์ž”๋ฅ˜์—ผ์†Œ ๋‹จ๋ฐœ ์ŠคํŒŒ์ดํฌ\", [], [(0, \"spike\", \"chlorine\", 40, 1)]),\n", " (\"๊ต๋ž€: ํ†ต์‹  ๋‹จ์ ˆ(์ „ ํ•ญ๋ชฉ ๊ฒฐ์ธก)\", [], [(6, \"dropout\", \"ph\", 30, 10)])]\n", "rows = []\n", "for name, ev, ft in SCEN:\n", " c = prepare(make_dataset(days=3, seed=21, events=ev, faults=ft))\n", " c[\"ml\"] = model.predict(c[FEATS])\n", " if ev:\n", " sid = SOURCES[ev[0][0]][0]; on = ev[0][2]\n", " hit = c[(c[\"source_id\"] == sid) & (c[\"ml\"] == 2) & (c[\"t\"] >= on)]\n", " lag = int(hit[\"t\"].min() - on) if len(hit) else None\n", " ok = lag is not None and lag <= 8\n", " rows.append((name, \"์˜ค์—ผ\", f\"{lag}h\" if lag is not None else \"๋ฏธํƒ\", \"PASS\" if ok else \"FAIL\"))\n", " else:\n", " sid = SOURCES[ft[0][0]][0]\n", " n = int(((c[\"source_id\"] == sid) & (c[\"ml\"] == 2)).sum())\n", " rows.append((name, \"๊ต๋ž€\", f\"์œ„ํ—˜ ์˜ค๊ฒฝ๋ณด {n}h\", \"PASS\" if n <= 2 else \"FAIL\"))\n", "res = pd.DataFrame(rows, columns=[\"์‹œ๋‚˜๋ฆฌ์˜ค\", \"๊ตฌ๋ถ„\", \"ํƒ์ง€ ์ง€์—ฐ/์˜ค๊ฒฝ๋ณด\", \"ํŒ์ •\"])\n", "print(\"\\n[๋ณตํ•ฉ ์œ„ํ˜‘ ์‹œ๋‚˜๋ฆฌ์˜ค 10์ข…]\")\n", "print(res.to_string(index=False))\n", "n_pass = int((res[\"ํŒ์ •\"] == \"PASS\").sum())\n", "print(f\"ํ†ต๊ณผ {n_pass}/10 (์˜ค์—ผ: ์‹œ์ž‘ ํ›„ 8์‹œ๊ฐ„ ๋‚ด ์œ„ํ—˜ ํŒ์ •, ๊ต๋ž€: ์œ„ํ—˜ ์˜ค๊ฒฝ๋ณด 2์‹œ๊ฐ„ ์ดํ•˜)\")\n", "\n", "plt.figure(figsize=(8, 4))\n", "imp.iloc[::-1].plot.barh(color=\"tab:blue\")\n", "plt.title(\"Random forest feature importance (top 6, synthetic)\"); plt.xlabel(\"importance\")\n", "plt.tight_layout(); plt.show()\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "
\n", "๐Ÿ’ก [์ฐธ๊ณ ] ๋ฏธ์…˜ 04 ์˜ˆ์ƒ ์‹คํ–‰ ๊ฒฐ๊ณผ (Output)\n", "\n", "```text\n", "[๊ต์œก์šฉ๊ฐ€์ƒ์ž๋ฃŒ] ํ•™์Šต 5,376๊ฑด / ํ‰๊ฐ€ 2,688๊ฑด, ํŠน์ง• 29๊ฐœ\n", "Baseline ๊ธฐ์ค€์น˜: ์ •ํ™•๋„ 97.9%, ์œ„ํ—˜ ์žฌํ˜„์œจ 88.6%, ์œ„ํ—˜ ์˜ค๊ฒฝ๋ณด 17๊ฑด\n", "๋žœ๋ค ํฌ๋ ˆ์ŠคํŠธ: ์ •ํ™•๋„ 99.3%, ์œ„ํ—˜ ์žฌํ˜„์œจ 97.2%, ์œ„ํ—˜ ์˜ค๊ฒฝ๋ณด 10๊ฑด\n", "๋ณตํ•ฉ ์œ„ํ˜‘ ์žฌํ˜„์œจ 97.2% (๋ชฉํ‘œ 85% ์ด์ƒ: ๋‹ฌ์„ฑ), ์ฒ˜๋ฆฌ ์ง€์—ฐ 1๊ฑด 20.7ms / ์ „์ฒด 49ms (๋ชฉํ‘œ 200ms ์ดํ•˜: ๋‹ฌ์„ฑ)\n", "Baseline์ด ๋†“์นœ ์„œ์„œํžˆ ์˜ค๋ฅด๋Š” ์ค‘๊ธˆ์†(creeping_metal) ์ค‘ ๋ชจ๋ธ ๊ฒ€์ถœ: 17/22์‹œ๊ฐ„\n", "๋ชจ๋ธ ์œ„ํ—˜ ์˜ค๊ฒฝ๋ณด์˜ ์„ผ์„œ ๊ณ ์žฅ ์œ ํ˜•: {'drift': 10} -> drift(์„œ์„œํžˆ ํ‹€์–ด์ง)๋Š” ๋ฏธ์…˜ 5 ๊ต์ฐจ ๊ฒ€์ฆ์œผ๋กœ ๋ณด์™„\n", "์ค‘์š” ํŠน์ง• Top6: storm 0.17, turbidity 0.15, metal 0.10, turbidity_dev 0.09, ph 0.07, river 0.06\n", "\n", "[๋ณตํ•ฉ ์œ„ํ˜‘ ์‹œ๋‚˜๋ฆฌ์˜ค 10์ข…]\n", " ์‹œ๋‚˜๋ฆฌ์˜ค ๊ตฌ๋ถ„ ํƒ์ง€ ์ง€์—ฐ/์˜ค๊ฒฝ๋ณด ํŒ์ •\n", " ํ•˜์ˆ˜ ์œ ์ž… ์˜ค์—ผ 0h PASS\n", " ์ค‘๊ธˆ์† ๊ธ‰์ฆ ์˜ค์—ผ 0h PASS\n", " ์†Œ๋… ์‹คํŒจ ์˜ค์—ผ 3h PASS\n", " ํ™”ํ•™ ์˜ค์—ผ(pH ๊ธ‰๋ณ€) ์˜ค์—ผ 0h PASS\n", " ์ค‘๊ธˆ์† ์„œ์„œํžˆ ์ฆ๊ฐ€ ์˜ค์—ผ 2h PASS\n", " ํ•˜์ˆ˜+์ค‘๊ธˆ์† ๋™์‹œ ์˜ค์—ผ 0h PASS\n", " ์•ฝํ•œ ํ•˜์ˆ˜ ์œ ์ž…(๊ฐ•๋„ 60%) ์˜ค์—ผ 0h PASS\n", " ๊ต๋ž€: ํƒ๋„ ์„ผ์„œ ๋“œ๋ฆฌํ”„ํŠธ ๊ต๋ž€ ์œ„ํ—˜ ์˜ค๊ฒฝ๋ณด 13h FAIL\n", " ๊ต๋ž€: ์ž”๋ฅ˜์—ผ์†Œ ๋‹จ๋ฐœ ์ŠคํŒŒ์ดํฌ ๊ต๋ž€ ์œ„ํ—˜ ์˜ค๊ฒฝ๋ณด 0h PASS\n", "๊ต๋ž€: ํ†ต์‹  ๋‹จ์ ˆ(์ „ ํ•ญ๋ชฉ ๊ฒฐ์ธก) ๊ต๋ž€ ์œ„ํ—˜ ์˜ค๊ฒฝ๋ณด 0h PASS\n", "ํ†ต๊ณผ 9/10 (์˜ค์—ผ: ์‹œ์ž‘ ํ›„ 8์‹œ๊ฐ„ ๋‚ด ์œ„ํ—˜ ํŒ์ •, ๊ต๋ž€: ์œ„ํ—˜ ์˜ค๊ฒฝ๋ณด 2์‹œ๊ฐ„ ์ดํ•˜)\n", "```\n", "
\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "---\n", "\n", "## ๐ŸŽฏ ๋ฏธ์…˜ 05. ์„ผ์„œ ์˜ค๋ฅ˜์— ์˜ํ•œ ๋‹จ์ˆ˜ ์˜ค๊ฒฝ๋ณด ๋ฐฉ์ง€ ๋ฐ ์ˆ˜์งˆ ์•ˆ์ „ ์‹ ๋ขฐ๋„ ์ง€์ˆ˜ ์‚ฐ์ถœ ๋ชจ๋“ˆ ๊ฐœ๋ฐœ\n", "**์†Œ์š”์‹œ๊ฐ„**: ์•ฝ 3.0์‹œ๊ฐ„\n", "\n", "### ๐Ÿ“‹ ๋ฏธ์…˜ ๋ฐฐ๊ฒฝ ๋ฐ ์ˆ˜ํ–‰ ์ง€์นจ\n", "#### ๋ฏธ์…˜ ๋ฐฐ๊ฒฝ ๋ฐ ๋‹น์œ„์„ฑ\n", "\n", "์„ผ์„œ ์˜ค๋ฅ˜์— ์˜ํ•œ ๋‹จ์ˆ˜ ์˜ค๊ฒฝ๋ณด ๋ฐฉ์ง€ ์‹ ๋ขฐ๋„ ์ง€์ˆ˜ ์‚ฐ์ถœ ๋ชจ๋“ˆ์„ ๊ฐœ๋ฐœํ•œ๋‹ค. ์‹ค์ œ ์ „์ˆ  ํ˜„์žฅ์—์„œ๋Š” ๋‹จ์ˆœ ํƒ์ง€์— ๊ทธ์น˜์ง€ ์•Š๊ณ  ์‹ ์†ํ•œ ์œ„ํ˜‘ ํŒ๋‹จ๊ณผ ์‹ ๋ขฐ๋„ ๋†’์€ ์˜์‚ฌ๊ฒฐ์ •์ด ํ•„์ˆ˜์ ์ด๋ฏ€๋กœ, **์„ผ์„œ ์˜ค๋ฅ˜๋กœ ์ธํ•œ ๊ธ‰์ˆ˜ ์ค‘๋‹จ ์˜ค๊ฒฝ๋ณด๋ฅผ ์ค„์ด๋Š” ์ˆ˜์งˆ ์•ˆ์ „ ์‹ ๋ขฐ๋„ ์ง€์ˆ˜์™€ HighยทMedยทLow ๋“ฑ๊ธ‰ ๋ชจ๋“ˆ์„ ๊ฐœ๋ฐœํ•œ๋‹ค.** ๊ณผ์—…์„ ์™„์ˆ˜ํ•˜์—ฌ ์˜คํŒ์„ ๋ฐฉ์ง€ํ•˜๊ณ  ์ž‘์ „ ๋Œ€์‘๋ ฅ์„ ๊ทน๋Œ€ํ™”ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.\n", "\n", "์ด ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด ๋ณธ ๋ฏธ์…˜์—์„œ๋Š” **์œ„ํ—˜ ๊ฒฝ๋ณด๋งˆ๋‹ค ํ•ญ๋ชฉ๋ณ„ ํŽธ์ฐจ์˜ ์ผ์น˜๋„, ๊ฒฝ๋ณด ์ง€์† ์‹œ๊ฐ„, ์„ผ์„œ ์ด์ƒ ํ‘œ์‹œ, ๋“œ๋ฆฌํ”„ํŠธ ์˜์‹ฌ ์—ฌ๋ถ€๋ฅผ ์ ์ˆ˜ํ™”ํ•ด 0~100์  ์‹ ๋ขฐ๋„๋ฅผ ๋งŒ๋“ค๊ณ  HighยทMedยทLow๋กœ ๋‚˜๋ˆˆ๋‹ค ๊ณผ์ •์„ ์ง„ํ–‰ํ•˜๋Š”** ์‹ค์Šต์„ ์ฒด๊ณ„์ ์œผ๋กœ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค.\n", "\n", "ํ•™์Šต์ž๋Š” ์‹ค์Šต๊ฐ€์ด๋“œ์— ์ œ์‹œ๋œ ์ด 4๊ฐœ ์„ธ๋ถ€ ํ™œ๋™(๊ธฐ๋ณธ ๊ธฐ๋Šฅ ๊ตฌํ˜„ โž” ๊ฒฐ๊ณผ ์ง€ํ‘œ ๊ฒ€์ฆ โž” ํŒŒ๋ผ๋ฏธํ„ฐ ํŠœ๋‹ ๋ฐ ๋„์ „๊ณผ์ œ)์„ ์ˆœ์ฐจ์ ์œผ๋กœ ์ง„ํ–‰ํ•˜๋ฉฐ ํ•ต์‹ฌ ํŒ์ • ๊ธฐ์ค€์„ ๋‹ฌ์„ฑํ•ฉ๋‹ˆ๋‹ค.\n", "\n", "**์ด๋ฒˆ ๊ฒฐ๊ณผ๋ฌผ:** `src/verification/wel03_confidence_engine.py` ํ˜•ํƒœ์˜ ๊ฒฐ๊ณผ๋ฌผ์„ ์™„์„ฑํ•œ๋‹ค.\n", "\n", "### ๐Ÿ” ํ‰๊ฐ€ ๋ฐ ๊ฒ€์ฆ ๊ธฐ์ค€ (PASS ์กฐ๊ฑด)\n", "#### ๋ฏธ์…˜ ํŒ์ •\n", "\n", "**ํ‰๊ฐ€ ๊ตฌ๋ถ„:** ํ•„์ˆ˜\n", "\n", "**ํ•ต์‹ฌ ํ‰๊ฐ€์˜์—ญ:** ์‹ ๋ขฐ๋„ ๋“ฑ๊ธ‰(High/Med/Low) ๋ถ€์—ฌ ๊ธฐ์ค€์˜ ๊ตฐ์‚ฌ์  ํƒ€๋‹น์„ฑ\n", "\n", "#### ๋ฏธ์…˜ PASS ์กฐ๊ฑด\n", "\n", "- ์ •๋Ÿ‰ ๊ธฐ์ค€: ์˜ค๊ฒฝ๋ณด์œจ(FAR) 40% ์ด์ƒ ๊ฐ์ถ•, ์‹ ๋ขฐ๋„ ์Šค์ฝ”์–ด ์‚ฐ์ถœ ์ผ๊ด€์„ฑ 90% ์ด์ƒ\n", "\n", "\n", "- ์ง„ํ–‰ ๊ธฐ์ค€: ์˜ค๊ฒฝ๋ณด ํ•„ํ„ฐ๋ง ์ „ํ›„ ๋น„๊ต ์ง€ํ‘œ ์‚ฐ์ถœ ๋ฐ ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ ์™„๋ฃŒ\n", "\n", "\n", "- ๊ฒ€์ฆ ๋ฐฉ๋ฒ•: ROC-AUC ๊ณก์„  ๋ถ„์„ ๋ฐ ์ž„๊ณ„๊ฐ’ ๋ฏผ๊ฐ๋„ ํ…Œ์ŠคํŠธ\n", "\n", "\n", "#### ์ฑ„์ ์ž ํŒ์ • ๋ฉ”๋ชจ\n", "\n", "> ๋‹จ์ˆœ ์ฝ”๋“œ ์‹คํ–‰ ์—ฌ๋ถ€๋ฟ๋งŒ ์•„๋‹ˆ๋ผ ์ถœ๋ ฅ๋œ ์ˆ˜์น˜์™€ ๊ทผ๊ฑฐ ์ž๋ฃŒ๊ฐ€ ๊ตฐ ์‹ค๋ฌด ์š”๊ตฌ์‚ฌํ•ญ์— ๋ถ€ํ•ฉํ•˜๋Š”์ง€ ๋ฉด๋ฐ€ํžˆ ํ™•์ธํ•œ๋‹ค.\n", "\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "\"\"\"์œ„ํ—˜ ๊ฒฝ๋ณด๋งˆ๋‹ค ๋‹คํ•ญ๋ชฉ ๋™์‹œ ๋ณ€ํ™”ยท์—ฐ์† ์ง€์†ยท์„ผ์„œ ์ƒํƒœยท๋‹จ์ผ ํ•ญ๋ชฉ ๋“œ๋ฆฌํ”„ํŠธ ์—ฌ๋ถ€๋กœ ์ˆ˜์งˆ ์•ˆ์ „ ์‹ ๋ขฐ๋„ ์ง€์ˆ˜(0~100, High/Med/Low)๋ฅผ ์‚ฐ์ถœํ•ด, ์„ผ์„œ ์˜ค๋ฅ˜์— ์˜ํ•œ ๊ธ‰์ˆ˜ ์ค‘๋‹จ(๋‹จ์ˆ˜) ์˜ค๊ฒฝ๋ณด๋ฅผ ์ค„์ด๊ณ  ROC-AUCยท์ž„๊ณ„๊ฐ’ ๋ฏผ๊ฐ๋„ยท์‚ฐ์ถœ ์ผ๊ด€์„ฑ์„ ํ™•์ธํ•œ๋‹ค.\"\"\"\n", "import numpy as np\n", "import pandas as pd\n", "import matplotlib.pyplot as plt\n", "from sklearn.ensemble import RandomForestClassifier\n", "from sklearn.metrics import roc_auc_score, roc_curve\n", "\n", "# ---------------------------------------------------------------------------\n", "# [๊ต์œก์šฉ๊ฐ€์ƒ์ž๋ฃŒ] ์•ผ์ „ ๊ธ‰์ˆ˜์› ๊ฐ€์ƒ ์ˆ˜์งˆ ์„ผ์„œ ๋ฐ์ดํ„ฐ ์ƒ์„ฑ๊ธฐ (์‹ค์ œ ๋ถ€๋Œ€ยท์œ„์น˜ยท๊ฒ€์‚ฌ ๊ฒฐ๊ณผ ์—†์Œ)\n", "# ๊ฐ€์ƒ ๊ธ‰์ˆ˜์› 8๊ณณ x 14์ผ x 1์‹œ๊ฐ„ ๊ฐ„๊ฒฉ. ์‹ค์ œ ์ˆ˜์งˆ(true_*)ยท์„ผ์„œ ์ธก์ •๊ฐ’ยท์˜ค์—ผ ์‚ฌ๊ฑดยท์„ผ์„œ ๊ณ ์žฅ ์ •๋‹ต์„ ํ•จ๊ป˜ ๋งŒ๋“ ๋‹ค.\n", "# ๊ธฐ์ค€์น˜๋Š” ๊ต์œก์šฉ ๊ฐ€์ •๊ฐ’์ด๋ฉฐ, ์‹ค์ œ ์Œ์šฉ ํŒ์ •์€ ๊ณต์ธ ๊ฒ€์‚ฌ ๊ฒฐ๊ณผ์™€ ์˜๋ฌด(์˜ˆ๋ฐฉ์˜ํ•™) ๋‹ด๋‹น ๊ธฐ์ค€์— ๋”ฐ๋ฅธ๋‹ค.\n", "# ---------------------------------------------------------------------------\n", "PARAMS = [\"ph\", \"turbidity\", \"chlorine\", \"metal\", \"ecoli\"]\n", "SOURCES = [(\"W01\", \"water_truck\", 1.0, 2.0), (\"W02\", \"water_truck\", 3.5, 1.2), (\"W03\", \"purification_unit\", 2.2, 4.1),\n", " (\"W04\", \"purification_unit\", 5.0, 3.3), (\"W05\", \"well\", 0.8, 5.6), (\"W06\", \"well\", 4.4, 6.0),\n", " (\"W07\", \"river_intake\", 6.3, 1.8), (\"W08\", \"river_intake\", 6.9, 4.9)]\n", "BASE = {\"water_truck\": (7.2, 0.25, 0.75, 1.5), \"purification_unit\": (7.0, 0.20, 0.85, 1.2),\n", " \"well\": (6.8, 0.45, 0.50, 3.0), \"river_intake\": (7.3, 0.55, 0.60, 2.0)} # pH, ํƒ๋„(NTU), ์ž”๋ฅ˜์—ผ์†Œ(mg/L), ์ค‘๊ธˆ์†(ug/L)\n", "LIMITS = {\"ph\": (5.8, 8.5), \"turbidity\": (1.0, 2.0), \"chlorine\": (0.2, 0.1), \"metal\": (5.0, 10.0), \"ecoli\": (1, 1)} # (์ฃผ์˜, ์œ„ํ—˜) ๊ฐ€์ • ๊ธฐ์ค€\n", "EVENT_TYPES = { # ์˜ค์—ผ ์‚ฌ๊ฑด: ๋ณ€์ˆ˜๋ณ„ ์ตœ๋Œ€ ๋ณ€ํ™”๋Ÿ‰, ์ตœ๋Œ€์น˜ ๋„๋‹ฌ ์‹œ๊ฐ„(h)\n", " \"sewage\": ({\"turbidity\": 2.8, \"chlorine\": -0.5, \"ecoli\": 30, \"ph\": -0.3}, 4),\n", " \"metal\": ({\"metal\": 22.0, \"ph\": -0.5}, 4),\n", " \"disinfect_fail\": ({\"chlorine\": -0.8, \"ecoli\": 6}, 6),\n", " \"chemical\": ({\"ph\": 1.7, \"turbidity\": 1.0}, 3),\n", " \"creeping_metal\": ({\"metal\": 13.0}, 18),\n", "}\n", "FAULT_TYPES = [\"stuck\", \"spike\", \"drift\", \"dropout\"]\n", "NOISE = {\"ph\": 0.04, \"turbidity\": 0.06, \"chlorine\": 0.03, \"metal\": 0.35}\n", "DIGITS = {\"ph\": 2, \"turbidity\": 2, \"chlorine\": 2, \"metal\": 1}\n", "\n", "\n", "def make_dataset(days=14, seed=7, n_events=6, n_faults=10, events=None, faults=None, sources=None):\n", " \"\"\"events: [(๊ธ‰์ˆ˜์› ๋ฒˆํ˜ธ, ์‚ฌ๊ฑด ์œ ํ˜•, ์‹œ์ž‘ ์‹œ๊ฐ h, ์ง€์† h, ๊ฐ•๋„)], faults: [(๊ธ‰์ˆ˜์› ๋ฒˆํ˜ธ, ๊ณ ์žฅ ์œ ํ˜•, ํ•ญ๋ชฉ, ์‹œ์ž‘ h, ์ง€์† h)]\"\"\"\n", " rng = np.random.default_rng(seed)\n", " sources = sources or SOURCES\n", " T = days * 24\n", " tt = np.arange(T)\n", " hour = tt % 24\n", " night = (hour < 6) | (hour >= 20)\n", " rain = np.zeros(T)\n", " for _ in range(max(1, days // 4)):\n", " s0, d = int(rng.integers(0, T - 14)), int(rng.integers(6, 14))\n", " rain[s0:s0 + d] = np.maximum(rain[s0:s0 + d], rng.uniform(0.6, 1.0))\n", " if events is None:\n", " events = [(int(rng.integers(len(sources))), str(rng.choice(list(EVENT_TYPES))), int(rng.integers(min(24, T // 4), max(T // 4 + 1, T - 48))),\n", " int(rng.integers(10, 30)), 1.0) for _ in range(n_events)]\n", " if faults is None:\n", " faults = [(int(rng.integers(len(sources))), str(rng.choice(FAULT_TYPES)), str(rng.choice([\"ph\", \"turbidity\", \"chlorine\", \"metal\"])),\n", " int(rng.integers(12, max(13, T - 30))), int(rng.integers(6, 24))) for _ in range(n_faults)]\n", " out = []\n", " for si, (sid, stype, x, y) in enumerate(sources):\n", " ph0, tb0, cl0, mt0 = BASE[stype]\n", " tr = {\"ph\": ph0 + 0.08 * np.sin(2 * np.pi * hour / 24) + rng.normal(0, 0.03, T),\n", " \"turbidity\": tb0 * (1 + 0.1 * rng.normal(0, 1, T)),\n", " \"chlorine\": (cl0 * np.exp(-0.022 * hour) if stype == \"water_truck\" else cl0 + rng.normal(0, 0.03, T)),\n", " \"metal\": mt0 + rng.normal(0, 0.2, T), \"ecoli\": np.zeros(T)}\n", " if stype in (\"river_intake\", \"well\"):\n", " tr[\"turbidity\"] = tr[\"turbidity\"] + rain * (0.9 if stype == \"river_intake\" else 0.45)\n", " f_all, etype = np.zeros(T), np.array([\"none\"] * T, dtype=object)\n", " for (s_i, et, onset, dur, scale) in events:\n", " if s_i != si:\n", " continue\n", " delta, ramp = EVENT_TYPES[et]\n", " f = np.clip((tt - onset + 1) / ramp, 0, 1) * ((tt >= onset) & (tt < onset + dur + ramp))\n", " for k, v in delta.items():\n", " tr[k] = tr[k] + v * f * scale\n", " etype[f > 0] = et\n", " f_all = np.maximum(f_all, f)\n", " tr[\"chlorine\"] = np.clip(tr[\"chlorine\"], 0.02, None)\n", " tr[\"turbidity\"] = np.clip(tr[\"turbidity\"], 0.05, None)\n", " tr[\"metal\"] = np.clip(tr[\"metal\"], 0.1, None)\n", " grade = np.where(f_all >= 0.2, 2, 0)\n", " mild = (tr[\"turbidity\"] > LIMITS[\"turbidity\"][0]) | (tr[\"chlorine\"] < LIMITS[\"chlorine\"][0]) | (tr[\"metal\"] > LIMITS[\"metal\"][0])\n", " grade = np.where((grade == 0) & mild, 1, grade)\n", " ms = {}\n", " for p in [\"ph\", \"turbidity\", \"chlorine\", \"metal\"]:\n", " sd = NOISE[p] * np.where(night, 1.5, 1.0) * (tr[p] if p == \"turbidity\" else 1)\n", " ms[p] = np.round(np.clip(tr[p] + rng.normal(0, 1, T) * sd, 0, None), DIGITS[p])\n", " ms[\"ecoli\"] = (rng.poisson(tr[\"ecoli\"]) + (rng.random(T) < 0.004)).astype(float)\n", " fault, fparam = np.array([\"none\"] * T, dtype=object), np.array([\"-\"] * T, dtype=object)\n", " for (s_i, ft, p, s0, d) in faults:\n", " if s_i != si:\n", " continue\n", " seg = slice(s0, min(T, s0 + d))\n", " if ft == \"stuck\":\n", " ms[p][seg] = ms[p][s0]\n", " elif ft == \"spike\":\n", " seg = slice(s0, s0 + 1)\n", " ms[p][s0] = {\"ph\": 11.8, \"turbidity\": 38.0, \"chlorine\": 0.0, \"metal\": 75.0}[p]\n", " elif ft == \"drift\":\n", " ramp = np.linspace(0, 1, seg.stop - s0)\n", " ms[p][seg] = np.clip(ms[p][seg] + ramp * {\"ph\": -1.6, \"turbidity\": 2.6, \"chlorine\": -0.75, \"metal\": 12.0}[p], 0, None).round(DIGITS[p])\n", " elif ft == \"dropout\":\n", " for q in PARAMS:\n", " ms[q][seg] = np.nan\n", " p = \"all\"\n", " fault[seg], fparam[seg] = ft, p\n", " df = pd.DataFrame({\"source_id\": sid, \"source_type\": stype, \"x_km\": x, \"y_km\": y, \"t\": tt,\n", " \"cond\": np.where(rain > 0, \"storm\", np.where(night, \"night\", \"day\")),\n", " **{p: ms[p] for p in PARAMS}, **{f\"true_{p}\": tr[p] for p in PARAMS},\n", " \"event\": etype, \"fault\": fault, \"fault_param\": fparam, \"grade\": grade})\n", " out.append(df)\n", " return pd.concat(out, ignore_index=True)\n", "# ---------------------------------------------------------------------------\n", "\n", "# ---- ์ „์ฒ˜๋ฆฌ ๊ณตํ†ต ํ•จ์ˆ˜: ๋ฌผ๋ฆฌ ๋ฒ”์œ„ ์ ๊ฒ€ -> ๊ณ ์ •๊ฐ’(stuck) ํ‘œ์‹œ -> ๋‹จ๋ฐœ ์ŠคํŒŒ์ดํฌ ์ œ๊ฑฐ -> ๊ฒฐ์ธก ๋ณด์ • ----\n", "PHYS = {\"ph\": (0, 14), \"turbidity\": (0, 1000), \"chlorine\": (0, 10), \"metal\": (0, 1000), \"ecoli\": (0, 100000)}\n", "SPIKE_K = {\"day\": 6.0, \"night\": 7.0, \"storm\": 9.0} # ์ƒํ™ฉ๋ณ„(์ฃผ๊ฐ„/์•ผ๊ฐ„/์•…์ฒœํ›„) ์ŠคํŒŒ์ดํฌ ํŒ์ • ๋ฐฐ์ˆ˜\n", "MIN_JUMP = {\"ph\": 0.8, \"turbidity\": 1.5, \"chlorine\": 0.3, \"metal\": 6.0}\n", "STUCK_RUN, SHORT_GAP = 6, 3\n", "\n", "\n", "def clean(df, spike_k=None, min_jump=None):\n", " \"\"\"๊ธ‰์ˆ˜์›๋ณ„ ์‹œ๊ณ„์—ด์„ ์ •์ œํ•˜๊ณ  ํ•ญ๋ชฉ๋ณ„ ์ฒ˜๋ฆฌ ๋‚ด์—ญ(<ํ•ญ๋ชฉ>_flag: range/stuck/spike/interp/long_gap)์„ ๋‚จ๊ธด๋‹ค.\"\"\"\n", " spike_k = spike_k or SPIKE_K\n", " min_jump = MIN_JUMP if min_jump is None else min_jump\n", " out = df.sort_values([\"source_id\", \"t\"]).reset_index(drop=True)\n", " for sid, idx in out.groupby(\"source_id\").groups.items():\n", " idx = np.asarray(idx)\n", " kk = out.loc[idx, \"cond\"].map(spike_k).to_numpy(float)\n", " for p in PARAMS:\n", " v = out.loc[idx, p].to_numpy(float).copy()\n", " fl = np.array([\"\"] * len(v), dtype=object)\n", " lo, hi = PHYS[p]\n", " bad = (v < lo) | (v > hi)\n", " v[bad], fl[bad] = np.nan, \"range\"\n", " if p != \"ecoli\":\n", " chg = np.r_[True, v[1:] != v[:-1]] # ๊ฐ™์€ ๊ฐ’์ด STUCK_RUN์‹œ๊ฐ„ ์ด์ƒ ๋ฐ˜๋ณต = ๊ณ ์ •๊ฐ’ ๊ณ ์žฅ ์˜์‹ฌ\n", " rid = np.cumsum(chg)\n", " rlen = np.bincount(rid)[rid]\n", " fl[(rlen >= STUCK_RUN) & ~chg & ~np.isnan(v)] = \"stuck\"\n", " d = np.abs(np.diff(v[~np.isnan(v)]))\n", " sig = 1.4826 * np.median(d) + 1e-6 if len(d) else 1.0\n", " up, dn = v - np.r_[np.nan, v[:-1]], v - np.r_[v[1:], np.nan]\n", " thr = np.maximum(kk * sig, min_jump[p])\n", " spike = (np.abs(up) > thr) & (np.abs(dn) > thr) & (np.sign(up) == np.sign(dn)) # ํŠ€์—ˆ๋‹ค๊ฐ€ ๋ฐ”๋กœ ๋Œ์•„์˜จ ๊ฐ’๋งŒ\n", " v[spike], fl[spike] = np.nan, \"spike\"\n", " s = pd.Series(v)\n", " na = s.isna().to_numpy()\n", " grp = np.cumsum(np.r_[True, na[1:] != na[:-1]])\n", " glen = pd.Series(na.astype(int)).groupby(grp).transform(\"sum\").to_numpy()\n", " lin, ff = s.interpolate(limit_area=\"inside\").to_numpy(), s.ffill().bfill().to_numpy()\n", " short, longg = na & (glen <= SHORT_GAP), na & (glen > SHORT_GAP)\n", " v = np.where(short, np.where(np.isnan(lin), ff, lin), np.where(longg, ff, v))\n", " fl[short & (fl == \"\")] = \"interp\"\n", " fl[longg] = \"long_gap\"\n", " out.loc[idx, p] = v\n", " out.loc[idx, f\"{p}_flag\"] = fl\n", " return out\n", "\n", "# ---- ๊ฐ€์ • ๊ธฐ์ค€์น˜ ํŒ์ •(Baseline)๊ณผ ์‹œ๊ณ„์—ด ํŠน์ง• ๊ณตํ†ต ํ•จ์ˆ˜ ----\n", "GRADE_NAME = {0: \"์•ˆ์ „\", 1: \"์ฃผ์˜\", 2: \"์œ„ํ—˜\"}\n", "\n", "\n", "def rule_grade(df):\n", " \"\"\"๊ธฐ์ค€์น˜ 1์ฐจ ํ•„ํ„ฐ: ์œ„ํ—˜(์Œ์šฉ ๋ถ€์ ํ•ฉ ์˜์‹ฌ) = ํ•œ ํ•ญ๋ชฉ์ด๋ผ๋„ ์œ„ํ—˜ ๊ธฐ์ค€ ์ดˆ๊ณผ, ์ฃผ์˜ = ์ฃผ์˜ ๊ธฐ์ค€ ์ดˆ๊ณผ.\"\"\"\n", " ph, tb, cl, mt, ec = (df[p].to_numpy(float) for p in PARAMS)\n", " danger = (ph < LIMITS[\"ph\"][0]) | (ph > LIMITS[\"ph\"][1]) | (tb > LIMITS[\"turbidity\"][1]) | (cl < LIMITS[\"chlorine\"][1]) \\\n", " | (mt > LIMITS[\"metal\"][1]) | (ec >= LIMITS[\"ecoli\"][1])\n", " caution = (tb > LIMITS[\"turbidity\"][0]) | (cl < LIMITS[\"chlorine\"][0]) | (mt > LIMITS[\"metal\"][0])\n", " return np.where(danger, 2, np.where(caution, 1, 0))\n", "\n", "\n", "def reasons(row):\n", " r = []\n", " if not LIMITS[\"ph\"][0] <= row[\"ph\"] <= LIMITS[\"ph\"][1]:\n", " r.append(f\"pH {row['ph']:.2f}\")\n", " if row[\"turbidity\"] > LIMITS[\"turbidity\"][0]:\n", " r.append(f\"ํƒ๋„ {row['turbidity']:.2f}NTU\")\n", " if row[\"chlorine\"] < LIMITS[\"chlorine\"][0]:\n", " r.append(f\"์ž”๋ฅ˜์—ผ์†Œ {row['chlorine']:.2f}mg/L\")\n", " if row[\"metal\"] > LIMITS[\"metal\"][0]:\n", " r.append(f\"์ค‘๊ธˆ์† {row['metal']:.1f}ug/L\")\n", " if row[\"ecoli\"] >= 1:\n", " r.append(f\"๋Œ€์žฅ๊ท  {row['ecoli']:.0f}\")\n", " return \", \".join(r) or \"-\"\n", "\n", "\n", "FEATS = [f\"{p}{s}\" for p in [\"ph\", \"turbidity\", \"chlorine\", \"metal\"] for s in [\"\", \"_d1\", \"_d3\", \"_dev\", \"_sd6\"]] + \\\n", " [\"ecoli\", \"ecoli_3h\", \"storm\", \"night\", \"n_flag\", \"truck\", \"purif\", \"well\", \"river\"]\n", "\n", "\n", "def add_features(c):\n", " \"\"\"์‹œ๊ฐ„ ์ˆœ์„œ๋ฅผ ์ง€ํ‚ค๋Š”(๊ณผ๊ฑฐ ๊ฐ’๋งŒ ์“ฐ๋Š”) ๊ธ‰์ˆ˜์›๋ณ„ ํŠน์ง•: ๋ณ€ํ™”๋Ÿ‰, 24์‹œ๊ฐ„ ๊ธฐ์ค€์„  ๋Œ€๋น„ ํŽธ์ฐจ, 6์‹œ๊ฐ„ ๋ณ€๋™์„ฑ, ๋Œ€์žฅ๊ท  ๋ˆ„์  ๋“ฑ.\"\"\"\n", " c = c.sort_values([\"source_id\", \"t\"]).reset_index(drop=True)\n", " g = c.groupby(\"source_id\")\n", " for p in [\"ph\", \"turbidity\", \"chlorine\", \"metal\"]:\n", " c[f\"{p}_d1\"] = g[p].diff().fillna(0)\n", " c[f\"{p}_d3\"] = g[p].diff(3).fillna(0)\n", " base = g[p].transform(lambda s: s.shift(1).rolling(24, min_periods=4).median())\n", " c[f\"{p}_dev\"] = (c[p] - base).fillna(0)\n", " c[f\"{p}_sd6\"] = g[p].transform(lambda s: s.rolling(6, min_periods=2).std()).fillna(0)\n", " c[\"ecoli_3h\"] = g[\"ecoli\"].transform(lambda s: s.rolling(3, min_periods=1).sum())\n", " c[\"storm\"], c[\"night\"] = (c[\"cond\"] == \"storm\").astype(int), (c[\"cond\"] == \"night\").astype(int)\n", " flags = [f\"{p}_flag\" for p in PARAMS if f\"{p}_flag\" in c]\n", " c[\"n_flag\"] = sum(c[f].isin([\"stuck\", \"spike\", \"long_gap\", \"range\"]).astype(int) for f in flags) if flags else 0\n", " for k, name in [(\"truck\", \"water_truck\"), (\"purif\", \"purification_unit\"), (\"well\", \"well\"), (\"river\", \"river_intake\")]:\n", " c[k] = (c[\"source_type\"] == name).astype(int)\n", " return c\n", "\n", "SCALE = {\"ph\": 0.35, \"turbidity\": 0.6, \"chlorine\": 0.2, \"metal\": 3.0} # ํ•ญ๋ชฉ๋ณ„ '์˜๋ฏธ ์žˆ๋Š” ํŽธ์ฐจ' ํฌ๊ธฐ(๊ฐ€์ •)\n", "W = {\"corrob\": 40, \"persist\": 25, \"health\": 20, \"nondrift\": 15} # ์‹ ๋ขฐ๋„ ๊ฐ€์ค‘์น˜ ๋งคํŠธ๋ฆญ์Šค(ํ•ฉ 100)\n", "CUT = {\"High\": 60, \"Med\": 35} # High๋งŒ ๊ธ‰์ˆ˜ ์ค‘๋‹จ ๊ฒ€ํ†  ์š”์ฒญ์œผ๋กœ ์˜ฌ๋ฆฐ๋‹ค\n", "\n", "\n", "def prepare(df):\n", " c = add_features(clean(df))\n", " c[\"rule\"] = rule_grade(c)\n", " return c\n", "\n", "\n", "def confidence(c, alert):\n", " \"\"\"๊ฒฝ๋ณด(alert=True) ํ–‰๋งˆ๋‹ค ์‹ ๋ขฐ๋„ ์ ์ˆ˜์™€ ๊ตฌ์„ฑ ์š”์†Œ๋ฅผ ๊ณ„์‚ฐํ•œ๋‹ค.\"\"\"\n", " dev = pd.DataFrame({p: (c[f\"{p}_dev\"].abs() > SCALE[p]) | (c[f\"{p}_d3\"].abs() > SCALE[p]) for p in SCALE})\n", " n_dev = dev.sum(axis=1) + (c[\"ecoli_3h\"] >= 2)\n", " corrob = np.minimum(n_dev, 3) / 3\n", " a = pd.Series(alert.astype(int), index=c.index)\n", " run = a.groupby([c[\"source_id\"], (a != a.groupby(c[\"source_id\"]).shift()).cumsum()]).cumsum()\n", " persist = np.minimum(run, 3) / 3\n", " flagged = c[\"n_flag\"].groupby(c[\"source_id\"]).transform(lambda s: s.rolling(6, min_periods=1).max()) > 0\n", " health = 1 - flagged.astype(float)\n", " # ๋“œ๋ฆฌํ”„ํŠธ ์˜์‹ฌ: ํ•œ ํ•ญ๋ชฉ๋งŒ 24์‹œ๊ฐ„ ๊ธฐ์ค€์„ ์—์„œ ๋ฒ—์–ด๋‚˜๊ณ , 6์‹œ๊ฐ„ ๋ณ€๋™์„ฑ์ด ์ž‘์œผ๋ฉฐ(๋งค๋„๋Ÿฌ์šด ๋ณ€ํ™”) ๋Œ€์žฅ๊ท ์ด ์—†๋‹ค\n", " g = c.groupby(\"source_id\")\n", " jitter = {p: g[f\"{p}_d1\"].transform(lambda s: s.rolling(6, min_periods=3).std()).fillna(0) for p in SCALE}\n", " smooth = pd.concat([jitter[p] < SCALE[p] * 0.5 for p in SCALE], axis=1).all(axis=1) # ๋ณ€ํ™”๋Ÿ‰์ด ์ผ์ • = ๊ธฐ์šธ๊ธฐ๋งŒ ์žˆ๋Š” ๋ณ€ํ™”\n", " nondrift = 1 - ((n_dev <= 1) & smooth & (c[\"ecoli_3h\"] < 2)).astype(float)\n", " score = W[\"corrob\"] * corrob + W[\"persist\"] * persist + W[\"health\"] * health + W[\"nondrift\"] * nondrift\n", " level = np.where(score >= CUT[\"High\"], \"High\", np.where(score >= CUT[\"Med\"], \"Med\", \"Low\"))\n", " return pd.DataFrame({\"score\": score.round(1), \"level\": level, \"n_dev\": n_dev}, index=c.index)\n", "\n", "\n", "train = pd.concat([prepare(make_dataset(seed=s, n_events=14, n_faults=14)) for s in (7, 8)], ignore_index=True)\n", "model = RandomForestClassifier(n_estimators=200, max_depth=10, class_weight=\"balanced_subsample\", random_state=0, n_jobs=1)\n", "model.fit(train[FEATS], train[\"grade\"])\n", "\n", "test = prepare(make_dataset(seed=11, n_events=10, n_faults=14))\n", "alert = ((test[\"rule\"] == 2) | (model.predict(test[FEATS]) == 2)).to_numpy() # Baseline ๋˜๋Š” ๋ชจ๋ธ์ด ์œ„ํ—˜ ํŒ์ •\n", "conf = confidence(test, alert)\n", "A = test[alert].join(conf)\n", "A[\"real\"] = A[\"grade\"] == 2\n", "far_before = (~A[\"real\"]).mean()\n", "stop = A[A[\"level\"] == \"High\"]\n", "far_after = (~stop[\"real\"]).mean()\n", "cut = 1 - far_after / far_before\n", "print(f\"[๊ต์œก์šฉ๊ฐ€์ƒ์ž๋ฃŒ] ์œ„ํ—˜ ๊ฒฝ๋ณด {len(A)}๊ฑด(์‹ค์ œ ์œ„ํ—˜ {int(A['real'].sum())}๊ฑด, ์˜ค๊ฒฝ๋ณด {int((~A['real']).sum())}๊ฑด)\")\n", "print(f\"๊ธ‰์ˆ˜ ์ค‘๋‹จ ๊ฒ€ํ†  ๊ฒฝ๋ณด ์˜ค๊ฒฝ๋ณด์œจ(FAR): ์‹ ๋ขฐ๋„ ์ ์šฉ ์ „ {far_before:.1%} -> High๋งŒ ์ƒ์‹  {far_after:.1%} \"\n", " f\"({cut:.0%} ๊ฐ์ถ•, ๋ชฉํ‘œ 40% ์ด์ƒ: {'๋‹ฌ์„ฑ' if cut >= 0.4 else '๋ฏธ๋‹ฌ'})\")\n", "print(\"\\n[์‹ ๋ขฐ๋„ ๋“ฑ๊ธ‰๋ณ„ ๊ฒฝ๋ณด ์ˆ˜์™€ ์กฐ์น˜]\")\n", "tab = A.groupby(\"level\").agg(๊ฒฝ๋ณด=(\"real\", \"size\"), ์‹ค์ œ์œ„ํ—˜=(\"real\", \"sum\"), ํ‰๊ท ์ ์ˆ˜=(\"score\", \"mean\"))\n", "tab[\"์กฐ์น˜\"] = tab.index.map({\"High\": \"๊ธ‰์ˆ˜ ์ค‘๋‹จ ๊ฒ€ํ†  ์š”์ฒญ(HITL)\", \"Med\": \"์ฆ‰์‹œ ์žฌ์ฑ„์ˆ˜ยทํ˜„์žฅ ํ‚คํŠธ ๊ฒ€์‚ฌ\", \"Low\": \"์„ผ์„œ ์ ๊ฒ€ยท์žฌ์ธก์ •\"})\n", "print(tab.reindex([\"High\", \"Med\", \"Low\"]).round(1).to_string())\n", "print(\"์˜ค๊ฒฝ๋ณด์˜ ์›์ธ(์„ผ์„œ ๊ณ ์žฅ ์œ ํ˜•) x ๋“ฑ๊ธ‰:\")\n", "print(pd.crosstab(A.loc[~A[\"real\"], \"fault\"], A.loc[~A[\"real\"], \"level\"]).to_string())\n", "miss_real = int((A[\"real\"] & (A[\"level\"] == \"Low\")).sum())\n", "print(f\"์‹ค์ œ ์œ„ํ—˜์ธ๋ฐ Low๋กœ ๋‚ฎ์ถฐ์ง„ ๊ฒฝ๋ณด {miss_real}๊ฑด -> Low๋„ ์žฌ์ธก์ • ๋Œ€์ƒ์ด๋ฏ€๋กœ ๋ฌด์‹œ๋˜์ง€ ์•Š๋Š”๋‹ค\")\n", "\n", "auc = roc_auc_score(A[\"real\"], A[\"score\"])\n", "print(f\"\\nROC-AUC(์‹ ๋ขฐ๋„ ์ ์ˆ˜๋กœ ์‹ค์ œ ์œ„ํ—˜ ๊ตฌ๋ถ„) {auc:.3f}\")\n", "print(\"[์ž„๊ณ„๊ฐ’ ๋ฏผ๊ฐ๋„] High ๊ธฐ์ค€ / ์ƒ์‹  ๊ฒฝ๋ณด / ์˜ค๊ฒฝ๋ณด์œจ / ์‹ค์ œ ์œ„ํ—˜ ์ƒ์‹  ๋น„์œจ\")\n", "for th in [45, 55, 60, 70, 80]:\n", " s = A[A[\"score\"] >= th]\n", " print(f\" {th:>3} / {len(s):>4} / {(~s['real']).mean():6.1%} / {s['real'].sum() / A['real'].sum():6.1%}\")\n", "\n", "# ์‚ฐ์ถœ ์ผ๊ด€์„ฑ: ๊ฐ™์€ ์‚ฌ๊ฑดยท๊ณ ์žฅ ์กฐ๊ฑด์—์„œ ์ธก์ • ์žก์Œ๋งŒ ๋ฐ”๊ฟ” 5ํšŒ ์žฌ์‚ฐ์ถœํ–ˆ์„ ๋•Œ ์‹ค์ œ ์œ„ํ—˜ ๊ฒฝ๋ณด์˜ ๋“ฑ๊ธ‰ ์ผ์น˜์œจ\n", "EV = [(6, \"sewage\", 30, 14, 1.0), (4, \"metal\", 60, 12, 1.0), (2, \"chemical\", 40, 10, 1.0), (5, \"creeping_metal\", 20, 24, 1.0)]\n", "FT = [(1, \"drift\", \"turbidity\", 24, 18), (0, \"stuck\", \"chlorine\", 30, 10)]\n", "levels = []\n", "for s in range(5):\n", " c = prepare(make_dataset(days=4, seed=100 + s, events=EV, faults=FT))\n", " al = ((c[\"rule\"] == 2) | (model.predict(c[FEATS]) == 2)).to_numpy()\n", " levels.append(confidence(c, al)[\"level\"].where(al, \"-\").to_numpy())\n", "L = np.array(levels)\n", "ref = pd.Series(L[0]).mode().iloc[0]\n", "mask = (L != \"-\").sum(axis=0) >= 3\n", "agree = np.mean([pd.Series(col[col != \"-\"]).value_counts(normalize=True).iloc[0] for col in L[:, mask].T])\n", "print(f\"\\n์žก์Œ ์žฌํ˜„ 5ํšŒ ๋“ฑ๊ธ‰ ์ผ์น˜์œจ {agree:.1%} (๊ฒฝ๋ณด 3ํšŒ ์ด์ƒ ์‹œ์  {int(mask.sum())}๊ฐœ, ๋ชฉํ‘œ 90% ์ด์ƒ: {'๋‹ฌ์„ฑ' if agree >= 0.9 else '๋ฏธ๋‹ฌ'})\")\n", "print(\"โ€ป ์‹ ๋ขฐ๋„ ๋“ฑ๊ธ‰์€ ํŒ๋‹จ ๋ณด์กฐ๋‹ค. ๊ธ‰์ˆ˜ ์ค‘๋‹จยท์žฌ๊ฐœ๋Š” ์˜๋ฌด(์˜ˆ๋ฐฉ์˜ํ•™)ยทํ™˜๊ฒฝ ๋‹ด๋‹น ํ™•์ธ๊ณผ ์ง€ํœ˜๊ด€ ์Šน์ธ์œผ๋กœ ๊ฒฐ์ •ํ•œ๋‹ค.\")\n", "\n", "fpr, tpr, _ = roc_curve(A[\"real\"], A[\"score\"])\n", "fig, ax = plt.subplots(1, 2, figsize=(10, 4))\n", "ax[0].plot(fpr, tpr, color=\"tab:blue\"); ax[0].plot([0, 1], [0, 1], \"k--\", lw=0.8)\n", "ax[0].set_title(f\"ROC of confidence score (AUC {auc:.2f})\"); ax[0].set_xlabel(\"false positive rate\"); ax[0].set_ylabel(\"true positive rate\")\n", "ax[1].hist([A.loc[A[\"real\"], \"score\"], A.loc[~A[\"real\"], \"score\"]], bins=12, label=[\"real danger\", \"false alarm\"], color=[\"tab:red\", \"tab:gray\"])\n", "ax[1].axvline(CUT[\"High\"], color=\"k\", ls=\"--\"); ax[1].set_xlabel(\"confidence score\"); ax[1].legend(); ax[1].set_title(\"Score distribution (synthetic)\")\n", "plt.tight_layout(); plt.show()\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "
\n", "๐Ÿ’ก [์ฐธ๊ณ ] ๋ฏธ์…˜ 05 ์˜ˆ์ƒ ์‹คํ–‰ ๊ฒฐ๊ณผ (Output)\n", "\n", "```text\n", "[๊ต์œก์šฉ๊ฐ€์ƒ์ž๋ฃŒ] ์œ„ํ—˜ ๊ฒฝ๋ณด 262๊ฑด(์‹ค์ œ ์œ„ํ—˜ 240๊ฑด, ์˜ค๊ฒฝ๋ณด 22๊ฑด)\n", "๊ธ‰์ˆ˜ ์ค‘๋‹จ ๊ฒ€ํ†  ๊ฒฝ๋ณด ์˜ค๊ฒฝ๋ณด์œจ(FAR): ์‹ ๋ขฐ๋„ ์ ์šฉ ์ „ 8.4% -> High๋งŒ ์ƒ์‹  0.7% (91% ๊ฐ์ถ•, ๋ชฉํ‘œ 40% ์ด์ƒ: ๋‹ฌ์„ฑ)\n", "\n", "[์‹ ๋ขฐ๋„ ๋“ฑ๊ธ‰๋ณ„ ๊ฒฝ๋ณด ์ˆ˜์™€ ์กฐ์น˜]\n", " ๊ฒฝ๋ณด ์‹ค์ œ์œ„ํ—˜ ํ‰๊ท ์ ์ˆ˜ ์กฐ์น˜\n", "level \n", "High 137 136 84.1 ๊ธ‰์ˆ˜ ์ค‘๋‹จ ๊ฒ€ํ†  ์š”์ฒญ(HITL)\n", "Med 112 100 53.5 ์ฆ‰์‹œ ์žฌ์ฑ„์ˆ˜ยทํ˜„์žฅ ํ‚คํŠธ ๊ฒ€์‚ฌ\n", "Low 13 4 27.5 ์„ผ์„œ ์ ๊ฒ€ยท์žฌ์ธก์ •\n", "์˜ค๊ฒฝ๋ณด์˜ ์›์ธ(์„ผ์„œ ๊ณ ์žฅ ์œ ํ˜•) x ๋“ฑ๊ธ‰:\n", "level High Low Med\n", "fault \n", "drift 1 0 10\n", "none 0 9 2\n", "์‹ค์ œ ์œ„ํ—˜์ธ๋ฐ Low๋กœ ๋‚ฎ์ถฐ์ง„ ๊ฒฝ๋ณด 4๊ฑด -> Low๋„ ์žฌ์ธก์ • ๋Œ€์ƒ์ด๋ฏ€๋กœ ๋ฌด์‹œ๋˜์ง€ ์•Š๋Š”๋‹ค\n", "\n", "ROC-AUC(์‹ ๋ขฐ๋„ ์ ์ˆ˜๋กœ ์‹ค์ œ ์œ„ํ—˜ ๊ตฌ๋ถ„) 0.821\n", "[์ž„๊ณ„๊ฐ’ ๋ฏผ๊ฐ๋„] High ๊ธฐ์ค€ / ์ƒ์‹  ๊ฒฝ๋ณด / ์˜ค๊ฒฝ๋ณด์œจ / ์‹ค์ œ ์œ„ํ—˜ ์ƒ์‹  ๋น„์œจ\n", " 45 / 243 / 4.9% / 96.2%\n", " 55 / 210 / 5.2% / 82.9%\n", " 60 / 137 / 0.7% / 56.7%\n", " 70 / 134 / 0.7% / 55.4%\n", " 80 / 88 / 1.1% / 36.2%\n", "\n", "์žก์Œ ์žฌํ˜„ 5ํšŒ ๋“ฑ๊ธ‰ ์ผ์น˜์œจ 93.6% (๊ฒฝ๋ณด 3ํšŒ ์ด์ƒ ์‹œ์  99๊ฐœ, ๋ชฉํ‘œ 90% ์ด์ƒ: ๋‹ฌ์„ฑ)\n", "โ€ป ์‹ ๋ขฐ๋„ ๋“ฑ๊ธ‰์€ ํŒ๋‹จ ๋ณด์กฐ๋‹ค. ๊ธ‰์ˆ˜ ์ค‘๋‹จยท์žฌ๊ฐœ๋Š” ์˜๋ฌด(์˜ˆ๋ฐฉ์˜ํ•™)ยทํ™˜๊ฒฝ ๋‹ด๋‹น ํ™•์ธ๊ณผ ์ง€ํœ˜๊ด€ ์Šน์ธ์œผ๋กœ ๊ฒฐ์ •ํ•œ๋‹ค.\n", "```\n", "
\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "---\n", "\n", "## ๐ŸŽฏ ๋ฏธ์…˜ 06. ์•ผ์ „ ๊ธ‰์ˆ˜์ฐจ ๋ฐ ์ •์ˆ˜ ์‹œ์„ค๋ณ„ ์ˆ˜์งˆ ์ƒํƒœ ์‹ค์‹œ๊ฐ„ ์‹œ๊ฐํ™” ๋Œ€์‹œ๋ณด๋“œ ๊ตฌํ˜„\n", "**์†Œ์š”์‹œ๊ฐ„**: ์•ฝ 2.5์‹œ๊ฐ„\n", "\n", "### ๐Ÿ“‹ ๋ฏธ์…˜ ๋ฐฐ๊ฒฝ ๋ฐ ์ˆ˜ํ–‰ ์ง€์นจ\n", "#### ๋ฏธ์…˜ ๋ฐฐ๊ฒฝ ๋ฐ ๋‹น์œ„์„ฑ\n", "\n", "์•ผ์ „ ๊ธ‰์ˆ˜ ์‹œ์„ค๋ณ„ ์ˆ˜์งˆ ์ƒํƒœ ์‹ค์‹œ๊ฐ„ ์‹œ๊ฐํ™” ๋Œ€์‹œ๋ณด๋“œ๋ฅผ ๊ตฌํ˜„ํ•œ๋‹ค. ์‹ค์ œ ์ „์ˆ  ํ˜„์žฅ์—์„œ๋Š” ๋‹จ์ˆœ ํƒ์ง€์— ๊ทธ์น˜์ง€ ์•Š๊ณ  ์‹ ์†ํ•œ ์œ„ํ˜‘ ํŒ๋‹จ๊ณผ ์‹ ๋ขฐ๋„ ๋†’์€ ์˜์‚ฌ๊ฒฐ์ •์ด ํ•„์ˆ˜์ ์ด๋ฏ€๋กœ, **์•ผ์ „ ๊ธ‰์ˆ˜์ฐจ์™€ ์ •์ˆ˜ ์‹œ์„ค๋ณ„ ์ˆ˜์งˆ ์ƒํƒœ๋ฅผ ์‹ค์‹œ๊ฐ„์ฒ˜๋Ÿผ ๊ฐฑ์‹ ํ•ด ๋ณด์—ฌ ์ฃผ๋Š” ์ƒํ™ฉํŒ ๋Œ€์‹œ๋ณด๋“œ๋ฅผ ๊ตฌํ˜„ํ•œ๋‹ค.** ๊ณผ์—…์„ ์™„์ˆ˜ํ•˜์—ฌ ์˜คํŒ์„ ๋ฐฉ์ง€ํ•˜๊ณ  ์ž‘์ „ ๋Œ€์‘๋ ฅ์„ ๊ทน๋Œ€ํ™”ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.\n", "\n", "์ด ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด ๋ณธ ๋ฏธ์…˜์—์„œ๋Š” **๊ฐ€์ƒ ๊ธ‰์ˆ˜์› 8๊ณณ์˜ ์ธก์ • ์ŠคํŠธ๋ฆผ์„ 1์‹œ๊ฐ„ ๋‹จ์œ„๋กœ ๊ฐฑ์‹ ํ•˜๋ฉฐ ๊ธ‰์ˆ˜์› ์ƒํƒœ ์ง€๋„, ํ•ญ๋ชฉ๋ณ„ ๊ธฐ์ค€ ๋Œ€๋น„ ๋น„์œจ ํžˆํŠธ๋งต, ๊ฒฝ๋ณด ๊ธ‰์ˆ˜์› ์ถ”์ด, ์ƒํƒœ ํ˜„ํ™ฉํŒ, ๊ธด๊ธ‰ ๊ฒฝ๋ณด ๋ชฉ๋ก 5์ข… ์ปดํฌ๋„ŒํŠธ๋ฅผ ๊ทธ๋ฆฐ๋‹ค ๊ณผ์ •์„ ์ง„ํ–‰ํ•˜๋Š”** ์‹ค์Šต์„ ์ฒด๊ณ„์ ์œผ๋กœ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค.\n", "\n", "ํ•™์Šต์ž๋Š” ์‹ค์Šต๊ฐ€์ด๋“œ์— ์ œ์‹œ๋œ ์ด 4๊ฐœ ์„ธ๋ถ€ ํ™œ๋™(๊ธฐ๋ณธ ๊ธฐ๋Šฅ ๊ตฌํ˜„ โž” ๊ฒฐ๊ณผ ์ง€ํ‘œ ๊ฒ€์ฆ โž” ํŒŒ๋ผ๋ฏธํ„ฐ ํŠœ๋‹ ๋ฐ ๋„์ „๊ณผ์ œ)์„ ์ˆœ์ฐจ์ ์œผ๋กœ ์ง„ํ–‰ํ•˜๋ฉฐ ํ•ต์‹ฌ ํŒ์ • ๊ธฐ์ค€์„ ๋‹ฌ์„ฑํ•ฉ๋‹ˆ๋‹ค.\n", "\n", "**์ด๋ฒˆ ๊ฒฐ๊ณผ๋ฌผ:** `src/ui/wel03_dashboard.py (๋˜๋Š” ์›น UI ์ปดํฌ๋„ŒํŠธ)` ํ˜•ํƒœ์˜ ๊ฒฐ๊ณผ๋ฌผ์„ ์™„์„ฑํ•œ๋‹ค.\n", "\n", "### ๐Ÿ” ํ‰๊ฐ€ ๋ฐ ๊ฒ€์ฆ ๊ธฐ์ค€ (PASS ์กฐ๊ฑด)\n", "#### ๋ฏธ์…˜ ํŒ์ •\n", "\n", "**ํ‰๊ฐ€ ๊ตฌ๋ถ„:** ํ•„์ˆ˜\n", "\n", "**ํ•ต์‹ฌ ํ‰๊ฐ€์˜์—ญ:** ์ „์ˆ  ์ž‘์ „ ์ƒํ™ฉํŒ์œผ๋กœ์„œ์˜ ์ง๊ด€์„ฑ ๋ฐ ์ธ์ง€ ๋ถ€ํ•˜ ๊ฐ์†Œ๋„\n", "\n", "#### ๋ฏธ์…˜ PASS ์กฐ๊ฑด\n", "\n", "- ์ •๋Ÿ‰ ๊ธฐ์ค€: ํ™”๋ฉด ๊ฐฑ์‹  ์ฃผ๊ธฐ 1์ดˆ ์ด๋‚ด, UI ์‘๋‹ต์„ฑ 100ms ์ด๋‚ด\n", "\n", "\n", "- ์ง„ํ–‰ ๊ธฐ์ค€: ์ฃผ์š” ์ง€ํ‘œ ์‹œ๊ฐํ™” ์ปดํฌ๋„ŒํŠธ 4์ข… ์ด์ƒ ๋ Œ๋”๋ง ๋ฐ ์‹ค์‹œ๊ฐ„ ๋ฐ์ดํ„ฐ ์—ฐ๋™\n", "\n", "\n", "- ๊ฒ€์ฆ ๋ฐฉ๋ฒ•: ์‚ฌ์šฉ์ž ์ธํ„ฐ๋ž™์…˜ ํ…Œ์ŠคํŠธ ๋ฐ ๋ชจ์˜ ๋ฐ์ดํ„ฐ ์ŠคํŠธ๋ฆผ ๋ Œ๋”๋ง ๊ฒ€์ฆ\n", "\n", "\n", "#### ์ฑ„์ ์ž ํŒ์ • ๋ฉ”๋ชจ\n", "\n", "> ๋‹จ์ˆœ ์ฝ”๋“œ ์‹คํ–‰ ์—ฌ๋ถ€๋ฟ๋งŒ ์•„๋‹ˆ๋ผ ์ถœ๋ ฅ๋œ ์ˆ˜์น˜์™€ ๊ทผ๊ฑฐ ์ž๋ฃŒ๊ฐ€ ๊ตฐ ์‹ค๋ฌด ์š”๊ตฌ์‚ฌํ•ญ์— ๋ถ€ํ•ฉํ•˜๋Š”์ง€ ๋ฉด๋ฐ€ํžˆ ํ™•์ธํ•œ๋‹ค.\n", "\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "\"\"\"๊ฐ€์ƒ ๊ธ‰์ˆ˜์› 8๊ณณ์˜ ์ˆ˜์งˆ ์ธก์ • ์ŠคํŠธ๋ฆผ์„ 1์‹œ๊ฐ„ ๋‹จ์œ„๋กœ ๊ฐฑ์‹ ํ•˜๋ฉฐ ๊ธ‰์ˆ˜์› ์ƒํƒœ ์ง€๋„, ํ•ญ๋ชฉ๋ณ„ ๊ธฐ์ค€ ๋Œ€๋น„ ๋น„์œจ ํžˆํŠธ๋งต, ๊ฒฝ๋ณด ๊ธ‰์ˆ˜์› ์ถ”์ด, ์ƒํƒœ ํ˜„ํ™ฉํŒ, ๊ธด๊ธ‰ ๊ฒฝ๋ณด ๋ชฉ๋ก 5์ข…์„ ๊ทธ๋ฆฌ๋Š” ์ƒํ™ฉํŒ์„ ๊ตฌํ˜„ํ•˜๊ณ , ๊ฐฑ์‹  ์ฃผ๊ธฐยท์‘๋‹ต ์‹œ๊ฐ„์„ ์ธก์ •ํ•œ๋‹ค(์•ผ๊ฐ„ ๋ชจ๋“œ ์ง€์›).\"\"\"\n", "import time\n", "import numpy as np\n", "import pandas as pd\n", "import matplotlib.pyplot as plt\n", "\n", "# ---------------------------------------------------------------------------\n", "# [๊ต์œก์šฉ๊ฐ€์ƒ์ž๋ฃŒ] ์•ผ์ „ ๊ธ‰์ˆ˜์› ๊ฐ€์ƒ ์ˆ˜์งˆ ์„ผ์„œ ๋ฐ์ดํ„ฐ ์ƒ์„ฑ๊ธฐ (์‹ค์ œ ๋ถ€๋Œ€ยท์œ„์น˜ยท๊ฒ€์‚ฌ ๊ฒฐ๊ณผ ์—†์Œ)\n", "# ๊ฐ€์ƒ ๊ธ‰์ˆ˜์› 8๊ณณ x 14์ผ x 1์‹œ๊ฐ„ ๊ฐ„๊ฒฉ. ์‹ค์ œ ์ˆ˜์งˆ(true_*)ยท์„ผ์„œ ์ธก์ •๊ฐ’ยท์˜ค์—ผ ์‚ฌ๊ฑดยท์„ผ์„œ ๊ณ ์žฅ ์ •๋‹ต์„ ํ•จ๊ป˜ ๋งŒ๋“ ๋‹ค.\n", "# ๊ธฐ์ค€์น˜๋Š” ๊ต์œก์šฉ ๊ฐ€์ •๊ฐ’์ด๋ฉฐ, ์‹ค์ œ ์Œ์šฉ ํŒ์ •์€ ๊ณต์ธ ๊ฒ€์‚ฌ ๊ฒฐ๊ณผ์™€ ์˜๋ฌด(์˜ˆ๋ฐฉ์˜ํ•™) ๋‹ด๋‹น ๊ธฐ์ค€์— ๋”ฐ๋ฅธ๋‹ค.\n", "# ---------------------------------------------------------------------------\n", "PARAMS = [\"ph\", \"turbidity\", \"chlorine\", \"metal\", \"ecoli\"]\n", "SOURCES = [(\"W01\", \"water_truck\", 1.0, 2.0), (\"W02\", \"water_truck\", 3.5, 1.2), (\"W03\", \"purification_unit\", 2.2, 4.1),\n", " (\"W04\", \"purification_unit\", 5.0, 3.3), (\"W05\", \"well\", 0.8, 5.6), (\"W06\", \"well\", 4.4, 6.0),\n", " (\"W07\", \"river_intake\", 6.3, 1.8), (\"W08\", \"river_intake\", 6.9, 4.9)]\n", "BASE = {\"water_truck\": (7.2, 0.25, 0.75, 1.5), \"purification_unit\": (7.0, 0.20, 0.85, 1.2),\n", " \"well\": (6.8, 0.45, 0.50, 3.0), \"river_intake\": (7.3, 0.55, 0.60, 2.0)} # pH, ํƒ๋„(NTU), ์ž”๋ฅ˜์—ผ์†Œ(mg/L), ์ค‘๊ธˆ์†(ug/L)\n", "LIMITS = {\"ph\": (5.8, 8.5), \"turbidity\": (1.0, 2.0), \"chlorine\": (0.2, 0.1), \"metal\": (5.0, 10.0), \"ecoli\": (1, 1)} # (์ฃผ์˜, ์œ„ํ—˜) ๊ฐ€์ • ๊ธฐ์ค€\n", "EVENT_TYPES = { # ์˜ค์—ผ ์‚ฌ๊ฑด: ๋ณ€์ˆ˜๋ณ„ ์ตœ๋Œ€ ๋ณ€ํ™”๋Ÿ‰, ์ตœ๋Œ€์น˜ ๋„๋‹ฌ ์‹œ๊ฐ„(h)\n", " \"sewage\": ({\"turbidity\": 2.8, \"chlorine\": -0.5, \"ecoli\": 30, \"ph\": -0.3}, 4),\n", " \"metal\": ({\"metal\": 22.0, \"ph\": -0.5}, 4),\n", " \"disinfect_fail\": ({\"chlorine\": -0.8, \"ecoli\": 6}, 6),\n", " \"chemical\": ({\"ph\": 1.7, \"turbidity\": 1.0}, 3),\n", " \"creeping_metal\": ({\"metal\": 13.0}, 18),\n", "}\n", "FAULT_TYPES = [\"stuck\", \"spike\", \"drift\", \"dropout\"]\n", "NOISE = {\"ph\": 0.04, \"turbidity\": 0.06, \"chlorine\": 0.03, \"metal\": 0.35}\n", "DIGITS = {\"ph\": 2, \"turbidity\": 2, \"chlorine\": 2, \"metal\": 1}\n", "\n", "\n", "def make_dataset(days=14, seed=7, n_events=6, n_faults=10, events=None, faults=None, sources=None):\n", " \"\"\"events: [(๊ธ‰์ˆ˜์› ๋ฒˆํ˜ธ, ์‚ฌ๊ฑด ์œ ํ˜•, ์‹œ์ž‘ ์‹œ๊ฐ h, ์ง€์† h, ๊ฐ•๋„)], faults: [(๊ธ‰์ˆ˜์› ๋ฒˆํ˜ธ, ๊ณ ์žฅ ์œ ํ˜•, ํ•ญ๋ชฉ, ์‹œ์ž‘ h, ์ง€์† h)]\"\"\"\n", " rng = np.random.default_rng(seed)\n", " sources = sources or SOURCES\n", " T = days * 24\n", " tt = np.arange(T)\n", " hour = tt % 24\n", " night = (hour < 6) | (hour >= 20)\n", " rain = np.zeros(T)\n", " for _ in range(max(1, days // 4)):\n", " s0, d = int(rng.integers(0, T - 14)), int(rng.integers(6, 14))\n", " rain[s0:s0 + d] = np.maximum(rain[s0:s0 + d], rng.uniform(0.6, 1.0))\n", " if events is None:\n", " events = [(int(rng.integers(len(sources))), str(rng.choice(list(EVENT_TYPES))), int(rng.integers(min(24, T // 4), max(T // 4 + 1, T - 48))),\n", " int(rng.integers(10, 30)), 1.0) for _ in range(n_events)]\n", " if faults is None:\n", " faults = [(int(rng.integers(len(sources))), str(rng.choice(FAULT_TYPES)), str(rng.choice([\"ph\", \"turbidity\", \"chlorine\", \"metal\"])),\n", " int(rng.integers(12, max(13, T - 30))), int(rng.integers(6, 24))) for _ in range(n_faults)]\n", " out = []\n", " for si, (sid, stype, x, y) in enumerate(sources):\n", " ph0, tb0, cl0, mt0 = BASE[stype]\n", " tr = {\"ph\": ph0 + 0.08 * np.sin(2 * np.pi * hour / 24) + rng.normal(0, 0.03, T),\n", " \"turbidity\": tb0 * (1 + 0.1 * rng.normal(0, 1, T)),\n", " \"chlorine\": (cl0 * np.exp(-0.022 * hour) if stype == \"water_truck\" else cl0 + rng.normal(0, 0.03, T)),\n", " \"metal\": mt0 + rng.normal(0, 0.2, T), \"ecoli\": np.zeros(T)}\n", " if stype in (\"river_intake\", \"well\"):\n", " tr[\"turbidity\"] = tr[\"turbidity\"] + rain * (0.9 if stype == \"river_intake\" else 0.45)\n", " f_all, etype = np.zeros(T), np.array([\"none\"] * T, dtype=object)\n", " for (s_i, et, onset, dur, scale) in events:\n", " if s_i != si:\n", " continue\n", " delta, ramp = EVENT_TYPES[et]\n", " f = np.clip((tt - onset + 1) / ramp, 0, 1) * ((tt >= onset) & (tt < onset + dur + ramp))\n", " for k, v in delta.items():\n", " tr[k] = tr[k] + v * f * scale\n", " etype[f > 0] = et\n", " f_all = np.maximum(f_all, f)\n", " tr[\"chlorine\"] = np.clip(tr[\"chlorine\"], 0.02, None)\n", " tr[\"turbidity\"] = np.clip(tr[\"turbidity\"], 0.05, None)\n", " tr[\"metal\"] = np.clip(tr[\"metal\"], 0.1, None)\n", " grade = np.where(f_all >= 0.2, 2, 0)\n", " mild = (tr[\"turbidity\"] > LIMITS[\"turbidity\"][0]) | (tr[\"chlorine\"] < LIMITS[\"chlorine\"][0]) | (tr[\"metal\"] > LIMITS[\"metal\"][0])\n", " grade = np.where((grade == 0) & mild, 1, grade)\n", " ms = {}\n", " for p in [\"ph\", \"turbidity\", \"chlorine\", \"metal\"]:\n", " sd = NOISE[p] * np.where(night, 1.5, 1.0) * (tr[p] if p == \"turbidity\" else 1)\n", " ms[p] = np.round(np.clip(tr[p] + rng.normal(0, 1, T) * sd, 0, None), DIGITS[p])\n", " ms[\"ecoli\"] = (rng.poisson(tr[\"ecoli\"]) + (rng.random(T) < 0.004)).astype(float)\n", " fault, fparam = np.array([\"none\"] * T, dtype=object), np.array([\"-\"] * T, dtype=object)\n", " for (s_i, ft, p, s0, d) in faults:\n", " if s_i != si:\n", " continue\n", " seg = slice(s0, min(T, s0 + d))\n", " if ft == \"stuck\":\n", " ms[p][seg] = ms[p][s0]\n", " elif ft == \"spike\":\n", " seg = slice(s0, s0 + 1)\n", " ms[p][s0] = {\"ph\": 11.8, \"turbidity\": 38.0, \"chlorine\": 0.0, \"metal\": 75.0}[p]\n", " elif ft == \"drift\":\n", " ramp = np.linspace(0, 1, seg.stop - s0)\n", " ms[p][seg] = np.clip(ms[p][seg] + ramp * {\"ph\": -1.6, \"turbidity\": 2.6, \"chlorine\": -0.75, \"metal\": 12.0}[p], 0, None).round(DIGITS[p])\n", " elif ft == \"dropout\":\n", " for q in PARAMS:\n", " ms[q][seg] = np.nan\n", " p = \"all\"\n", " fault[seg], fparam[seg] = ft, p\n", " df = pd.DataFrame({\"source_id\": sid, \"source_type\": stype, \"x_km\": x, \"y_km\": y, \"t\": tt,\n", " \"cond\": np.where(rain > 0, \"storm\", np.where(night, \"night\", \"day\")),\n", " **{p: ms[p] for p in PARAMS}, **{f\"true_{p}\": tr[p] for p in PARAMS},\n", " \"event\": etype, \"fault\": fault, \"fault_param\": fparam, \"grade\": grade})\n", " out.append(df)\n", " return pd.concat(out, ignore_index=True)\n", "# ---------------------------------------------------------------------------\n", "\n", "# ---- ์ „์ฒ˜๋ฆฌ ๊ณตํ†ต ํ•จ์ˆ˜: ๋ฌผ๋ฆฌ ๋ฒ”์œ„ ์ ๊ฒ€ -> ๊ณ ์ •๊ฐ’(stuck) ํ‘œ์‹œ -> ๋‹จ๋ฐœ ์ŠคํŒŒ์ดํฌ ์ œ๊ฑฐ -> ๊ฒฐ์ธก ๋ณด์ • ----\n", "PHYS = {\"ph\": (0, 14), \"turbidity\": (0, 1000), \"chlorine\": (0, 10), \"metal\": (0, 1000), \"ecoli\": (0, 100000)}\n", "SPIKE_K = {\"day\": 6.0, \"night\": 7.0, \"storm\": 9.0} # ์ƒํ™ฉ๋ณ„(์ฃผ๊ฐ„/์•ผ๊ฐ„/์•…์ฒœํ›„) ์ŠคํŒŒ์ดํฌ ํŒ์ • ๋ฐฐ์ˆ˜\n", "MIN_JUMP = {\"ph\": 0.8, \"turbidity\": 1.5, \"chlorine\": 0.3, \"metal\": 6.0}\n", "STUCK_RUN, SHORT_GAP = 6, 3\n", "\n", "\n", "def clean(df, spike_k=None, min_jump=None):\n", " \"\"\"๊ธ‰์ˆ˜์›๋ณ„ ์‹œ๊ณ„์—ด์„ ์ •์ œํ•˜๊ณ  ํ•ญ๋ชฉ๋ณ„ ์ฒ˜๋ฆฌ ๋‚ด์—ญ(<ํ•ญ๋ชฉ>_flag: range/stuck/spike/interp/long_gap)์„ ๋‚จ๊ธด๋‹ค.\"\"\"\n", " spike_k = spike_k or SPIKE_K\n", " min_jump = MIN_JUMP if min_jump is None else min_jump\n", " out = df.sort_values([\"source_id\", \"t\"]).reset_index(drop=True)\n", " for sid, idx in out.groupby(\"source_id\").groups.items():\n", " idx = np.asarray(idx)\n", " kk = out.loc[idx, \"cond\"].map(spike_k).to_numpy(float)\n", " for p in PARAMS:\n", " v = out.loc[idx, p].to_numpy(float).copy()\n", " fl = np.array([\"\"] * len(v), dtype=object)\n", " lo, hi = PHYS[p]\n", " bad = (v < lo) | (v > hi)\n", " v[bad], fl[bad] = np.nan, \"range\"\n", " if p != \"ecoli\":\n", " chg = np.r_[True, v[1:] != v[:-1]] # ๊ฐ™์€ ๊ฐ’์ด STUCK_RUN์‹œ๊ฐ„ ์ด์ƒ ๋ฐ˜๋ณต = ๊ณ ์ •๊ฐ’ ๊ณ ์žฅ ์˜์‹ฌ\n", " rid = np.cumsum(chg)\n", " rlen = np.bincount(rid)[rid]\n", " fl[(rlen >= STUCK_RUN) & ~chg & ~np.isnan(v)] = \"stuck\"\n", " d = np.abs(np.diff(v[~np.isnan(v)]))\n", " sig = 1.4826 * np.median(d) + 1e-6 if len(d) else 1.0\n", " up, dn = v - np.r_[np.nan, v[:-1]], v - np.r_[v[1:], np.nan]\n", " thr = np.maximum(kk * sig, min_jump[p])\n", " spike = (np.abs(up) > thr) & (np.abs(dn) > thr) & (np.sign(up) == np.sign(dn)) # ํŠ€์—ˆ๋‹ค๊ฐ€ ๋ฐ”๋กœ ๋Œ์•„์˜จ ๊ฐ’๋งŒ\n", " v[spike], fl[spike] = np.nan, \"spike\"\n", " s = pd.Series(v)\n", " na = s.isna().to_numpy()\n", " grp = np.cumsum(np.r_[True, na[1:] != na[:-1]])\n", " glen = pd.Series(na.astype(int)).groupby(grp).transform(\"sum\").to_numpy()\n", " lin, ff = s.interpolate(limit_area=\"inside\").to_numpy(), s.ffill().bfill().to_numpy()\n", " short, longg = na & (glen <= SHORT_GAP), na & (glen > SHORT_GAP)\n", " v = np.where(short, np.where(np.isnan(lin), ff, lin), np.where(longg, ff, v))\n", " fl[short & (fl == \"\")] = \"interp\"\n", " fl[longg] = \"long_gap\"\n", " out.loc[idx, p] = v\n", " out.loc[idx, f\"{p}_flag\"] = fl\n", " return out\n", "\n", "# ---- ๊ฐ€์ • ๊ธฐ์ค€์น˜ ํŒ์ •(Baseline)๊ณผ ์‹œ๊ณ„์—ด ํŠน์ง• ๊ณตํ†ต ํ•จ์ˆ˜ ----\n", "GRADE_NAME = {0: \"์•ˆ์ „\", 1: \"์ฃผ์˜\", 2: \"์œ„ํ—˜\"}\n", "\n", "\n", "def rule_grade(df):\n", " \"\"\"๊ธฐ์ค€์น˜ 1์ฐจ ํ•„ํ„ฐ: ์œ„ํ—˜(์Œ์šฉ ๋ถ€์ ํ•ฉ ์˜์‹ฌ) = ํ•œ ํ•ญ๋ชฉ์ด๋ผ๋„ ์œ„ํ—˜ ๊ธฐ์ค€ ์ดˆ๊ณผ, ์ฃผ์˜ = ์ฃผ์˜ ๊ธฐ์ค€ ์ดˆ๊ณผ.\"\"\"\n", " ph, tb, cl, mt, ec = (df[p].to_numpy(float) for p in PARAMS)\n", " danger = (ph < LIMITS[\"ph\"][0]) | (ph > LIMITS[\"ph\"][1]) | (tb > LIMITS[\"turbidity\"][1]) | (cl < LIMITS[\"chlorine\"][1]) \\\n", " | (mt > LIMITS[\"metal\"][1]) | (ec >= LIMITS[\"ecoli\"][1])\n", " caution = (tb > LIMITS[\"turbidity\"][0]) | (cl < LIMITS[\"chlorine\"][0]) | (mt > LIMITS[\"metal\"][0])\n", " return np.where(danger, 2, np.where(caution, 1, 0))\n", "\n", "\n", "def reasons(row):\n", " r = []\n", " if not LIMITS[\"ph\"][0] <= row[\"ph\"] <= LIMITS[\"ph\"][1]:\n", " r.append(f\"pH {row['ph']:.2f}\")\n", " if row[\"turbidity\"] > LIMITS[\"turbidity\"][0]:\n", " r.append(f\"ํƒ๋„ {row['turbidity']:.2f}NTU\")\n", " if row[\"chlorine\"] < LIMITS[\"chlorine\"][0]:\n", " r.append(f\"์ž”๋ฅ˜์—ผ์†Œ {row['chlorine']:.2f}mg/L\")\n", " if row[\"metal\"] > LIMITS[\"metal\"][0]:\n", " r.append(f\"์ค‘๊ธˆ์† {row['metal']:.1f}ug/L\")\n", " if row[\"ecoli\"] >= 1:\n", " r.append(f\"๋Œ€์žฅ๊ท  {row['ecoli']:.0f}\")\n", " return \", \".join(r) or \"-\"\n", "\n", "\n", "FEATS = [f\"{p}{s}\" for p in [\"ph\", \"turbidity\", \"chlorine\", \"metal\"] for s in [\"\", \"_d1\", \"_d3\", \"_dev\", \"_sd6\"]] + \\\n", " [\"ecoli\", \"ecoli_3h\", \"storm\", \"night\", \"n_flag\", \"truck\", \"purif\", \"well\", \"river\"]\n", "\n", "\n", "def add_features(c):\n", " \"\"\"์‹œ๊ฐ„ ์ˆœ์„œ๋ฅผ ์ง€ํ‚ค๋Š”(๊ณผ๊ฑฐ ๊ฐ’๋งŒ ์“ฐ๋Š”) ๊ธ‰์ˆ˜์›๋ณ„ ํŠน์ง•: ๋ณ€ํ™”๋Ÿ‰, 24์‹œ๊ฐ„ ๊ธฐ์ค€์„  ๋Œ€๋น„ ํŽธ์ฐจ, 6์‹œ๊ฐ„ ๋ณ€๋™์„ฑ, ๋Œ€์žฅ๊ท  ๋ˆ„์  ๋“ฑ.\"\"\"\n", " c = c.sort_values([\"source_id\", \"t\"]).reset_index(drop=True)\n", " g = c.groupby(\"source_id\")\n", " for p in [\"ph\", \"turbidity\", \"chlorine\", \"metal\"]:\n", " c[f\"{p}_d1\"] = g[p].diff().fillna(0)\n", " c[f\"{p}_d3\"] = g[p].diff(3).fillna(0)\n", " base = g[p].transform(lambda s: s.shift(1).rolling(24, min_periods=4).median())\n", " c[f\"{p}_dev\"] = (c[p] - base).fillna(0)\n", " c[f\"{p}_sd6\"] = g[p].transform(lambda s: s.rolling(6, min_periods=2).std()).fillna(0)\n", " c[\"ecoli_3h\"] = g[\"ecoli\"].transform(lambda s: s.rolling(3, min_periods=1).sum())\n", " c[\"storm\"], c[\"night\"] = (c[\"cond\"] == \"storm\").astype(int), (c[\"cond\"] == \"night\").astype(int)\n", " flags = [f\"{p}_flag\" for p in PARAMS if f\"{p}_flag\" in c]\n", " c[\"n_flag\"] = sum(c[f].isin([\"stuck\", \"spike\", \"long_gap\", \"range\"]).astype(int) for f in flags) if flags else 0\n", " for k, name in [(\"truck\", \"water_truck\"), (\"purif\", \"purification_unit\"), (\"well\", \"well\"), (\"river\", \"river_intake\")]:\n", " c[k] = (c[\"source_type\"] == name).astype(int)\n", " return c\n", "\n", "DARK = True # ์•ผ๊ฐ„ ์ž‘์ „ ๋ชจ๋“œ(์–ด๋‘์šด ๋ฐฐ๊ฒฝ)\n", "NOW = 290 # ์ƒํ™ฉํŒ ๊ธฐ์ค€ ์‹œ๊ฐ(๋ชจ์˜ ์‹œ์ž‘ ํ›„ ๊ฒฝ๊ณผ ์‹œ๊ฐ„ h)\n", "COLOR = {0: \"tab:green\", 1: \"gold\", 2: \"red\"}\n", "\n", "data = make_dataset(days=14, seed=11, n_events=10, n_faults=14)\n", "data = data[data[\"t\"] <= NOW]\n", "\n", "\n", "def ratio(row):\n", " \"\"\"ํ•ญ๋ชฉ๋ณ„ '์œ„ํ—˜ ๊ธฐ์ค€ ๋Œ€๋น„ ๋น„์œจ'(1 ์ด์ƒ์ด๋ฉด ์œ„ํ—˜). ์ž”๋ฅ˜์—ผ์†ŒยทpH๋Š” ๋ฐฉํ–ฅ์„ ๋งž์ถฐ ํ™˜์‚ฐํ•œ๋‹ค.\"\"\"\n", " lo, hi = LIMITS[\"ph\"]\n", " return {\"pH\": max((row[\"ph\"] - 7.0) / (hi - 7.0), (7.0 - row[\"ph\"]) / (7.0 - lo)),\n", " \"turb\": row[\"turbidity\"] / LIMITS[\"turbidity\"][1], \"Cl\": LIMITS[\"chlorine\"][1] / max(row[\"chlorine\"], 0.01),\n", " \"metal\": row[\"metal\"] / LIMITS[\"metal\"][1], \"E.coli\": row[\"ecoli\"] / LIMITS[\"ecoli\"][1]}\n", "\n", "\n", "# 1) ์‹ค์‹œ๊ฐ„ ๊ฐฑ์‹  ๋ชจ์˜: ์ตœ๊ทผ 24์‹œ๊ฐ„ ์ฐฝ์„ ์œ ์ง€ํ•˜๋ฉฐ 1์‹œ๊ฐ„์”ฉ ์ƒˆ ์ธก์ •๊ฐ’์ด ๋“ค์–ด์˜ฌ ๋•Œ ์ƒํƒœ ๊ณ„์‚ฐ ์‹œ๊ฐ„\n", "window = data[data[\"t\"] <= NOW - 24].copy()\n", "upd = []\n", "for h in range(NOW - 23, NOW + 1):\n", " t0 = time.perf_counter()\n", " window = pd.concat([window[window[\"t\"] > h - 48], data[data[\"t\"] == h]])\n", " c = clean(window)\n", " c[\"g\"] = rule_grade(c)\n", " latest = c[c[\"t\"] == h]\n", " upd.append((time.perf_counter() - t0) * 1000)\n", "print(f\"[๊ต์œก์šฉ๊ฐ€์ƒ์ž๋ฃŒ] ์‹ค์‹œ๊ฐ„ ๊ฐฑ์‹  ๋ชจ์˜ 24ํšŒ: 1ํšŒ ํ‰๊ท  {np.mean(upd):.1f}ms, ์ตœ๋Œ€ {np.max(upd):.1f}ms \"\n", " f\"(๊ฐฑ์‹  ์ฃผ๊ธฐ 1์ดˆ ์ด๋‚ด: {'๋‹ฌ์„ฑ' if max(upd) < 1000 else '๋ฏธ๋‹ฌ'})\")\n", "\n", "# 2) ํ˜„์žฌ ์ƒํƒœํ‘œ\n", "state = latest.set_index(\"source_id\")\n", "hist = c.groupby(\"source_id\")[\"g\"].apply(lambda s: int((s.tail(6) == 2).sum()))\n", "board = pd.DataFrame({\"์œ ํ˜•\": state[\"source_type\"], \"์ƒํƒœ\": state[\"g\"].map(GRADE_NAME), \"์ตœ๊ทผ6h ์œ„ํ—˜\": hist,\n", " \"๊ทผ๊ฑฐ\": [reasons(r) for _, r in state.iterrows()]})\n", "print(f\"\\n[D+{NOW // 24:02d} {NOW % 24:02d}์‹œ ๊ธ‰์ˆ˜์› ์ƒํƒœํŒ]\")\n", "print(board.to_string())\n", "alerts = board[(board[\"์ƒํƒœ\"] != \"์•ˆ์ „\") | (board[\"์ตœ๊ทผ6h ์œ„ํ—˜\"] > 0)]\n", "print(f\"๊ธด๊ธ‰ ๊ฒฝ๋ณด ๊ฐ•์กฐ ๋Œ€์ƒ {len(alerts)}๊ณณ: {', '.join(alerts.index)}\")\n", "\n", "# 3) ์ƒํ™ฉํŒ ๊ทธ๋ฆฌ๊ธฐ(์ปดํฌ๋„ŒํŠธ 5์ข…) + ๋ Œ๋”๋ง ์‹œ๊ฐ„\n", "plt.style.use(\"dark_background\" if DARK else \"default\")\n", "t0 = time.perf_counter()\n", "fig = plt.figure(figsize=(13, 7.5))\n", "gs = fig.add_gridspec(2, 3)\n", "ax1 = fig.add_subplot(gs[0, 0])\n", "for sid, r in state.iterrows():\n", " ax1.scatter(r[\"x_km\"], r[\"y_km\"], s=260, color=COLOR[r[\"g\"]], edgecolor=\"white\" if r[\"g\"] == 2 else \"none\", lw=2)\n", " ax1.annotate(sid, (r[\"x_km\"], r[\"y_km\"]), xytext=(6, 6), textcoords=\"offset points\", fontsize=8)\n", "ax1.set_title(\"Water source status map (virtual grid km)\"); ax1.set_xlim(0, 7.5); ax1.set_ylim(0, 7)\n", "ax2 = fig.add_subplot(gs[0, 1])\n", "R = pd.DataFrame([ratio(r) for _, r in state.iterrows()], index=state.index)\n", "im = ax2.imshow(R.clip(0, 2).to_numpy(), cmap=\"RdYlGn_r\", vmin=0, vmax=2, aspect=\"auto\")\n", "ax2.set_xticks(range(R.shape[1]), R.columns); ax2.set_yticks(range(len(R)), R.index)\n", "ax2.set_title(\"Ratio to danger limit (>=1 danger)\"); fig.colorbar(im, ax=ax2, fraction=0.046)\n", "ax3 = fig.add_subplot(gs[0, 2])\n", "cnt = state[\"g\"].value_counts().reindex([0, 1, 2], fill_value=0)\n", "ax3.bar([\"safe\", \"caution\", \"danger\"], cnt.values, color=[COLOR[i] for i in range(3)])\n", "ax3.set_title(f\"Status count at hour {NOW}\")\n", "ax4 = fig.add_subplot(gs[1, :2])\n", "focus = alerts.index[0] if len(alerts) else state.index[0]\n", "s = c[c[\"source_id\"] == focus]\n", "for p, col in [(\"turbidity\", \"tab:cyan\"), (\"chlorine\", \"tab:orange\"), (\"metal\", \"violet\")]:\n", " ax4.plot(s[\"t\"], s[p] / LIMITS[p][1] if p != \"chlorine\" else LIMITS[p][1] / s[p].clip(lower=0.01), label=p, color=col)\n", "ax4.axhline(1, color=\"red\", ls=\"--\", lw=1); ax4.set_ylim(0, 3); ax4.legend(loc=\"upper left\")\n", "ax4.set_title(f\"{focus} last 48h ratio to danger limit\"); ax4.set_xlabel(\"hour\")\n", "ax5 = fig.add_subplot(gs[1, 2]); ax5.axis(\"off\")\n", "lines = [f\"{sid}: {('DANGER' if r['์ƒํƒœ'] == '์œ„ํ—˜' else 'CHECK')} 6h danger {r['์ตœ๊ทผ6h ์œ„ํ—˜']}\" for sid, r in alerts.iterrows()]\n", "ax5.text(0, 1, \"ALERT LIST\\n\" + \"\\n\".join(lines or [\"no alert\"]), va=\"top\", family=\"monospace\", fontsize=10,\n", " color=\"red\" if len(alerts) else \"white\" if DARK else \"black\")\n", "fig.suptitle(\"Field water quality risk board (synthetic, decision support only)\")\n", "fig.tight_layout()\n", "fig.canvas.draw()\n", "draw_ms = (time.perf_counter() - t0) * 1000\n", "\n", "# 4) UI ์‘๋‹ต์„ฑ: ๊ธ‰์ˆ˜์› ํด๋ฆญ(์„ ํƒ) ์‹œ ์ƒ์„ธ ์กฐํšŒ ์‘๋‹ต ์‹œ๊ฐ„\n", "resp = []\n", "for sid in state.index:\n", " t0 = time.perf_counter()\n", " d = c[c[\"source_id\"] == sid].tail(6)\n", " _ = (d[[\"ph\", \"turbidity\", \"chlorine\", \"metal\", \"ecoli\"]].agg([\"min\", \"max\"]), reasons(d.iloc[-1]))\n", " resp.append((time.perf_counter() - t0) * 1000)\n", "print(f\"\\n์‹œ๊ฐํ™” ์ปดํฌ๋„ŒํŠธ 5์ข… ๋ Œ๋”๋ง {draw_ms:.0f}ms, ๊ธ‰์ˆ˜์› ์ƒ์„ธ ์กฐํšŒ ์‘๋‹ต ํ‰๊ท  {np.mean(resp):.1f}ms \"\n", " f\"(UI ์‘๋‹ต์„ฑ 100ms ์ด๋‚ด: {'๋‹ฌ์„ฑ' if max(resp) < 100 else '๋ฏธ๋‹ฌ'})\")\n", "print(\"โ€ป ์ƒํ™ฉํŒ์˜ ์œ„ํ—˜ ํ‘œ์‹œ๋Š” ๊ฐ€์ • ๊ธฐ์ค€์น˜ ๊ธฐ๋ฐ˜ ํŒ๋‹จ ๋ณด์กฐ์ด๋ฉฐ, ๊ธ‰์ˆ˜ ์ค‘๋‹จ ์—ฌ๋ถ€๋Š” ์˜๋ฌด(์˜ˆ๋ฐฉ์˜ํ•™)ยทํ™˜๊ฒฝ ๋‹ด๋‹น ํ™•์ธ ํ›„ ์ง€ํœ˜๊ด€์ด ๊ฒฐ์ •ํ•œ๋‹ค.\")\n", "plt.show()\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "
\n", "๐Ÿ’ก [์ฐธ๊ณ ] ๋ฏธ์…˜ 06 ์˜ˆ์ƒ ์‹คํ–‰ ๊ฒฐ๊ณผ (Output)\n", "\n", "```text\n", "[๊ต์œก์šฉ๊ฐ€์ƒ์ž๋ฃŒ] ์‹ค์‹œ๊ฐ„ ๊ฐฑ์‹  ๋ชจ์˜ 24ํšŒ: 1ํšŒ ํ‰๊ท  158.2ms, ์ตœ๋Œ€ 205.7ms (๊ฐฑ์‹  ์ฃผ๊ธฐ 1์ดˆ ์ด๋‚ด: ๋‹ฌ์„ฑ)\n", "\n", "[D+12 02์‹œ ๊ธ‰์ˆ˜์› ์ƒํƒœํŒ]\n", " ์œ ํ˜• ์ƒํƒœ ์ตœ๊ทผ6h ์œ„ํ—˜ ๊ทผ๊ฑฐ\n", "source_id \n", "W01 water_truck ์•ˆ์ „ 0 -\n", "W02 water_truck ์•ˆ์ „ 0 -\n", "W03 purification_unit ์•ˆ์ „ 0 -\n", "W04 purification_unit ์œ„ํ—˜ 3 pH 8.72, ํƒ๋„ 1.31NTU\n", "W05 well ์•ˆ์ „ 0 -\n", "W06 well ์•ˆ์ „ 0 -\n", "W07 river_intake ์ฃผ์˜ 0 ์ค‘๊ธˆ์† 8.7ug/L\n", "W08 river_intake ์œ„ํ—˜ 6 ์ค‘๊ธˆ์† 24.1ug/L\n", "๊ธด๊ธ‰ ๊ฒฝ๋ณด ๊ฐ•์กฐ ๋Œ€์ƒ 3๊ณณ: W04, W07, W08\n", "\n", "์‹œ๊ฐํ™” ์ปดํฌ๋„ŒํŠธ 5์ข… ๋ Œ๋”๋ง 581ms, ๊ธ‰์ˆ˜์› ์ƒ์„ธ ์กฐํšŒ ์‘๋‹ต ํ‰๊ท  5.7ms (UI ์‘๋‹ต์„ฑ 100ms ์ด๋‚ด: ๋‹ฌ์„ฑ)\n", "โ€ป ์ƒํ™ฉํŒ์˜ ์œ„ํ—˜ ํ‘œ์‹œ๋Š” ๊ฐ€์ • ๊ธฐ์ค€์น˜ ๊ธฐ๋ฐ˜ ํŒ๋‹จ ๋ณด์กฐ์ด๋ฉฐ, ๊ธ‰์ˆ˜ ์ค‘๋‹จ ์—ฌ๋ถ€๋Š” ์˜๋ฌด(์˜ˆ๋ฐฉ์˜ํ•™)ยทํ™˜๊ฒฝ ๋‹ด๋‹น ํ™•์ธ ํ›„ ์ง€ํœ˜๊ด€์ด ๊ฒฐ์ •ํ•œ๋‹ค.\n", "```\n", "
\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "---\n", "\n", "## ๐ŸŽฏ ๋ฏธ์…˜ 07. ์ˆ˜์งˆ ์œ„ํ—˜ ๊ฒฝ๋ณด ๋ฐœ์ƒ ์‹œ ๊ธ‰์ˆ˜๊ด€ ๊ธด๊ธ‰ ์ฑ„์ˆ˜ ๊ฒ€์‚ฌ ๋ฐ ๊ธ‰์ˆ˜ ์ค‘๋‹จ HITL ๊ฒฐ์žฌ์„  ์„ค๊ณ„\n", "**์†Œ์š”์‹œ๊ฐ„**: ์•ฝ 3.0์‹œ๊ฐ„\n", "\n", "### ๐Ÿ“‹ ๋ฏธ์…˜ ๋ฐฐ๊ฒฝ ๋ฐ ์ˆ˜ํ–‰ ์ง€์นจ\n", "#### ๋ฏธ์…˜ ๋ฐฐ๊ฒฝ ๋ฐ ๋‹น์œ„์„ฑ\n", "\n", "์ˆ˜์งˆ ์œ„ํ—˜ ๊ฒฝ๋ณด ๋ฐœ์ƒ ์‹œ ๊ธ‰์ˆ˜ ์ค‘๋‹จ ๋ฐ ์žฌ๊ฒ€์‚ฌ HITL ๊ฒฐ์žฌ์„ ์„ ์„ค๊ณ„ํ•œ๋‹ค. ์‹ค์ œ ์ „์ˆ  ํ˜„์žฅ์—์„œ๋Š” ๋‹จ์ˆœ ํƒ์ง€์— ๊ทธ์น˜์ง€ ์•Š๊ณ  ์‹ ์†ํ•œ ์œ„ํ˜‘ ํŒ๋‹จ๊ณผ ์‹ ๋ขฐ๋„ ๋†’์€ ์˜์‚ฌ๊ฒฐ์ •์ด ํ•„์ˆ˜์ ์ด๋ฏ€๋กœ, **์ˆ˜์งˆ ์œ„ํ—˜ ๊ฒฝ๋ณด ๋ฐœ์ƒ ์‹œ ๊ธด๊ธ‰ ์ฑ„์ˆ˜ ๊ฒ€์‚ฌ์™€ ๊ธ‰์ˆ˜ ์ค‘๋‹จ์„ ์‚ฌ๋žŒ์ด ๊ฒฐ์ •ํ•˜๋Š” HITL ๊ฒฐ์žฌ์„ ์„ ์„ค๊ณ„ํ•œ๋‹ค.** ๊ณผ์—…์„ ์™„์ˆ˜ํ•˜์—ฌ ์˜คํŒ์„ ๋ฐฉ์ง€ํ•˜๊ณ  ์ž‘์ „ ๋Œ€์‘๋ ฅ์„ ๊ทน๋Œ€ํ™”ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.\n", "\n", "์ด ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด ๋ณธ ๋ฏธ์…˜์—์„œ๋Š” **๊ฒฝ๋ณด ์‚ฌ๊ฑด์„ ๊ฒ€ํ†  ํ์— ์šฐ์„ ์ˆœ์œ„๋กœ ๋“ฑ๋กํ•˜๊ณ  ์ธก์ •๋ณ‘ ์ฑ„์ˆ˜, ์˜ˆ๋ฐฉ์˜ํ•™ ๋‹ด๋‹น ๊ฒ€ํ† , ์ง€ํœ˜๊ด€ ๊ฒฐ์‹ฌ์˜ ์ˆœ์„œ๋กœ ๊ธ‰์ˆ˜ ์ค‘๋‹จ ๋˜๋Š” ์œ ์ง€๋ฅผ ๊ฒฐ์ •ํ•˜๋Š” ์ƒํƒœ ๊ธฐ๊ณ„ ๊ตฌํ˜„ํ•˜๊ณ  ๋™์ž‘์„ ๊ฒ€์ฆํ•˜๋Š”** ์‹ค์Šต์„ ์ฒด๊ณ„์ ์œผ๋กœ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค.\n", "\n", "ํ•™์Šต์ž๋Š” ์‹ค์Šต๊ฐ€์ด๋“œ์— ์ œ์‹œ๋œ ์ด 4๊ฐœ ์„ธ๋ถ€ ํ™œ๋™(๊ธฐ๋ณธ ๊ธฐ๋Šฅ ๊ตฌํ˜„ โž” ๊ฒฐ๊ณผ ์ง€ํ‘œ ๊ฒ€์ฆ โž” ํŒŒ๋ผ๋ฏธํ„ฐ ํŠœ๋‹ ๋ฐ ๋„์ „๊ณผ์ œ)์„ ์ˆœ์ฐจ์ ์œผ๋กœ ์ง„ํ–‰ํ•˜๋ฉฐ ํ•ต์‹ฌ ํŒ์ • ๊ธฐ์ค€์„ ๋‹ฌ์„ฑํ•ฉ๋‹ˆ๋‹ค.\n", "\n", "**์ด๋ฒˆ ๊ฒฐ๊ณผ๋ฌผ:** `src/hitl/wel03_approval_workflow.py ๋ฐ docs/wel03_hitl_guide.md` ํ˜•ํƒœ์˜ ๊ฒฐ๊ณผ๋ฌผ์„ ์™„์„ฑํ•œ๋‹ค.\n", "\n", "### ๐Ÿ” ํ‰๊ฐ€ ๋ฐ ๊ฒ€์ฆ ๊ธฐ์ค€ (PASS ์กฐ๊ฑด)\n", "#### ๋ฏธ์…˜ ํŒ์ •\n", "\n", "**ํ‰๊ฐ€ ๊ตฌ๋ถ„:** ํ•„์ˆ˜\n", "\n", "**ํ•ต์‹ฌ ํ‰๊ฐ€์˜์—ญ:** ์ง€ํœ˜ ์ฑ…์ž„ ๋ถ„๊ณ„์„  ๋ช…ํ™•ํ™” ๋ฐ ์„ฑ๊ธ‰ํ•œ ์ž๋™ ๋‹จ์ • ๋ฐฉ์ง€ ์ฒด๊ณ„ ๋ถ€ํ•ฉ์„ฑ\n", "\n", "#### ๋ฏธ์…˜ PASS ์กฐ๊ฑด\n", "\n", "- ์ •๋Ÿ‰ ๊ธฐ์ค€: ์ธ๊ฐ„ ๊ฒ€ํ†  ํ(Queue) ๋Œ€๊ธฐ์‹œ๊ฐ„ ๋ฐ ์Šน์ธ/๋ฐ˜๋ ค ์ฒ˜๋ฆฌ ๋กœ๊น… ์„ฑ๊ณต๋ฅ  100%\n", "\n", "\n", "- ์ง„ํ–‰ ๊ธฐ์ค€: ์Šน์ธ/๋ฐ˜๋ ค ์ธํ„ฐํŽ˜์ด์Šค ๋™์ž‘ ๋ฐ ๊ฐ์‚ฌ ๋กœ๊ทธ(Audit Log) ์ƒ์„ฑ ๊ฒ€์ฆ\n", "\n", "\n", "- ๊ฒ€์ฆ ๋ฐฉ๋ฒ•: ๋ชจ์˜ ์กฐ์ž‘์ž ์Šน์ธ/๋ฐ˜๋ ค ์‹œ๋ฎฌ๋ ˆ์ด์…˜ ๋ฐ ๋ฐ์ดํ„ฐ ๋ฌด๊ฒฐ์„ฑ ๊ฒ€์ฆ\n", "\n", "\n", "#### ์ฑ„์ ์ž ํŒ์ • ๋ฉ”๋ชจ\n", "\n", "> ๋‹จ์ˆœ ์ฝ”๋“œ ์‹คํ–‰ ์—ฌ๋ถ€๋ฟ๋งŒ ์•„๋‹ˆ๋ผ ์ถœ๋ ฅ๋œ ์ˆ˜์น˜์™€ ๊ทผ๊ฑฐ ์ž๋ฃŒ๊ฐ€ ๊ตฐ ์‹ค๋ฌด ์š”๊ตฌ์‚ฌํ•ญ์— ๋ถ€ํ•ฉํ•˜๋Š”์ง€ ๋ฉด๋ฐ€ํžˆ ํ™•์ธํ•œ๋‹ค.\n", "\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "\"\"\"์ˆ˜์งˆ ์œ„ํ—˜ ๊ฒฝ๋ณด๊ฐ€ ๋‚˜๋ฉด ์‹œ์Šคํ…œ์€ ๊ธ‰์ˆ˜ ์ค‘๋‹จ์„ ์ž๋™ ํ™•์ •ํ•˜์ง€ ์•Š๊ณ  ์šฐ์„ ์ˆœ์œ„ ๊ฒ€ํ†  ํ์— ์˜ฌ๋ฆฌ๋ฉฐ, ์ธก์ • ๋‹ด๋‹น ๊ธด๊ธ‰ ์ฑ„์ˆ˜ โ†’ ์˜ˆ๋ฐฉ์˜ํ•™ ๋‹ด๋‹น ๊ฒ€ํ†  โ†’ ์ง€ํœ˜๊ด€ ๊ธ‰์ˆ˜ ์ค‘๋‹จ ์Šน์ธยท๋ฐ˜๋ ค(๋ฐ˜๋ ค ์‚ฌ์œ  ํ•„์ˆ˜) ๊ฒฐ์žฌ์„ ๊ณผ ๊ถŒํ•œ ํ†ต์ œ, ํ•ด์‹œ ์ฒด์ธ ๊ฐ์‚ฌ ๋กœ๊ทธ, ํ ๋Œ€๊ธฐ์‹œ๊ฐ„์„ ์‹œ๋ฎฌ๋ ˆ์ด์…˜ํ•œ๋‹ค.\"\"\"\n", "import hashlib\n", "import json\n", "import numpy as np\n", "import pandas as pd\n", "\n", "# ---------------------------------------------------------------------------\n", "# [๊ต์œก์šฉ๊ฐ€์ƒ์ž๋ฃŒ] ์•ผ์ „ ๊ธ‰์ˆ˜์› ๊ฐ€์ƒ ์ˆ˜์งˆ ์„ผ์„œ ๋ฐ์ดํ„ฐ ์ƒ์„ฑ๊ธฐ (์‹ค์ œ ๋ถ€๋Œ€ยท์œ„์น˜ยท๊ฒ€์‚ฌ ๊ฒฐ๊ณผ ์—†์Œ)\n", "# ๊ฐ€์ƒ ๊ธ‰์ˆ˜์› 8๊ณณ x 14์ผ x 1์‹œ๊ฐ„ ๊ฐ„๊ฒฉ. ์‹ค์ œ ์ˆ˜์งˆ(true_*)ยท์„ผ์„œ ์ธก์ •๊ฐ’ยท์˜ค์—ผ ์‚ฌ๊ฑดยท์„ผ์„œ ๊ณ ์žฅ ์ •๋‹ต์„ ํ•จ๊ป˜ ๋งŒ๋“ ๋‹ค.\n", "# ๊ธฐ์ค€์น˜๋Š” ๊ต์œก์šฉ ๊ฐ€์ •๊ฐ’์ด๋ฉฐ, ์‹ค์ œ ์Œ์šฉ ํŒ์ •์€ ๊ณต์ธ ๊ฒ€์‚ฌ ๊ฒฐ๊ณผ์™€ ์˜๋ฌด(์˜ˆ๋ฐฉ์˜ํ•™) ๋‹ด๋‹น ๊ธฐ์ค€์— ๋”ฐ๋ฅธ๋‹ค.\n", "# ---------------------------------------------------------------------------\n", "PARAMS = [\"ph\", \"turbidity\", \"chlorine\", \"metal\", \"ecoli\"]\n", "SOURCES = [(\"W01\", \"water_truck\", 1.0, 2.0), (\"W02\", \"water_truck\", 3.5, 1.2), (\"W03\", \"purification_unit\", 2.2, 4.1),\n", " (\"W04\", \"purification_unit\", 5.0, 3.3), (\"W05\", \"well\", 0.8, 5.6), (\"W06\", \"well\", 4.4, 6.0),\n", " (\"W07\", \"river_intake\", 6.3, 1.8), (\"W08\", \"river_intake\", 6.9, 4.9)]\n", "BASE = {\"water_truck\": (7.2, 0.25, 0.75, 1.5), \"purification_unit\": (7.0, 0.20, 0.85, 1.2),\n", " \"well\": (6.8, 0.45, 0.50, 3.0), \"river_intake\": (7.3, 0.55, 0.60, 2.0)} # pH, ํƒ๋„(NTU), ์ž”๋ฅ˜์—ผ์†Œ(mg/L), ์ค‘๊ธˆ์†(ug/L)\n", "LIMITS = {\"ph\": (5.8, 8.5), \"turbidity\": (1.0, 2.0), \"chlorine\": (0.2, 0.1), \"metal\": (5.0, 10.0), \"ecoli\": (1, 1)} # (์ฃผ์˜, ์œ„ํ—˜) ๊ฐ€์ • ๊ธฐ์ค€\n", "EVENT_TYPES = { # ์˜ค์—ผ ์‚ฌ๊ฑด: ๋ณ€์ˆ˜๋ณ„ ์ตœ๋Œ€ ๋ณ€ํ™”๋Ÿ‰, ์ตœ๋Œ€์น˜ ๋„๋‹ฌ ์‹œ๊ฐ„(h)\n", " \"sewage\": ({\"turbidity\": 2.8, \"chlorine\": -0.5, \"ecoli\": 30, \"ph\": -0.3}, 4),\n", " \"metal\": ({\"metal\": 22.0, \"ph\": -0.5}, 4),\n", " \"disinfect_fail\": ({\"chlorine\": -0.8, \"ecoli\": 6}, 6),\n", " \"chemical\": ({\"ph\": 1.7, \"turbidity\": 1.0}, 3),\n", " \"creeping_metal\": ({\"metal\": 13.0}, 18),\n", "}\n", "FAULT_TYPES = [\"stuck\", \"spike\", \"drift\", \"dropout\"]\n", "NOISE = {\"ph\": 0.04, \"turbidity\": 0.06, \"chlorine\": 0.03, \"metal\": 0.35}\n", "DIGITS = {\"ph\": 2, \"turbidity\": 2, \"chlorine\": 2, \"metal\": 1}\n", "\n", "\n", "def make_dataset(days=14, seed=7, n_events=6, n_faults=10, events=None, faults=None, sources=None):\n", " \"\"\"events: [(๊ธ‰์ˆ˜์› ๋ฒˆํ˜ธ, ์‚ฌ๊ฑด ์œ ํ˜•, ์‹œ์ž‘ ์‹œ๊ฐ h, ์ง€์† h, ๊ฐ•๋„)], faults: [(๊ธ‰์ˆ˜์› ๋ฒˆํ˜ธ, ๊ณ ์žฅ ์œ ํ˜•, ํ•ญ๋ชฉ, ์‹œ์ž‘ h, ์ง€์† h)]\"\"\"\n", " rng = np.random.default_rng(seed)\n", " sources = sources or SOURCES\n", " T = days * 24\n", " tt = np.arange(T)\n", " hour = tt % 24\n", " night = (hour < 6) | (hour >= 20)\n", " rain = np.zeros(T)\n", " for _ in range(max(1, days // 4)):\n", " s0, d = int(rng.integers(0, T - 14)), int(rng.integers(6, 14))\n", " rain[s0:s0 + d] = np.maximum(rain[s0:s0 + d], rng.uniform(0.6, 1.0))\n", " if events is None:\n", " events = [(int(rng.integers(len(sources))), str(rng.choice(list(EVENT_TYPES))), int(rng.integers(min(24, T // 4), max(T // 4 + 1, T - 48))),\n", " int(rng.integers(10, 30)), 1.0) for _ in range(n_events)]\n", " if faults is None:\n", " faults = [(int(rng.integers(len(sources))), str(rng.choice(FAULT_TYPES)), str(rng.choice([\"ph\", \"turbidity\", \"chlorine\", \"metal\"])),\n", " int(rng.integers(12, max(13, T - 30))), int(rng.integers(6, 24))) for _ in range(n_faults)]\n", " out = []\n", " for si, (sid, stype, x, y) in enumerate(sources):\n", " ph0, tb0, cl0, mt0 = BASE[stype]\n", " tr = {\"ph\": ph0 + 0.08 * np.sin(2 * np.pi * hour / 24) + rng.normal(0, 0.03, T),\n", " \"turbidity\": tb0 * (1 + 0.1 * rng.normal(0, 1, T)),\n", " \"chlorine\": (cl0 * np.exp(-0.022 * hour) if stype == \"water_truck\" else cl0 + rng.normal(0, 0.03, T)),\n", " \"metal\": mt0 + rng.normal(0, 0.2, T), \"ecoli\": np.zeros(T)}\n", " if stype in (\"river_intake\", \"well\"):\n", " tr[\"turbidity\"] = tr[\"turbidity\"] + rain * (0.9 if stype == \"river_intake\" else 0.45)\n", " f_all, etype = np.zeros(T), np.array([\"none\"] * T, dtype=object)\n", " for (s_i, et, onset, dur, scale) in events:\n", " if s_i != si:\n", " continue\n", " delta, ramp = EVENT_TYPES[et]\n", " f = np.clip((tt - onset + 1) / ramp, 0, 1) * ((tt >= onset) & (tt < onset + dur + ramp))\n", " for k, v in delta.items():\n", " tr[k] = tr[k] + v * f * scale\n", " etype[f > 0] = et\n", " f_all = np.maximum(f_all, f)\n", " tr[\"chlorine\"] = np.clip(tr[\"chlorine\"], 0.02, None)\n", " tr[\"turbidity\"] = np.clip(tr[\"turbidity\"], 0.05, None)\n", " tr[\"metal\"] = np.clip(tr[\"metal\"], 0.1, None)\n", " grade = np.where(f_all >= 0.2, 2, 0)\n", " mild = (tr[\"turbidity\"] > LIMITS[\"turbidity\"][0]) | (tr[\"chlorine\"] < LIMITS[\"chlorine\"][0]) | (tr[\"metal\"] > LIMITS[\"metal\"][0])\n", " grade = np.where((grade == 0) & mild, 1, grade)\n", " ms = {}\n", " for p in [\"ph\", \"turbidity\", \"chlorine\", \"metal\"]:\n", " sd = NOISE[p] * np.where(night, 1.5, 1.0) * (tr[p] if p == \"turbidity\" else 1)\n", " ms[p] = np.round(np.clip(tr[p] + rng.normal(0, 1, T) * sd, 0, None), DIGITS[p])\n", " ms[\"ecoli\"] = (rng.poisson(tr[\"ecoli\"]) + (rng.random(T) < 0.004)).astype(float)\n", " fault, fparam = np.array([\"none\"] * T, dtype=object), np.array([\"-\"] * T, dtype=object)\n", " for (s_i, ft, p, s0, d) in faults:\n", " if s_i != si:\n", " continue\n", " seg = slice(s0, min(T, s0 + d))\n", " if ft == \"stuck\":\n", " ms[p][seg] = ms[p][s0]\n", " elif ft == \"spike\":\n", " seg = slice(s0, s0 + 1)\n", " ms[p][s0] = {\"ph\": 11.8, \"turbidity\": 38.0, \"chlorine\": 0.0, \"metal\": 75.0}[p]\n", " elif ft == \"drift\":\n", " ramp = np.linspace(0, 1, seg.stop - s0)\n", " ms[p][seg] = np.clip(ms[p][seg] + ramp * {\"ph\": -1.6, \"turbidity\": 2.6, \"chlorine\": -0.75, \"metal\": 12.0}[p], 0, None).round(DIGITS[p])\n", " elif ft == \"dropout\":\n", " for q in PARAMS:\n", " ms[q][seg] = np.nan\n", " p = \"all\"\n", " fault[seg], fparam[seg] = ft, p\n", " df = pd.DataFrame({\"source_id\": sid, \"source_type\": stype, \"x_km\": x, \"y_km\": y, \"t\": tt,\n", " \"cond\": np.where(rain > 0, \"storm\", np.where(night, \"night\", \"day\")),\n", " **{p: ms[p] for p in PARAMS}, **{f\"true_{p}\": tr[p] for p in PARAMS},\n", " \"event\": etype, \"fault\": fault, \"fault_param\": fparam, \"grade\": grade})\n", " out.append(df)\n", " return pd.concat(out, ignore_index=True)\n", "# ---------------------------------------------------------------------------\n", "\n", "# ---- ์ „์ฒ˜๋ฆฌ ๊ณตํ†ต ํ•จ์ˆ˜: ๋ฌผ๋ฆฌ ๋ฒ”์œ„ ์ ๊ฒ€ -> ๊ณ ์ •๊ฐ’(stuck) ํ‘œ์‹œ -> ๋‹จ๋ฐœ ์ŠคํŒŒ์ดํฌ ์ œ๊ฑฐ -> ๊ฒฐ์ธก ๋ณด์ • ----\n", "PHYS = {\"ph\": (0, 14), \"turbidity\": (0, 1000), \"chlorine\": (0, 10), \"metal\": (0, 1000), \"ecoli\": (0, 100000)}\n", "SPIKE_K = {\"day\": 6.0, \"night\": 7.0, \"storm\": 9.0} # ์ƒํ™ฉ๋ณ„(์ฃผ๊ฐ„/์•ผ๊ฐ„/์•…์ฒœํ›„) ์ŠคํŒŒ์ดํฌ ํŒ์ • ๋ฐฐ์ˆ˜\n", "MIN_JUMP = {\"ph\": 0.8, \"turbidity\": 1.5, \"chlorine\": 0.3, \"metal\": 6.0}\n", "STUCK_RUN, SHORT_GAP = 6, 3\n", "\n", "\n", "def clean(df, spike_k=None, min_jump=None):\n", " \"\"\"๊ธ‰์ˆ˜์›๋ณ„ ์‹œ๊ณ„์—ด์„ ์ •์ œํ•˜๊ณ  ํ•ญ๋ชฉ๋ณ„ ์ฒ˜๋ฆฌ ๋‚ด์—ญ(<ํ•ญ๋ชฉ>_flag: range/stuck/spike/interp/long_gap)์„ ๋‚จ๊ธด๋‹ค.\"\"\"\n", " spike_k = spike_k or SPIKE_K\n", " min_jump = MIN_JUMP if min_jump is None else min_jump\n", " out = df.sort_values([\"source_id\", \"t\"]).reset_index(drop=True)\n", " for sid, idx in out.groupby(\"source_id\").groups.items():\n", " idx = np.asarray(idx)\n", " kk = out.loc[idx, \"cond\"].map(spike_k).to_numpy(float)\n", " for p in PARAMS:\n", " v = out.loc[idx, p].to_numpy(float).copy()\n", " fl = np.array([\"\"] * len(v), dtype=object)\n", " lo, hi = PHYS[p]\n", " bad = (v < lo) | (v > hi)\n", " v[bad], fl[bad] = np.nan, \"range\"\n", " if p != \"ecoli\":\n", " chg = np.r_[True, v[1:] != v[:-1]] # ๊ฐ™์€ ๊ฐ’์ด STUCK_RUN์‹œ๊ฐ„ ์ด์ƒ ๋ฐ˜๋ณต = ๊ณ ์ •๊ฐ’ ๊ณ ์žฅ ์˜์‹ฌ\n", " rid = np.cumsum(chg)\n", " rlen = np.bincount(rid)[rid]\n", " fl[(rlen >= STUCK_RUN) & ~chg & ~np.isnan(v)] = \"stuck\"\n", " d = np.abs(np.diff(v[~np.isnan(v)]))\n", " sig = 1.4826 * np.median(d) + 1e-6 if len(d) else 1.0\n", " up, dn = v - np.r_[np.nan, v[:-1]], v - np.r_[v[1:], np.nan]\n", " thr = np.maximum(kk * sig, min_jump[p])\n", " spike = (np.abs(up) > thr) & (np.abs(dn) > thr) & (np.sign(up) == np.sign(dn)) # ํŠ€์—ˆ๋‹ค๊ฐ€ ๋ฐ”๋กœ ๋Œ์•„์˜จ ๊ฐ’๋งŒ\n", " v[spike], fl[spike] = np.nan, \"spike\"\n", " s = pd.Series(v)\n", " na = s.isna().to_numpy()\n", " grp = np.cumsum(np.r_[True, na[1:] != na[:-1]])\n", " glen = pd.Series(na.astype(int)).groupby(grp).transform(\"sum\").to_numpy()\n", " lin, ff = s.interpolate(limit_area=\"inside\").to_numpy(), s.ffill().bfill().to_numpy()\n", " short, longg = na & (glen <= SHORT_GAP), na & (glen > SHORT_GAP)\n", " v = np.where(short, np.where(np.isnan(lin), ff, lin), np.where(longg, ff, v))\n", " fl[short & (fl == \"\")] = \"interp\"\n", " fl[longg] = \"long_gap\"\n", " out.loc[idx, p] = v\n", " out.loc[idx, f\"{p}_flag\"] = fl\n", " return out\n", "\n", "# ---- ๊ฐ€์ • ๊ธฐ์ค€์น˜ ํŒ์ •(Baseline)๊ณผ ์‹œ๊ณ„์—ด ํŠน์ง• ๊ณตํ†ต ํ•จ์ˆ˜ ----\n", "GRADE_NAME = {0: \"์•ˆ์ „\", 1: \"์ฃผ์˜\", 2: \"์œ„ํ—˜\"}\n", "\n", "\n", "def rule_grade(df):\n", " \"\"\"๊ธฐ์ค€์น˜ 1์ฐจ ํ•„ํ„ฐ: ์œ„ํ—˜(์Œ์šฉ ๋ถ€์ ํ•ฉ ์˜์‹ฌ) = ํ•œ ํ•ญ๋ชฉ์ด๋ผ๋„ ์œ„ํ—˜ ๊ธฐ์ค€ ์ดˆ๊ณผ, ์ฃผ์˜ = ์ฃผ์˜ ๊ธฐ์ค€ ์ดˆ๊ณผ.\"\"\"\n", " ph, tb, cl, mt, ec = (df[p].to_numpy(float) for p in PARAMS)\n", " danger = (ph < LIMITS[\"ph\"][0]) | (ph > LIMITS[\"ph\"][1]) | (tb > LIMITS[\"turbidity\"][1]) | (cl < LIMITS[\"chlorine\"][1]) \\\n", " | (mt > LIMITS[\"metal\"][1]) | (ec >= LIMITS[\"ecoli\"][1])\n", " caution = (tb > LIMITS[\"turbidity\"][0]) | (cl < LIMITS[\"chlorine\"][0]) | (mt > LIMITS[\"metal\"][0])\n", " return np.where(danger, 2, np.where(caution, 1, 0))\n", "\n", "\n", "def reasons(row):\n", " r = []\n", " if not LIMITS[\"ph\"][0] <= row[\"ph\"] <= LIMITS[\"ph\"][1]:\n", " r.append(f\"pH {row['ph']:.2f}\")\n", " if row[\"turbidity\"] > LIMITS[\"turbidity\"][0]:\n", " r.append(f\"ํƒ๋„ {row['turbidity']:.2f}NTU\")\n", " if row[\"chlorine\"] < LIMITS[\"chlorine\"][0]:\n", " r.append(f\"์ž”๋ฅ˜์—ผ์†Œ {row['chlorine']:.2f}mg/L\")\n", " if row[\"metal\"] > LIMITS[\"metal\"][0]:\n", " r.append(f\"์ค‘๊ธˆ์† {row['metal']:.1f}ug/L\")\n", " if row[\"ecoli\"] >= 1:\n", " r.append(f\"๋Œ€์žฅ๊ท  {row['ecoli']:.0f}\")\n", " return \", \".join(r) or \"-\"\n", "\n", "\n", "FEATS = [f\"{p}{s}\" for p in [\"ph\", \"turbidity\", \"chlorine\", \"metal\"] for s in [\"\", \"_d1\", \"_d3\", \"_dev\", \"_sd6\"]] + \\\n", " [\"ecoli\", \"ecoli_3h\", \"storm\", \"night\", \"n_flag\", \"truck\", \"purif\", \"well\", \"river\"]\n", "\n", "\n", "def add_features(c):\n", " \"\"\"์‹œ๊ฐ„ ์ˆœ์„œ๋ฅผ ์ง€ํ‚ค๋Š”(๊ณผ๊ฑฐ ๊ฐ’๋งŒ ์“ฐ๋Š”) ๊ธ‰์ˆ˜์›๋ณ„ ํŠน์ง•: ๋ณ€ํ™”๋Ÿ‰, 24์‹œ๊ฐ„ ๊ธฐ์ค€์„  ๋Œ€๋น„ ํŽธ์ฐจ, 6์‹œ๊ฐ„ ๋ณ€๋™์„ฑ, ๋Œ€์žฅ๊ท  ๋ˆ„์  ๋“ฑ.\"\"\"\n", " c = c.sort_values([\"source_id\", \"t\"]).reset_index(drop=True)\n", " g = c.groupby(\"source_id\")\n", " for p in [\"ph\", \"turbidity\", \"chlorine\", \"metal\"]:\n", " c[f\"{p}_d1\"] = g[p].diff().fillna(0)\n", " c[f\"{p}_d3\"] = g[p].diff(3).fillna(0)\n", " base = g[p].transform(lambda s: s.shift(1).rolling(24, min_periods=4).median())\n", " c[f\"{p}_dev\"] = (c[p] - base).fillna(0)\n", " c[f\"{p}_sd6\"] = g[p].transform(lambda s: s.rolling(6, min_periods=2).std()).fillna(0)\n", " c[\"ecoli_3h\"] = g[\"ecoli\"].transform(lambda s: s.rolling(3, min_periods=1).sum())\n", " c[\"storm\"], c[\"night\"] = (c[\"cond\"] == \"storm\").astype(int), (c[\"cond\"] == \"night\").astype(int)\n", " flags = [f\"{p}_flag\" for p in PARAMS if f\"{p}_flag\" in c]\n", " c[\"n_flag\"] = sum(c[f].isin([\"stuck\", \"spike\", \"long_gap\", \"range\"]).astype(int) for f in flags) if flags else 0\n", " for k, name in [(\"truck\", \"water_truck\"), (\"purif\", \"purification_unit\"), (\"well\", \"well\"), (\"river\", \"river_intake\")]:\n", " c[k] = (c[\"source_type\"] == name).astype(int)\n", " return c\n", "\n", "ROLE_ACTIONS = {\"์ธก์ •๋ณ‘\": {\"sample\"}, \"์˜ˆ๋ฐฉ์˜ํ•™๋‹ด๋‹น\": {\"review\"}, \"์ง€ํœ˜๊ด€\": {\"approve_stop\", \"reject\"}, \"์‹œ์Šคํ…œ\": {\"enqueue\"}}\n", "USERS = {\"op01\": \"์ธก์ •๋ณ‘\", \"med01\": \"์˜ˆ๋ฐฉ์˜ํ•™๋‹ด๋‹น\", \"cdr01\": \"์ง€ํœ˜๊ด€\", \"sys\": \"์‹œ์Šคํ…œ\"}\n", "FLOW = {\"enqueue\": (\"-\", \"์ฑ„์ˆ˜๋Œ€๊ธฐ\"), \"sample\": (\"์ฑ„์ˆ˜๋Œ€๊ธฐ\", \"๊ฒ€ํ† ๋Œ€๊ธฐ\"), \"review\": (\"๊ฒ€ํ† ๋Œ€๊ธฐ\", \"์Šน์ธ๋Œ€๊ธฐ\"),\n", " \"approve_stop\": (\"์Šน์ธ๋Œ€๊ธฐ\", \"๊ธ‰์ˆ˜์ค‘๋‹จ\"), \"reject\": (\"์Šน์ธ๋Œ€๊ธฐ\", \"๊ธ‰์ˆ˜์œ ์ง€\")}\n", "\n", "\n", "class AuditLog:\n", " def __init__(self):\n", " self.rows = []\n", "\n", " def add(self, **kw):\n", " prev = self.rows[-1][\"hash\"] if self.rows else \"0\" * 16\n", " body = json.dumps(kw, ensure_ascii=False, sort_keys=True)\n", " self.rows.append({**kw, \"prev\": prev, \"hash\": hashlib.sha256((prev + body).encode()).hexdigest()[:16]})\n", "\n", " def verify(self):\n", " prev = \"0\" * 16\n", " for r in self.rows:\n", " body = json.dumps({k: v for k, v in r.items() if k not in (\"prev\", \"hash\")}, ensure_ascii=False, sort_keys=True)\n", " if r[\"prev\"] != prev or hashlib.sha256((prev + body).encode()).hexdigest()[:16] != r[\"hash\"]:\n", " return False\n", " prev = r[\"hash\"]\n", " return True\n", "\n", "\n", "class Workflow:\n", " def __init__(self):\n", " self.log, self.state, self.n_try = AuditLog(), {}, 0\n", "\n", " def act(self, user, action, case, t, note=\"\"):\n", " \"\"\"๊ถŒํ•œยท์ˆœ์„œยท์‚ฌ์œ ๋ฅผ ๊ฒ€์‚ฌํ•œ ๋’ค ์ƒํƒœ๋ฅผ ๋ฐ”๊พผ๋‹ค. ๊ฑฐ๋ถ€๋„ ๊ฐ์‚ฌ ๋กœ๊ทธ์— ๋‚จ๊ธด๋‹ค.\"\"\"\n", " self.n_try += 1\n", " role = USERS.get(user, \"๋ฏธ๋“ฑ๋ก\")\n", " cur = self.state.get(case, \"-\")\n", " err = None\n", " if action not in ROLE_ACTIONS.get(role, set()):\n", " err = f\"๊ถŒํ•œ ์—†์Œ({role} ์—ญํ• ์— {action} ๊ถŒํ•œ ์—†์Œ)\"\n", " elif FLOW[action][0] != cur:\n", " err = f\"์ˆœ์„œ ์œ„๋ฐ˜(ํ˜„์žฌ {cur})\"\n", " elif action in (\"reject\", \"approve_stop\", \"review\") and len(note.strip()) < 5:\n", " err = \"์‚ฌ์œ ยท๊ทผ๊ฑฐ ์ž…๋ ฅ ํ•„์ˆ˜\"\n", " if err is None:\n", " self.state[case] = FLOW[action][1]\n", " self.log.add(t=t, user=user, role=role, case=case, action=action, result=\"๊ฑฐ๋ถ€\" if err else \"์ฒ˜๋ฆฌ\",\n", " detail=err or note, state=self.state.get(case, \"-\"))\n", " return err is None\n", "\n", "\n", "# 1) ๊ฒฝ๋ณด ์ƒ์„ฑ: ๊ฐ€์ƒ ๋ฒค์น˜๋งˆํฌ์—์„œ ์œ„ํ—˜ ํŒ์ •์ด 2์‹œ๊ฐ„ ์ด์ƒ ์ด์–ด์ง„ ๊ตฌ๊ฐ„์„ '๊ฒฝ๋ณด ์‚ฌ๊ฑด'์œผ๋กœ ๋ฌถ๋Š”๋‹ค\n", "c = clean(make_dataset(days=14, seed=11, n_events=10, n_faults=14))\n", "c[\"g\"] = rule_grade(c)\n", "cases = []\n", "for sid, s in c.groupby(\"source_id\"):\n", " d = (s[\"g\"] == 2).astype(int).to_numpy()\n", " seg = np.split(np.arange(len(d)), np.where(np.diff(d) != 0)[0] + 1)\n", " for idx in seg:\n", " if d[idx[0]] == 1 and len(idx) >= 2:\n", " r = s.iloc[idx[0]]\n", " cases.append({\"case\": f\"AL-{len(cases) + 1:02d}\", \"source\": sid, \"type\": r[\"source_type\"], \"t\": int(r[\"t\"]),\n", " \"hours\": len(idx), \"real\": bool((s.iloc[idx][\"grade\"] == 2).mean() >= 0.5), \"basis\": reasons(s.iloc[idx[-1]])})\n", "Q = pd.DataFrame(cases)\n", "# ์šฐ์„ ์ˆœ์œ„: ์ง€์†์‹œ๊ฐ„ + ๊ธ‰์ˆ˜์ฐจยท์ •์ˆ˜์žฅ(๊ณต๊ธ‰ ์ธ์› ๋งŽ์Œ) ๊ฐ€์ค‘ + ๋Œ€์žฅ๊ท ยท์ค‘๊ธˆ์† ํฌํ•จ ๊ฐ€์ค‘\n", "Q[\"priority\"] = Q[\"hours\"].clip(upper=12) + Q[\"type\"].isin([\"water_truck\", \"purification_unit\"]) * 4 \\\n", " + Q[\"basis\"].str.contains(\"๋Œ€์žฅ๊ท |์ค‘๊ธˆ์†\") * 3\n", "Q = Q.sort_values([\"priority\", \"t\"], ascending=[False, True]).reset_index(drop=True)\n", "print(f\"[๊ต์œก์šฉ๊ฐ€์ƒ์ž๋ฃŒ] ๊ฒฝ๋ณด ์‚ฌ๊ฑด {len(Q)}๊ฑด์ด ๊ฒ€ํ†  ํ์— ๋“ฑ๋ก๋จ(์ž๋™ ๊ธ‰์ˆ˜ ์ค‘๋‹จ 0๊ฑด)\")\n", "\n", "# 2) ๊ฒฐ์žฌ ์‹œ๋ฎฌ๋ ˆ์ด์…˜: ์ฑ„์ˆ˜ ๋‹ด๋‹น 30๋ถ„ ๋‚ด ์ฑ„์ˆ˜, ์˜ˆ๋ฐฉ์˜ํ•™ ๋‹ด๋‹น ์žฌ๊ฒ€์‚ฌ ๊ฒฐ๊ณผ๋กœ ์˜๊ฒฌ, ์ง€ํœ˜๊ด€์ด ์Šน์ธยท๋ฐ˜๋ ค\n", "wf = Workflow()\n", "rng = np.random.default_rng(5)\n", "wait = []\n", "for _, q in Q.iterrows():\n", " t = q[\"t\"] * 60\n", " wf.act(\"sys\", \"enqueue\", q[\"case\"], t, \"๊ธฐ์ค€์น˜ ์ดˆ๊ณผ ๊ฒฝ๋ณด ์ž๋™ ๋“ฑ๋ก\")\n", " t += int(rng.integers(10, 30)); wf.act(\"op01\", \"sample\", q[\"case\"], t, \"ํ˜„์žฅ ํ‚คํŠธ ์žฌ์ฑ„์ˆ˜\")\n", " t += int(rng.integers(20, 60))\n", " opinion = \"์žฌ๊ฒ€์‚ฌ ์œ„ํ—˜ ํ™•์ธ, ๊ธ‰์ˆ˜ ์ค‘๋‹จ ๊ถŒ๊ณ \" if q[\"real\"] else \"์žฌ๊ฒ€์‚ฌ ์ •์ƒ, ์„ผ์„œ ์ ๊ฒ€ ๊ถŒ๊ณ \"\n", " wf.act(\"med01\", \"review\", q[\"case\"], t, opinion)\n", " t += int(rng.integers(5, 25))\n", " if q[\"real\"]:\n", " wf.act(\"cdr01\", \"approve_stop\", q[\"case\"], t, f\"{q['basis']} ํ™•์ธ, ๋Œ€์ฒด ๊ธ‰์ˆ˜์› ์ „ํ™˜\")\n", " else:\n", " wf.act(\"cdr01\", \"reject\", q[\"case\"], t, \"์žฌ๊ฒ€์‚ฌ ์ •์ƒ, ๊ธ‰์ˆ˜ ์œ ์ง€ํ•˜๊ณ  ์„ผ์„œ ๊ต์ •\")\n", " wait.append(t - q[\"t\"] * 60)\n", "st = pd.Series(wf.state).value_counts()\n", "print(\"๊ฒฐ์žฌ ๊ฒฐ๊ณผ:\", st.to_dict(), f\"/ ๊ฒฝ๋ณด๋ถ€ํ„ฐ ๊ฒฐ์ •๊นŒ์ง€ ํ‰๊ท  {np.mean(wait):.0f}๋ถ„, ์ตœ๋Œ€ {max(wait)}๋ถ„\")\n", "print(\"\\n[๊ฒ€ํ†  ํ ์ƒ์œ„ 5๊ฑด]\")\n", "print(Q.head(5)[[\"case\", \"source\", \"type\", \"t\", \"hours\", \"priority\", \"basis\"]].to_string(index=False))\n", "\n", "# 3) ๊ถŒํ•œ ์šฐํšŒยท์ ˆ์ฐจ ์œ„๋ฐ˜ ์‹œ๋„ 6์ข… (์‚ฌ์ด์‚ฌ์ด ์ •์ƒ ์ ˆ์ฐจ๋ฅผ ์ง„ํ–‰)\n", "wf.act(\"sys\", \"enqueue\", \"AL-X\", 99999, \"์‹œํ—˜ ๊ฒฝ๋ณด\")\n", "STEPS = [(\"์œ„๋ฐ˜\", \"op01\", \"approve_stop\", \"์ธก์ •๋ณ‘์ด ์ง์ ‘ ๊ธ‰์ˆ˜ ์ค‘๋‹จ\", \"์ค‘๋‹จ\"), (\"์œ„๋ฐ˜\", \"sys\", \"approve_stop\", \"์‹œ์Šคํ…œ ์ž๋™ ํ™•์ •\", \"์ž๋™\"),\n", " (\"์œ„๋ฐ˜\", \"cdr01\", \"approve_stop\", \"์ฑ„์ˆ˜ยท๊ฒ€ํ†  ์—†์ด ์ง€ํœ˜๊ด€ ์Šน์ธ\", \"์ฆ‰์‹œ ์ค‘๋‹จ\"), (\"์œ„๋ฐ˜\", \"hacker\", \"review\", \"๋ฏธ๋“ฑ๋ก ์‚ฌ์šฉ์ž ๊ฒ€ํ† \", \"์œ„ํ—˜ ํ™•์ธ๋จ\"),\n", " (\"์ •์ƒ\", \"op01\", \"sample\", \"์ •์ƒ ์ฑ„์ˆ˜\", \"์žฌ์ฑ„์ˆ˜\"), (\"์œ„๋ฐ˜\", \"med01\", \"review\", \"๊ทผ๊ฑฐ ์—†๋Š” ๊ฒ€ํ†  ์˜๊ฒฌ\", \"\"),\n", " (\"์ •์ƒ\", \"med01\", \"review\", \"์ •์ƒ ๊ฒ€ํ† \", \"์žฌ๊ฒ€์‚ฌ ์ •์ƒ ํ™•์ธ\"), (\"์œ„๋ฐ˜\", \"cdr01\", \"reject\", \"์‚ฌ์œ  ์—†๋Š” ๋ฐ˜๋ ค\", \"\")]\n", "blocked = n_v = 0\n", "print(\"\\n[๊ถŒํ•œ ์šฐํšŒยท์ ˆ์ฐจ ์œ„๋ฐ˜ ์‹œ๋„]\")\n", "for kind, u, a, desc, note in STEPS:\n", " ok = wf.act(u, a, \"AL-X\", 99999, note)\n", " if kind == \"์œ„๋ฐ˜\":\n", " n_v += 1; blocked += not ok\n", " print(f\" {desc:16s} -> {'์ฐจ๋‹จ' if not ok else 'ํ†ต๊ณผ(๋ฌธ์ œ)'}: {wf.log.rows[-1]['detail']}\")\n", "print(f\"์šฐํšŒยท์œ„๋ฐ˜ ์‹œ๋„ ์ฐจ๋‹จ {blocked}/{n_v} ({blocked / n_v:.0%})\")\n", "\n", "# 4) ๊ฐ์‚ฌ ๋กœ๊ทธ ๋ฌด๊ฒฐ์„ฑ๊ณผ ๋กœ๊น… ์„ฑ๊ณต๋ฅ \n", "n_log = len(wf.log.rows)\n", "print(f\"\\n๊ฐ์‚ฌ ๋กœ๊ทธ {n_log}๊ฑด / ์š”์ฒญ {wf.n_try}๊ฑด -> ๋กœ๊น… ์„ฑ๊ณต๋ฅ  {n_log / wf.n_try:.0%}, ํ•ด์‹œ ์ฒด์ธ ๊ฒ€์ฆ: {'์ •์ƒ' if wf.log.verify() else '์œ„๋ณ€์กฐ'}\")\n", "wf.log.rows[5][\"detail\"] = \"๊ธฐ๋ก ์ž„์˜ ์ˆ˜์ •\"\n", "print(f\"5๋ฒˆ์งธ ๊ธฐ๋ก ์ž„์˜ ์ˆ˜์ • ํ›„ ๊ฒ€์ฆ: {'์ •์ƒ' if wf.log.verify() else '์œ„๋ณ€์กฐ ํƒ์ง€'}\")\n", "print(\"โ€ป ๊ธ‰์ˆ˜ ์ค‘๋‹จยท์žฌ๊ฐœ๋Š” ์ง€ํœ˜๊ด€ ๊ฒฐ์ • ์‚ฌํ•ญ์ด๋ฉฐ, ์‹œ์Šคํ…œ์€ ๊ฒฝ๋ณด์™€ ๊ทผ๊ฑฐ๋งŒ ์ œ๊ณตํ•œ๋‹ค. ์‹ค์ œ ํŒ์ •์€ ๊ณต์‹ ๊ธฐ์ค€๊ณผ ์˜๋ฌดยทํ™˜๊ฒฝ ๋‹ด๋‹น ํ™•์ธ์— ๋”ฐ๋ฅธ๋‹ค.\")\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "
\n", "๐Ÿ’ก [์ฐธ๊ณ ] ๋ฏธ์…˜ 07 ์˜ˆ์ƒ ์‹คํ–‰ ๊ฒฐ๊ณผ (Output)\n", "\n", "```text\n", "[๊ต์œก์šฉ๊ฐ€์ƒ์ž๋ฃŒ] ๊ฒฝ๋ณด ์‚ฌ๊ฑด 10๊ฑด์ด ๊ฒ€ํ†  ํ์— ๋“ฑ๋ก๋จ(์ž๋™ ๊ธ‰์ˆ˜ ์ค‘๋‹จ 0๊ฑด)\n", "๊ฒฐ์žฌ ๊ฒฐ๊ณผ: {'๊ธ‰์ˆ˜์ค‘๋‹จ': 9, '๊ธ‰์ˆ˜์œ ์ง€': 1} / ๊ฒฝ๋ณด๋ถ€ํ„ฐ ๊ฒฐ์ •๊นŒ์ง€ ํ‰๊ท  69๋ถ„, ์ตœ๋Œ€ 82๋ถ„\n", "\n", "[๊ฒ€ํ†  ํ ์ƒ์œ„ 5๊ฑด]\n", " case source type t hours priority basis\n", "AL-02 W03 purification_unit 243 23 19 ์ž”๋ฅ˜์—ผ์†Œ 0.00mg/L, ๋Œ€์žฅ๊ท  9\n", "AL-01 W02 water_truck 147 21 16 pH 8.87, ํƒ๋„ 1.21NTU\n", "AL-03 W04 purification_unit 288 16 16 pH 8.72, ํƒ๋„ 1.12NTU\n", "AL-09 W08 river_intake 69 22 15 ์ค‘๊ธˆ์† 15.0ug/L\n", "AL-07 W07 river_intake 115 29 15 ํƒ๋„ 3.11NTU, ์ž”๋ฅ˜์—ผ์†Œ 0.08mg/L, ๋Œ€์žฅ๊ท  39\n", "\n", "[๊ถŒํ•œ ์šฐํšŒยท์ ˆ์ฐจ ์œ„๋ฐ˜ ์‹œ๋„]\n", " ์ธก์ •๋ณ‘์ด ์ง์ ‘ ๊ธ‰์ˆ˜ ์ค‘๋‹จ -> ์ฐจ๋‹จ: ๊ถŒํ•œ ์—†์Œ(์ธก์ •๋ณ‘ ์—ญํ• ์— approve_stop ๊ถŒํ•œ ์—†์Œ)\n", " ์‹œ์Šคํ…œ ์ž๋™ ํ™•์ • -> ์ฐจ๋‹จ: ๊ถŒํ•œ ์—†์Œ(์‹œ์Šคํ…œ ์—ญํ• ์— approve_stop ๊ถŒํ•œ ์—†์Œ)\n", " ์ฑ„์ˆ˜ยท๊ฒ€ํ†  ์—†์ด ์ง€ํœ˜๊ด€ ์Šน์ธ -> ์ฐจ๋‹จ: ์ˆœ์„œ ์œ„๋ฐ˜(ํ˜„์žฌ ์ฑ„์ˆ˜๋Œ€๊ธฐ)\n", " ๋ฏธ๋“ฑ๋ก ์‚ฌ์šฉ์ž ๊ฒ€ํ†  -> ์ฐจ๋‹จ: ๊ถŒํ•œ ์—†์Œ(๋ฏธ๋“ฑ๋ก ์—ญํ• ์— review ๊ถŒํ•œ ์—†์Œ)\n", " ๊ทผ๊ฑฐ ์—†๋Š” ๊ฒ€ํ†  ์˜๊ฒฌ -> ์ฐจ๋‹จ: ์‚ฌ์œ ยท๊ทผ๊ฑฐ ์ž…๋ ฅ ํ•„์ˆ˜\n", " ์‚ฌ์œ  ์—†๋Š” ๋ฐ˜๋ ค -> ์ฐจ๋‹จ: ์‚ฌ์œ ยท๊ทผ๊ฑฐ ์ž…๋ ฅ ํ•„์ˆ˜\n", "์šฐํšŒยท์œ„๋ฐ˜ ์‹œ๋„ ์ฐจ๋‹จ 6/6 (100%)\n", "\n", "๊ฐ์‚ฌ ๋กœ๊ทธ 49๊ฑด / ์š”์ฒญ 49๊ฑด -> ๋กœ๊น… ์„ฑ๊ณต๋ฅ  100%, ํ•ด์‹œ ์ฒด์ธ ๊ฒ€์ฆ: ์ •์ƒ\n", "5๋ฒˆ์งธ ๊ธฐ๋ก ์ž„์˜ ์ˆ˜์ • ํ›„ ๊ฒ€์ฆ: ์œ„๋ณ€์กฐ ํƒ์ง€\n", "โ€ป ๊ธ‰์ˆ˜ ์ค‘๋‹จยท์žฌ๊ฐœ๋Š” ์ง€ํœ˜๊ด€ ๊ฒฐ์ • ์‚ฌํ•ญ์ด๋ฉฐ, ์‹œ์Šคํ…œ์€ ๊ฒฝ๋ณด์™€ ๊ทผ๊ฑฐ๋งŒ ์ œ๊ณตํ•œ๋‹ค. ์‹ค์ œ ํŒ์ •์€ ๊ณต์‹ ๊ธฐ์ค€๊ณผ ์˜๋ฌดยทํ™˜๊ฒฝ ๋‹ด๋‹น ํ™•์ธ์— ๋”ฐ๋ฅธ๋‹ค.\n", "```\n", "
\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "---\n", "\n", "## ๐ŸŽฏ ๋ฏธ์…˜ 08. ์˜ค์—ผ๋ฌผ์งˆ ๋Œ€๋Ÿ‰ ์œ ์ž… ๊ฐ€์ƒ ์‹œ๋‚˜๋ฆฌ์˜ค ๋ถ€ํ•˜ ํ…Œ์ŠคํŠธ ๋ฐ ๊ฒฝ๋ณด ๋ฐ˜์‘ ์‹œ๊ฐ„ ๊ฒ€์ฆ\n", "**์†Œ์š”์‹œ๊ฐ„**: ์•ฝ 3.5์‹œ๊ฐ„\n", "\n", "### ๐Ÿ“‹ ๋ฏธ์…˜ ๋ฐฐ๊ฒฝ ๋ฐ ์ˆ˜ํ–‰ ์ง€์นจ\n", "#### ๋ฏธ์…˜ ๋ฐฐ๊ฒฝ ๋ฐ ๋‹น์œ„์„ฑ\n", "\n", "์˜ค์—ผ๋ฌผ์งˆ ๋Œ€๋Ÿ‰ ์œ ์ž… ๊ฐ€์ƒ ์‹œ๋‚˜๋ฆฌ์˜ค ๋ถ€ํ•˜ ํ…Œ์ŠคํŠธ๋ฅผ ์ˆ˜ํ–‰ํ•œ๋‹ค. ์‹ค์ œ ์ „์ˆ  ํ˜„์žฅ์—์„œ๋Š” ๋‹จ์ˆœ ํƒ์ง€์— ๊ทธ์น˜์ง€ ์•Š๊ณ  ์‹ ์†ํ•œ ์œ„ํ˜‘ ํŒ๋‹จ๊ณผ ์‹ ๋ขฐ๋„ ๋†’์€ ์˜์‚ฌ๊ฒฐ์ •์ด ํ•„์ˆ˜์ ์ด๋ฏ€๋กœ, **์˜ค์—ผ๋ฌผ์งˆ์ด ๋Œ€๋Ÿ‰ ์œ ์ž…๋˜๋Š” ๊ฐ€์ƒ ์‹œ๋‚˜๋ฆฌ์˜ค์—์„œ ํŒŒ์ดํ”„๋ผ์ธ ๋ถ€ํ•˜ ์‹œํ—˜์„ ํ•˜๊ณ  ๊ฒฝ๋ณด ๋ฐ˜์‘ ์‹œ๊ฐ„๊ณผ Fail-safe ๋™์ž‘์„ ๊ฒ€์ฆํ•œ๋‹ค.** ๊ณผ์—…์„ ์™„์ˆ˜ํ•˜์—ฌ ์˜คํŒ์„ ๋ฐฉ์ง€ํ•˜๊ณ  ์ž‘์ „ ๋Œ€์‘๋ ฅ์„ ๊ทน๋Œ€ํ™”ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.\n", "\n", "์ด ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด ๋ณธ ๋ฏธ์…˜์—์„œ๋Š” **๊ธ‰์ˆ˜์›์„ 8๊ณณ์—์„œ 48๊ณณ์œผ๋กœ ๋Š˜๋ ค ํŠธ๋ž˜ํ”ฝ์„ 500% ํ‚ค์šฐ๊ณ  30๊ณณ์— ์˜ค์—ผ์ด ๋™์‹œ์— ์œ ์ž…๋˜๋Š” ์ƒํ™ฉ์—์„œ ์ˆ˜์ง‘ยท์ •์ œยทํŒ์ • ์ฃผ๊ธฐ์˜ ์ฒ˜๋ฆฌ ์‹œ๊ฐ„, ์‹œ์Šคํ…œ ์ค‘๋‹จ, ๋ฐ์ดํ„ฐ ์œ ์‹ค, ๋ฉ”๋ชจ๋ฆฌ ์ฆ๊ฐ€, ๊ฒฝ๋ณด ํƒ์ง€์™€ ๋ฐ˜์‘ ์‹œ๊ฐ„์„ ํ‰์‹œ์™€ ๋น„๊ต ์ˆ˜ํ–‰ํ•˜๊ณ  ๊ฒ€์ฆํ•˜๋Š”** ์‹ค์Šต์„ ์ฒด๊ณ„์ ์œผ๋กœ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค.\n", "\n", "ํ•™์Šต์ž๋Š” ์‹ค์Šต๊ฐ€์ด๋“œ์— ์ œ์‹œ๋œ ์ด 4๊ฐœ ์„ธ๋ถ€ ํ™œ๋™(๊ธฐ๋ณธ ๊ธฐ๋Šฅ ๊ตฌํ˜„ โž” ๊ฒฐ๊ณผ ์ง€ํ‘œ ๊ฒ€์ฆ โž” ํŒŒ๋ผ๋ฏธํ„ฐ ํŠœ๋‹ ๋ฐ ๋„์ „๊ณผ์ œ)์„ ์ˆœ์ฐจ์ ์œผ๋กœ ์ง„ํ–‰ํ•˜๋ฉฐ ํ•ต์‹ฌ ํŒ์ • ๊ธฐ์ค€์„ ๋‹ฌ์„ฑํ•ฉ๋‹ˆ๋‹ค.\n", "\n", "**์ด๋ฒˆ ๊ฒฐ๊ณผ๋ฌผ:** `tests/wel03_stress_test.py ๋ฐ ์ŠคํŠธ๋ ˆ์Šค ํ…Œ์ŠคํŠธ ๋ถ„์„ ๋ณด๊ณ ์„œ` ํ˜•ํƒœ์˜ ๊ฒฐ๊ณผ๋ฌผ์„ ์™„์„ฑํ•œ๋‹ค.\n", "\n", "### ๐Ÿ” ํ‰๊ฐ€ ๋ฐ ๊ฒ€์ฆ ๊ธฐ์ค€ (PASS ์กฐ๊ฑด)\n", "#### ๋ฏธ์…˜ ํŒ์ •\n", "\n", "**ํ‰๊ฐ€ ๊ตฌ๋ถ„:** ํ•„์ˆ˜\n", "\n", "**ํ•ต์‹ฌ ํ‰๊ฐ€์˜์—ญ:** ๊ทน๋‹จ ์ƒํ™ฉ์—์„œ์˜ ์•ˆ์ „ ์‹คํŒจ(Fail-safe) ๋ฐ ๊ธฐ๋Šฅ ์ถ•์†Œ ์ž‘๋™ ์ ์ ˆ์„ฑ\n", "\n", "#### ๋ฏธ์…˜ PASS ์กฐ๊ฑด\n", "\n", "- ์ •๋Ÿ‰ ๊ธฐ์ค€: ๋™์‹œ ํŠธ๋ž˜ํ”ฝ 500% ์ฆ๊ฐ€ ์‹œ ์‹œ์Šคํ…œ ๋‹ค์šด 0๊ฑด, ๋ฐ์ดํ„ฐ ์œ ์‹ค๋ฅ  0%\n", "\n", "\n", "- ์ง„ํ–‰ ๊ธฐ์ค€: ๋Œ€๋Ÿ‰ ์‹œ๋ฎฌ๋ ˆ์ด์…˜ ๋ถ€ํ•˜ ์ฃผ์ž… ํ…Œ์ŠคํŠธ ํ†ต๊ณผ ๋ฐ ๋ณ‘๋ชฉ ๊ตฌ๊ฐ„ ํ”„๋กœํŒŒ์ผ๋ง ์™„๋ฃŒ\n", "\n", "\n", "- ๊ฒ€์ฆ ๋ฐฉ๋ฒ•: ๋ถ€ํ•˜ ํ…Œ์ŠคํŠธ ๋„๊ตฌ๋ฅผ ํ†ตํ•œ ์ฒ˜๋ฆฌ๋Ÿ‰(Throughput) ๋ฐ ๋ฉ”๋ชจ๋ฆฌ ๋ˆ„์ˆ˜ ๊ฒ€์ฆ\n", "\n", "\n", "#### ์ฑ„์ ์ž ํŒ์ • ๋ฉ”๋ชจ\n", "\n", "> ๋‹จ์ˆœ ์ฝ”๋“œ ์‹คํ–‰ ์—ฌ๋ถ€๋ฟ๋งŒ ์•„๋‹ˆ๋ผ ์ถœ๋ ฅ๋œ ์ˆ˜์น˜์™€ ๊ทผ๊ฑฐ ์ž๋ฃŒ๊ฐ€ ๊ตฐ ์‹ค๋ฌด ์š”๊ตฌ์‚ฌํ•ญ์— ๋ถ€ํ•ฉํ•˜๋Š”์ง€ ๋ฉด๋ฐ€ํžˆ ํ™•์ธํ•œ๋‹ค.\n", "\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "\"\"\"๊ฐ€์ƒ ๊ธ‰์ˆ˜์›์„ 8๊ณณ์—์„œ 48๊ณณ(ํŠธ๋ž˜ํ”ฝ 500% ์ฆ๊ฐ€)์œผ๋กœ ๋Š˜๋ฆฌ๊ณ  30๊ณณ์— ์˜ค์—ผ๋ฌผ์งˆ์ด ๋™์‹œ์— ์œ ์ž…๋˜๋Š” ์‹œ๋‚˜๋ฆฌ์˜ค๋กœ ์ˆ˜์ง‘ยทํŒ์ • ํŒŒ์ดํ”„๋ผ์ธ ๋ถ€ํ•˜ ์‹œํ—˜์„ ์ˆ˜ํ–‰ํ•ด, ์‹œ์Šคํ…œ ์ค‘๋‹จยท๋ฐ์ดํ„ฐ ์œ ์‹คยท๋ฉ”๋ชจ๋ฆฌ ์ฆ๊ฐ€ยท๊ฒฝ๋ณด ๋ฐ˜์‘ ์‹œ๊ฐ„๊ณผ ๊ณผ๋ถ€ํ•˜ ์‹œ ๊ธฐ๋Šฅ ์ถ•์†Œ(Fail-safe) ๋™์ž‘์„ ๊ฒ€์ฆํ•œ๋‹ค.\"\"\"\n", "import time\n", "import numpy as np\n", "import pandas as pd\n", "import matplotlib.pyplot as plt\n", "\n", "# ---------------------------------------------------------------------------\n", "# [๊ต์œก์šฉ๊ฐ€์ƒ์ž๋ฃŒ] ์•ผ์ „ ๊ธ‰์ˆ˜์› ๊ฐ€์ƒ ์ˆ˜์งˆ ์„ผ์„œ ๋ฐ์ดํ„ฐ ์ƒ์„ฑ๊ธฐ (์‹ค์ œ ๋ถ€๋Œ€ยท์œ„์น˜ยท๊ฒ€์‚ฌ ๊ฒฐ๊ณผ ์—†์Œ)\n", "# ๊ฐ€์ƒ ๊ธ‰์ˆ˜์› 8๊ณณ x 14์ผ x 1์‹œ๊ฐ„ ๊ฐ„๊ฒฉ. ์‹ค์ œ ์ˆ˜์งˆ(true_*)ยท์„ผ์„œ ์ธก์ •๊ฐ’ยท์˜ค์—ผ ์‚ฌ๊ฑดยท์„ผ์„œ ๊ณ ์žฅ ์ •๋‹ต์„ ํ•จ๊ป˜ ๋งŒ๋“ ๋‹ค.\n", "# ๊ธฐ์ค€์น˜๋Š” ๊ต์œก์šฉ ๊ฐ€์ •๊ฐ’์ด๋ฉฐ, ์‹ค์ œ ์Œ์šฉ ํŒ์ •์€ ๊ณต์ธ ๊ฒ€์‚ฌ ๊ฒฐ๊ณผ์™€ ์˜๋ฌด(์˜ˆ๋ฐฉ์˜ํ•™) ๋‹ด๋‹น ๊ธฐ์ค€์— ๋”ฐ๋ฅธ๋‹ค.\n", "# ---------------------------------------------------------------------------\n", "PARAMS = [\"ph\", \"turbidity\", \"chlorine\", \"metal\", \"ecoli\"]\n", "SOURCES = [(\"W01\", \"water_truck\", 1.0, 2.0), (\"W02\", \"water_truck\", 3.5, 1.2), (\"W03\", \"purification_unit\", 2.2, 4.1),\n", " (\"W04\", \"purification_unit\", 5.0, 3.3), (\"W05\", \"well\", 0.8, 5.6), (\"W06\", \"well\", 4.4, 6.0),\n", " (\"W07\", \"river_intake\", 6.3, 1.8), (\"W08\", \"river_intake\", 6.9, 4.9)]\n", "BASE = {\"water_truck\": (7.2, 0.25, 0.75, 1.5), \"purification_unit\": (7.0, 0.20, 0.85, 1.2),\n", " \"well\": (6.8, 0.45, 0.50, 3.0), \"river_intake\": (7.3, 0.55, 0.60, 2.0)} # pH, ํƒ๋„(NTU), ์ž”๋ฅ˜์—ผ์†Œ(mg/L), ์ค‘๊ธˆ์†(ug/L)\n", "LIMITS = {\"ph\": (5.8, 8.5), \"turbidity\": (1.0, 2.0), \"chlorine\": (0.2, 0.1), \"metal\": (5.0, 10.0), \"ecoli\": (1, 1)} # (์ฃผ์˜, ์œ„ํ—˜) ๊ฐ€์ • ๊ธฐ์ค€\n", "EVENT_TYPES = { # ์˜ค์—ผ ์‚ฌ๊ฑด: ๋ณ€์ˆ˜๋ณ„ ์ตœ๋Œ€ ๋ณ€ํ™”๋Ÿ‰, ์ตœ๋Œ€์น˜ ๋„๋‹ฌ ์‹œ๊ฐ„(h)\n", " \"sewage\": ({\"turbidity\": 2.8, \"chlorine\": -0.5, \"ecoli\": 30, \"ph\": -0.3}, 4),\n", " \"metal\": ({\"metal\": 22.0, \"ph\": -0.5}, 4),\n", " \"disinfect_fail\": ({\"chlorine\": -0.8, \"ecoli\": 6}, 6),\n", " \"chemical\": ({\"ph\": 1.7, \"turbidity\": 1.0}, 3),\n", " \"creeping_metal\": ({\"metal\": 13.0}, 18),\n", "}\n", "FAULT_TYPES = [\"stuck\", \"spike\", \"drift\", \"dropout\"]\n", "NOISE = {\"ph\": 0.04, \"turbidity\": 0.06, \"chlorine\": 0.03, \"metal\": 0.35}\n", "DIGITS = {\"ph\": 2, \"turbidity\": 2, \"chlorine\": 2, \"metal\": 1}\n", "\n", "\n", "def make_dataset(days=14, seed=7, n_events=6, n_faults=10, events=None, faults=None, sources=None):\n", " \"\"\"events: [(๊ธ‰์ˆ˜์› ๋ฒˆํ˜ธ, ์‚ฌ๊ฑด ์œ ํ˜•, ์‹œ์ž‘ ์‹œ๊ฐ h, ์ง€์† h, ๊ฐ•๋„)], faults: [(๊ธ‰์ˆ˜์› ๋ฒˆํ˜ธ, ๊ณ ์žฅ ์œ ํ˜•, ํ•ญ๋ชฉ, ์‹œ์ž‘ h, ์ง€์† h)]\"\"\"\n", " rng = np.random.default_rng(seed)\n", " sources = sources or SOURCES\n", " T = days * 24\n", " tt = np.arange(T)\n", " hour = tt % 24\n", " night = (hour < 6) | (hour >= 20)\n", " rain = np.zeros(T)\n", " for _ in range(max(1, days // 4)):\n", " s0, d = int(rng.integers(0, T - 14)), int(rng.integers(6, 14))\n", " rain[s0:s0 + d] = np.maximum(rain[s0:s0 + d], rng.uniform(0.6, 1.0))\n", " if events is None:\n", " events = [(int(rng.integers(len(sources))), str(rng.choice(list(EVENT_TYPES))), int(rng.integers(min(24, T // 4), max(T // 4 + 1, T - 48))),\n", " int(rng.integers(10, 30)), 1.0) for _ in range(n_events)]\n", " if faults is None:\n", " faults = [(int(rng.integers(len(sources))), str(rng.choice(FAULT_TYPES)), str(rng.choice([\"ph\", \"turbidity\", \"chlorine\", \"metal\"])),\n", " int(rng.integers(12, max(13, T - 30))), int(rng.integers(6, 24))) for _ in range(n_faults)]\n", " out = []\n", " for si, (sid, stype, x, y) in enumerate(sources):\n", " ph0, tb0, cl0, mt0 = BASE[stype]\n", " tr = {\"ph\": ph0 + 0.08 * np.sin(2 * np.pi * hour / 24) + rng.normal(0, 0.03, T),\n", " \"turbidity\": tb0 * (1 + 0.1 * rng.normal(0, 1, T)),\n", " \"chlorine\": (cl0 * np.exp(-0.022 * hour) if stype == \"water_truck\" else cl0 + rng.normal(0, 0.03, T)),\n", " \"metal\": mt0 + rng.normal(0, 0.2, T), \"ecoli\": np.zeros(T)}\n", " if stype in (\"river_intake\", \"well\"):\n", " tr[\"turbidity\"] = tr[\"turbidity\"] + rain * (0.9 if stype == \"river_intake\" else 0.45)\n", " f_all, etype = np.zeros(T), np.array([\"none\"] * T, dtype=object)\n", " for (s_i, et, onset, dur, scale) in events:\n", " if s_i != si:\n", " continue\n", " delta, ramp = EVENT_TYPES[et]\n", " f = np.clip((tt - onset + 1) / ramp, 0, 1) * ((tt >= onset) & (tt < onset + dur + ramp))\n", " for k, v in delta.items():\n", " tr[k] = tr[k] + v * f * scale\n", " etype[f > 0] = et\n", " f_all = np.maximum(f_all, f)\n", " tr[\"chlorine\"] = np.clip(tr[\"chlorine\"], 0.02, None)\n", " tr[\"turbidity\"] = np.clip(tr[\"turbidity\"], 0.05, None)\n", " tr[\"metal\"] = np.clip(tr[\"metal\"], 0.1, None)\n", " grade = np.where(f_all >= 0.2, 2, 0)\n", " mild = (tr[\"turbidity\"] > LIMITS[\"turbidity\"][0]) | (tr[\"chlorine\"] < LIMITS[\"chlorine\"][0]) | (tr[\"metal\"] > LIMITS[\"metal\"][0])\n", " grade = np.where((grade == 0) & mild, 1, grade)\n", " ms = {}\n", " for p in [\"ph\", \"turbidity\", \"chlorine\", \"metal\"]:\n", " sd = NOISE[p] * np.where(night, 1.5, 1.0) * (tr[p] if p == \"turbidity\" else 1)\n", " ms[p] = np.round(np.clip(tr[p] + rng.normal(0, 1, T) * sd, 0, None), DIGITS[p])\n", " ms[\"ecoli\"] = (rng.poisson(tr[\"ecoli\"]) + (rng.random(T) < 0.004)).astype(float)\n", " fault, fparam = np.array([\"none\"] * T, dtype=object), np.array([\"-\"] * T, dtype=object)\n", " for (s_i, ft, p, s0, d) in faults:\n", " if s_i != si:\n", " continue\n", " seg = slice(s0, min(T, s0 + d))\n", " if ft == \"stuck\":\n", " ms[p][seg] = ms[p][s0]\n", " elif ft == \"spike\":\n", " seg = slice(s0, s0 + 1)\n", " ms[p][s0] = {\"ph\": 11.8, \"turbidity\": 38.0, \"chlorine\": 0.0, \"metal\": 75.0}[p]\n", " elif ft == \"drift\":\n", " ramp = np.linspace(0, 1, seg.stop - s0)\n", " ms[p][seg] = np.clip(ms[p][seg] + ramp * {\"ph\": -1.6, \"turbidity\": 2.6, \"chlorine\": -0.75, \"metal\": 12.0}[p], 0, None).round(DIGITS[p])\n", " elif ft == \"dropout\":\n", " for q in PARAMS:\n", " ms[q][seg] = np.nan\n", " p = \"all\"\n", " fault[seg], fparam[seg] = ft, p\n", " df = pd.DataFrame({\"source_id\": sid, \"source_type\": stype, \"x_km\": x, \"y_km\": y, \"t\": tt,\n", " \"cond\": np.where(rain > 0, \"storm\", np.where(night, \"night\", \"day\")),\n", " **{p: ms[p] for p in PARAMS}, **{f\"true_{p}\": tr[p] for p in PARAMS},\n", " \"event\": etype, \"fault\": fault, \"fault_param\": fparam, \"grade\": grade})\n", " out.append(df)\n", " return pd.concat(out, ignore_index=True)\n", "# ---------------------------------------------------------------------------\n", "\n", "# ---- ์ „์ฒ˜๋ฆฌ ๊ณตํ†ต ํ•จ์ˆ˜: ๋ฌผ๋ฆฌ ๋ฒ”์œ„ ์ ๊ฒ€ -> ๊ณ ์ •๊ฐ’(stuck) ํ‘œ์‹œ -> ๋‹จ๋ฐœ ์ŠคํŒŒ์ดํฌ ์ œ๊ฑฐ -> ๊ฒฐ์ธก ๋ณด์ • ----\n", "PHYS = {\"ph\": (0, 14), \"turbidity\": (0, 1000), \"chlorine\": (0, 10), \"metal\": (0, 1000), \"ecoli\": (0, 100000)}\n", "SPIKE_K = {\"day\": 6.0, \"night\": 7.0, \"storm\": 9.0} # ์ƒํ™ฉ๋ณ„(์ฃผ๊ฐ„/์•ผ๊ฐ„/์•…์ฒœํ›„) ์ŠคํŒŒ์ดํฌ ํŒ์ • ๋ฐฐ์ˆ˜\n", "MIN_JUMP = {\"ph\": 0.8, \"turbidity\": 1.5, \"chlorine\": 0.3, \"metal\": 6.0}\n", "STUCK_RUN, SHORT_GAP = 6, 3\n", "\n", "\n", "def clean(df, spike_k=None, min_jump=None):\n", " \"\"\"๊ธ‰์ˆ˜์›๋ณ„ ์‹œ๊ณ„์—ด์„ ์ •์ œํ•˜๊ณ  ํ•ญ๋ชฉ๋ณ„ ์ฒ˜๋ฆฌ ๋‚ด์—ญ(<ํ•ญ๋ชฉ>_flag: range/stuck/spike/interp/long_gap)์„ ๋‚จ๊ธด๋‹ค.\"\"\"\n", " spike_k = spike_k or SPIKE_K\n", " min_jump = MIN_JUMP if min_jump is None else min_jump\n", " out = df.sort_values([\"source_id\", \"t\"]).reset_index(drop=True)\n", " for sid, idx in out.groupby(\"source_id\").groups.items():\n", " idx = np.asarray(idx)\n", " kk = out.loc[idx, \"cond\"].map(spike_k).to_numpy(float)\n", " for p in PARAMS:\n", " v = out.loc[idx, p].to_numpy(float).copy()\n", " fl = np.array([\"\"] * len(v), dtype=object)\n", " lo, hi = PHYS[p]\n", " bad = (v < lo) | (v > hi)\n", " v[bad], fl[bad] = np.nan, \"range\"\n", " if p != \"ecoli\":\n", " chg = np.r_[True, v[1:] != v[:-1]] # ๊ฐ™์€ ๊ฐ’์ด STUCK_RUN์‹œ๊ฐ„ ์ด์ƒ ๋ฐ˜๋ณต = ๊ณ ์ •๊ฐ’ ๊ณ ์žฅ ์˜์‹ฌ\n", " rid = np.cumsum(chg)\n", " rlen = np.bincount(rid)[rid]\n", " fl[(rlen >= STUCK_RUN) & ~chg & ~np.isnan(v)] = \"stuck\"\n", " d = np.abs(np.diff(v[~np.isnan(v)]))\n", " sig = 1.4826 * np.median(d) + 1e-6 if len(d) else 1.0\n", " up, dn = v - np.r_[np.nan, v[:-1]], v - np.r_[v[1:], np.nan]\n", " thr = np.maximum(kk * sig, min_jump[p])\n", " spike = (np.abs(up) > thr) & (np.abs(dn) > thr) & (np.sign(up) == np.sign(dn)) # ํŠ€์—ˆ๋‹ค๊ฐ€ ๋ฐ”๋กœ ๋Œ์•„์˜จ ๊ฐ’๋งŒ\n", " v[spike], fl[spike] = np.nan, \"spike\"\n", " s = pd.Series(v)\n", " na = s.isna().to_numpy()\n", " grp = np.cumsum(np.r_[True, na[1:] != na[:-1]])\n", " glen = pd.Series(na.astype(int)).groupby(grp).transform(\"sum\").to_numpy()\n", " lin, ff = s.interpolate(limit_area=\"inside\").to_numpy(), s.ffill().bfill().to_numpy()\n", " short, longg = na & (glen <= SHORT_GAP), na & (glen > SHORT_GAP)\n", " v = np.where(short, np.where(np.isnan(lin), ff, lin), np.where(longg, ff, v))\n", " fl[short & (fl == \"\")] = \"interp\"\n", " fl[longg] = \"long_gap\"\n", " out.loc[idx, p] = v\n", " out.loc[idx, f\"{p}_flag\"] = fl\n", " return out\n", "\n", "# ---- ๊ฐ€์ • ๊ธฐ์ค€์น˜ ํŒ์ •(Baseline)๊ณผ ์‹œ๊ณ„์—ด ํŠน์ง• ๊ณตํ†ต ํ•จ์ˆ˜ ----\n", "GRADE_NAME = {0: \"์•ˆ์ „\", 1: \"์ฃผ์˜\", 2: \"์œ„ํ—˜\"}\n", "\n", "\n", "def rule_grade(df):\n", " \"\"\"๊ธฐ์ค€์น˜ 1์ฐจ ํ•„ํ„ฐ: ์œ„ํ—˜(์Œ์šฉ ๋ถ€์ ํ•ฉ ์˜์‹ฌ) = ํ•œ ํ•ญ๋ชฉ์ด๋ผ๋„ ์œ„ํ—˜ ๊ธฐ์ค€ ์ดˆ๊ณผ, ์ฃผ์˜ = ์ฃผ์˜ ๊ธฐ์ค€ ์ดˆ๊ณผ.\"\"\"\n", " ph, tb, cl, mt, ec = (df[p].to_numpy(float) for p in PARAMS)\n", " danger = (ph < LIMITS[\"ph\"][0]) | (ph > LIMITS[\"ph\"][1]) | (tb > LIMITS[\"turbidity\"][1]) | (cl < LIMITS[\"chlorine\"][1]) \\\n", " | (mt > LIMITS[\"metal\"][1]) | (ec >= LIMITS[\"ecoli\"][1])\n", " caution = (tb > LIMITS[\"turbidity\"][0]) | (cl < LIMITS[\"chlorine\"][0]) | (mt > LIMITS[\"metal\"][0])\n", " return np.where(danger, 2, np.where(caution, 1, 0))\n", "\n", "\n", "def reasons(row):\n", " r = []\n", " if not LIMITS[\"ph\"][0] <= row[\"ph\"] <= LIMITS[\"ph\"][1]:\n", " r.append(f\"pH {row['ph']:.2f}\")\n", " if row[\"turbidity\"] > LIMITS[\"turbidity\"][0]:\n", " r.append(f\"ํƒ๋„ {row['turbidity']:.2f}NTU\")\n", " if row[\"chlorine\"] < LIMITS[\"chlorine\"][0]:\n", " r.append(f\"์ž”๋ฅ˜์—ผ์†Œ {row['chlorine']:.2f}mg/L\")\n", " if row[\"metal\"] > LIMITS[\"metal\"][0]:\n", " r.append(f\"์ค‘๊ธˆ์† {row['metal']:.1f}ug/L\")\n", " if row[\"ecoli\"] >= 1:\n", " r.append(f\"๋Œ€์žฅ๊ท  {row['ecoli']:.0f}\")\n", " return \", \".join(r) or \"-\"\n", "\n", "\n", "FEATS = [f\"{p}{s}\" for p in [\"ph\", \"turbidity\", \"chlorine\", \"metal\"] for s in [\"\", \"_d1\", \"_d3\", \"_dev\", \"_sd6\"]] + \\\n", " [\"ecoli\", \"ecoli_3h\", \"storm\", \"night\", \"n_flag\", \"truck\", \"purif\", \"well\", \"river\"]\n", "\n", "\n", "def add_features(c):\n", " \"\"\"์‹œ๊ฐ„ ์ˆœ์„œ๋ฅผ ์ง€ํ‚ค๋Š”(๊ณผ๊ฑฐ ๊ฐ’๋งŒ ์“ฐ๋Š”) ๊ธ‰์ˆ˜์›๋ณ„ ํŠน์ง•: ๋ณ€ํ™”๋Ÿ‰, 24์‹œ๊ฐ„ ๊ธฐ์ค€์„  ๋Œ€๋น„ ํŽธ์ฐจ, 6์‹œ๊ฐ„ ๋ณ€๋™์„ฑ, ๋Œ€์žฅ๊ท  ๋ˆ„์  ๋“ฑ.\"\"\"\n", " c = c.sort_values([\"source_id\", \"t\"]).reset_index(drop=True)\n", " g = c.groupby(\"source_id\")\n", " for p in [\"ph\", \"turbidity\", \"chlorine\", \"metal\"]:\n", " c[f\"{p}_d1\"] = g[p].diff().fillna(0)\n", " c[f\"{p}_d3\"] = g[p].diff(3).fillna(0)\n", " base = g[p].transform(lambda s: s.shift(1).rolling(24, min_periods=4).median())\n", " c[f\"{p}_dev\"] = (c[p] - base).fillna(0)\n", " c[f\"{p}_sd6\"] = g[p].transform(lambda s: s.rolling(6, min_periods=2).std()).fillna(0)\n", " c[\"ecoli_3h\"] = g[\"ecoli\"].transform(lambda s: s.rolling(3, min_periods=1).sum())\n", " c[\"storm\"], c[\"night\"] = (c[\"cond\"] == \"storm\").astype(int), (c[\"cond\"] == \"night\").astype(int)\n", " flags = [f\"{p}_flag\" for p in PARAMS if f\"{p}_flag\" in c]\n", " c[\"n_flag\"] = sum(c[f].isin([\"stuck\", \"spike\", \"long_gap\", \"range\"]).astype(int) for f in flags) if flags else 0\n", " for k, name in [(\"truck\", \"water_truck\"), (\"purif\", \"purification_unit\"), (\"well\", \"well\"), (\"river\", \"river_intake\")]:\n", " c[k] = (c[\"source_type\"] == name).astype(int)\n", " return c\n", "\n", "BUDGET_MS = 300 # 1์‹œ๊ฐ„ ์ฃผ๊ธฐ ์ฒ˜๋ฆฌ ์˜ˆ์‚ฐ(๋ชจ์˜): ์ง์ „ ์ฃผ๊ธฐ๊ฐ€ ์˜ˆ์‚ฐ์„ ๋„˜์œผ๋ฉด ๊ธฐ๋Šฅ ์ถ•์†Œ ๋ชจ๋“œ๋กœ ์ „ํ™˜\n", "FULL_EVERY = 12 # ๊ธฐ๋Šฅ ์ถ•์†Œ ์ค‘์—๋„ 12์‹œ๊ฐ„๋งˆ๋‹ค ์ „์ฒด ์ •์ œ(์ง€์—ฐ ์ฒ˜๋ฆฌ)๋ฅผ ์ˆ˜ํ–‰\n", "\n", "\n", "def make_sources(n, seed=0):\n", " rng = np.random.default_rng(seed)\n", " types = [s[1] for s in SOURCES]\n", " return [(f\"W{i + 1:02d}\", types[i % len(types)], float(round(rng.uniform(0, 30), 1)), float(round(rng.uniform(0, 30), 1))) for i in range(n)]\n", "\n", "\n", "def lite_check(batch, last):\n", " \"\"\"๊ธฐ๋Šฅ ์ถ•์†Œ ๋ชจ๋“œ: ๋ ˆ์ฝ”๋“œ ๋‹จ์œ„ ๋ฒ”์œ„ ์ ๊ฒ€ -> ์ง์ „ ์ •์ƒ๊ฐ’ ์œ ์ง€ -> ๊ธฐ์ค€์น˜ ํŒ์ •(์ •์ œยทํŠน์ง• ๊ณ„์‚ฐ ์ƒ๋žต).\"\"\"\n", " b = batch.copy()\n", " for p in PARAMS:\n", " v = pd.to_numeric(b[p], errors=\"coerce\")\n", " lo, hi = PHYS[p]\n", " v = v.where(v.between(lo, hi))\n", " b[p] = v.fillna(b[\"source_id\"].map(last.get(p, {}))).fillna(0 if p == \"ecoli\" else np.nan)\n", " last.setdefault(p, {}).update(b.dropna(subset=[p]).set_index(\"source_id\")[p].to_dict())\n", " b = b.dropna(subset=[\"ph\", \"turbidity\", \"chlorine\", \"metal\"])\n", " b[\"g\"] = rule_grade(b)\n", " return b\n", "\n", "\n", "def run(n_src, days, events, seed, mode_auto=True, hours=None):\n", " src = make_sources(n_src)\n", " data = make_dataset(days=days, seed=seed, events=events, faults=[], sources=src)\n", " rng = np.random.default_rng(seed)\n", " bad_idx = rng.choice(len(data), int(len(data) * 0.01), replace=False) # 1% ์†์ƒ ๋ ˆ์ฝ”๋“œ ์ฃผ์ž…(๋ฌธ์žยทNone)\n", " data[PARAMS] = data[PARAMS].astype(object)\n", " data.loc[bad_idx[: len(bad_idx) // 2], \"turbidity\"] = \"ERR\"\n", " data.loc[bad_idx[len(bad_idx) // 2:], \"ph\"] = None\n", " stored, quarantine, alerts, ticks, down, last = 0, 0, {}, [], 0, {}\n", " buf, mode, switched = [], \"full\", None\n", " mem = []\n", " for h in range(hours or days * 24):\n", " t0 = time.perf_counter()\n", " try:\n", " batch = data[data[\"t\"] == h]\n", " if mode_auto and mode == \"full\" and ticks and ticks[-1] > BUDGET_MS:\n", " mode, switched = \"lite\", h # ํ•œ ๋ฒˆ ์ „ํ™˜ํ•˜๋ฉด ๋ถ€ํ•˜ ํ•ด์†Œ ํ™•์ธ ์ „๊นŒ์ง€ ์œ ์ง€\n", " ok_mask = batch[[\"ph\", \"turbidity\", \"chlorine\", \"metal\"]].apply(pd.to_numeric, errors=\"coerce\").notna().all(axis=1)\n", " quarantine += int((~ok_mask).sum())\n", " if mode == \"full\":\n", " buf.append(batch[ok_mask].astype({p: float for p in PARAMS}))\n", " win = pd.concat(buf[-24:])\n", " c = clean(win)\n", " res = c[c[\"t\"] == h].assign(g=lambda d: rule_grade(d))\n", " else:\n", " res = lite_check(batch, last)\n", " buf.append(batch[ok_mask].astype({p: float for p in PARAMS}))\n", " if h % FULL_EVERY == FULL_EVERY - 1: # ์ง€์—ฐ ์ „์ฒด ์ •์ œ(๊ธฐ๋ก ๋ณด์ •์šฉ)\n", " clean(pd.concat(buf[-FULL_EVERY:]))\n", " buf = buf[-24:]\n", " stored += len(batch)\n", " for sid in res.loc[res[\"g\"] == 2, \"source_id\"]:\n", " alerts.setdefault(sid, []).append(h)\n", " except Exception:\n", " down += 1\n", " ticks.append((time.perf_counter() - t0) * 1000)\n", " mem.append(sum(b.memory_usage(deep=True).sum() for b in buf) / 1e6) # ๋ณด๊ด€ ๋ฒ„ํผ ๋ฉ”๋ชจ๋ฆฌ(MB)\n", " last = {p: last.get(p, {}) for p in PARAMS}\n", " react = []\n", " for e in events:\n", " hs = [h for h in alerts.get(src[e[0]][0], []) if h >= e[2]]\n", " if hs:\n", " react.append(hs[0] - e[2])\n", " return {\"src\": n_src, \"recs\": len(data), \"stored\": stored, \"quarantine\": quarantine, \"down\": down, \"mode\": mode if switched is None else f\"lite({switched}h~)\",\n", " \"tick_avg\": np.mean(ticks), \"tick_max\": np.max(ticks), \"mem_first\": np.mean(mem[24:48]) if len(mem) >= 48 else mem[-1], \"mem_last\": np.mean(mem[-24:]),\n", " \"react\": react, \"detected\": len(react), \"n_events\": len(events), \"ticks\": ticks}\n", "\n", "\n", "days = 2\n", "ev8 = [(i, et, 20 + i, 30, 1.0) for i, et in zip(range(5), [\"sewage\", \"metal\", \"chemical\", \"disinfect_fail\", \"sewage\"])]\n", "ev48 = [(i, [\"sewage\", \"metal\", \"chemical\", \"disinfect_fail\"][i % 4], 20 + i % 6, 30, 1.0) for i in range(0, 48, 48 // 30 + 0)][:30]\n", "t_all = time.perf_counter()\n", "R = {\"ํ‰์‹œ(8๊ณณ)\": run(8, days, ev8, 1), \"๋ถ€ํ•˜ 500%(48๊ณณ, 30๊ณณ ๋™์‹œ ์˜ค์—ผ)\": run(48, days, ev48, 2)}\n", "print(f\"[๊ต์œก์šฉ๊ฐ€์ƒ์ž๋ฃŒ] ๋ถ€ํ•˜ ์‹œํ—˜ {days}์ผ ๋ชจ์˜, ์ „์ฒด ์‹คํ–‰ {time.perf_counter() - t_all:.1f}์ดˆ\")\n", "rows = []\n", "for k, r in R.items():\n", " loss = 1 - r[\"stored\"] / r[\"recs\"]\n", " rows.append({\"๊ตฌ๋ถ„\": k, \"๋ ˆ์ฝ”๋“œ\": r[\"recs\"], \"๊ฒฉ๋ฆฌ(์†์ƒ)\": r[\"quarantine\"], \"์œ ์‹ค๋ฅ \": f\"{loss:.1%}\", \"์ค‘๋‹จ\": r[\"down\"],\n", " \"๋ชจ๋“œ\": r[\"mode\"], \"์ฃผ๊ธฐ ํ‰๊ท ms\": round(r[\"tick_avg\"], 1), \"์ฃผ๊ธฐ ์ตœ๋Œ€ms\": round(r[\"tick_max\"], 1),\n", " \"๋ฉ”๋ชจ๋ฆฌMB(์ดˆ๊ธฐ->๋ง๊ธฐ)\": f\"{r['mem_first']:.1f}->{r['mem_last']:.1f}\",\n", " \"๊ฒฝ๋ณด ํƒ์ง€\": f\"{r['detected']}/{r['n_events']}\", \"๋ฐ˜์‘ ํ‰๊ท h\": round(float(np.mean(r[\"react\"])), 1)})\n", "print(pd.DataFrame(rows).set_index(\"๊ตฌ๋ถ„\").T.to_string())\n", "S = R[\"๋ถ€ํ•˜ 500%(48๊ณณ, 30๊ณณ ๋™์‹œ ์˜ค์—ผ)\"]\n", "print(f\"\\n๋ถ€ํ•˜ 500%: ์‹œ์Šคํ…œ ์ค‘๋‹จ {S['down']}๊ฑด(๋ชฉํ‘œ 0๊ฑด: {'๋‹ฌ์„ฑ' if S['down'] == 0 else '๋ฏธ๋‹ฌ'}), \"\n", " f\"๋ฐ์ดํ„ฐ ์œ ์‹ค๋ฅ  {1 - S['stored'] / S['recs']:.1%}(๋ชฉํ‘œ 0%: {'๋‹ฌ์„ฑ' if S['stored'] == S['recs'] else '๋ฏธ๋‹ฌ'})\")\n", "print(f\"๋ฉ”๋ชจ๋ฆฌ ๋ง๊ธฐ/์ดˆ๊ธฐ ๋น„์œจ {S['mem_last'] / max(S['mem_first'], 1e-6):.2f} (๋ฒ„ํผ 24์‹œ๊ฐ„ ๊ณ ์ •, 1.2 ์ดํ•˜๋ฉด ๋ˆ„์ˆ˜ ์—†์Œ), \"\n", " f\"๊ฒฝ๋ณด ๋ฐ˜์‘ ์ตœ๋Œ€ {max(S['react'])}์‹œ๊ฐ„\")\n", "\n", "# ๊ธฐ๋Šฅ ์ถ•์†Œ๋ฅผ ๋„๊ณ  ์ „์ฒด ์ •์ œ๋ฅผ ๊ฐ•ํ–‰ํ•˜๋ฉด ์ฃผ๊ธฐ ์‹œ๊ฐ„์ด ์–ผ๋งˆ๋‚˜ ๋Š˜์–ด๋‚˜๋Š”์ง€ (6์‹œ๊ฐ„๋งŒ ์ธก์ •)\n", "F = run(48, 1, [], 3, mode_auto=False, hours=6)\n", "print(f\"๊ธฐ๋Šฅ ์ถ•์†Œ ๋ฏธ์ ์šฉ(์ „์ฒด ์ •์ œ ๊ฐ•ํ–‰) ์ฃผ๊ธฐ ํ‰๊ท  {np.mean(F['ticks']):.0f}ms vs ๊ธฐ๋Šฅ ์ถ•์†Œ {S['tick_avg']:.0f}ms \"\n", " f\"(์˜ˆ์‚ฐ {BUDGET_MS}ms) -> ๋ณ‘๋ชฉ: ๊ธ‰์ˆ˜์›๋ณ„ ์ •์ œ ๋ฐ˜๋ณต(clean ํ•จ์ˆ˜)\")\n", "print(\"Fail-safe ์›์น™: ์†์ƒ ๋ ˆ์ฝ”๋“œ๋Š” ๋ฒ„๋ฆฌ์ง€ ์•Š๊ณ  ๊ฒฉ๋ฆฌ ๋ณด๊ด€, ํŒ์ •์€ ์ง์ „ ์ •์ƒ๊ฐ’์œผ๋กœ ์ด์–ด๊ฐ€๋ฉฐ, ๊ณผ๋ถ€ํ•˜ ์‹œ ์ •์ œยทํŠน์ง• ๊ณ„์‚ฐ์„ ๋ฏธ๋ฃจ๊ณ  ๊ฒฝ๋ณด ํŒ์ •์€ ์œ ์ง€\")\n", "\n", "plt.figure(figsize=(9, 3.5))\n", "plt.plot(R[\"ํ‰์‹œ(8๊ณณ)\"][\"ticks\"], label=\"normal (8 sources)\")\n", "plt.plot(S[\"ticks\"], label=\"500% load (48 sources, degraded mode)\")\n", "plt.plot(F[\"ticks\"], label=\"500% load, full mode forced (6 h)\")\n", "plt.axhline(BUDGET_MS, color=\"red\", ls=\"--\", lw=1, label=\"budget\")\n", "plt.xlabel(\"simulated hour\"); plt.ylabel(\"processing ms per tick\"); plt.legend(); plt.title(\"Stress test tick time (synthetic)\")\n", "plt.tight_layout(); plt.show()\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "
\n", "๐Ÿ’ก [์ฐธ๊ณ ] ๋ฏธ์…˜ 08 ์˜ˆ์ƒ ์‹คํ–‰ ๊ฒฐ๊ณผ (Output)\n", "\n", "```text\n", "[๊ต์œก์šฉ๊ฐ€์ƒ์ž๋ฃŒ] ๋ถ€ํ•˜ ์‹œํ—˜ 2์ผ ๋ชจ์˜, ์ „์ฒด ์‹คํ–‰ 11.9์ดˆ\n", "๊ตฌ๋ถ„ ํ‰์‹œ(8๊ณณ) ๋ถ€ํ•˜ 500%(48๊ณณ, 30๊ณณ ๋™์‹œ ์˜ค์—ผ)\n", "๋ ˆ์ฝ”๋“œ 384 2304\n", "๊ฒฉ๋ฆฌ(์†์ƒ) 3 23\n", "์œ ์‹ค๋ฅ  0.0% 0.0%\n", "์ค‘๋‹จ 0 0\n", "๋ชจ๋“œ full lite(1h~)\n", "์ฃผ๊ธฐ ํ‰๊ท ms 127.1 87.7\n", "์ฃผ๊ธฐ ์ตœ๋Œ€ms 183.6 742.7\n", "๋ฉ”๋ชจ๋ฆฌMB(์ดˆ๊ธฐ->๋ง๊ธฐ) 0.1->0.1 0.6->0.6\n", "๊ฒฝ๋ณด ํƒ์ง€ 5/5 30/30\n", "๋ฐ˜์‘ ํ‰๊ท h 0.8 0.9\n", "\n", "๋ถ€ํ•˜ 500%: ์‹œ์Šคํ…œ ์ค‘๋‹จ 0๊ฑด(๋ชฉํ‘œ 0๊ฑด: ๋‹ฌ์„ฑ), ๋ฐ์ดํ„ฐ ์œ ์‹ค๋ฅ  0.0%(๋ชฉํ‘œ 0%: ๋‹ฌ์„ฑ)\n", "๋ฉ”๋ชจ๋ฆฌ ๋ง๊ธฐ/์ดˆ๊ธฐ ๋น„์œจ 1.00 (๋ฒ„ํผ 24์‹œ๊ฐ„ ๊ณ ์ •, 1.2 ์ดํ•˜๋ฉด ๋ˆ„์ˆ˜ ์—†์Œ), ๊ฒฝ๋ณด ๋ฐ˜์‘ ์ตœ๋Œ€ 2์‹œ๊ฐ„\n", "๊ธฐ๋Šฅ ์ถ•์†Œ ๋ฏธ์ ์šฉ(์ „์ฒด ์ •์ œ ๊ฐ•ํ–‰) ์ฃผ๊ธฐ ํ‰๊ท  808ms vs ๊ธฐ๋Šฅ ์ถ•์†Œ 88ms (์˜ˆ์‚ฐ 300ms) -> ๋ณ‘๋ชฉ: ๊ธ‰์ˆ˜์›๋ณ„ ์ •์ œ ๋ฐ˜๋ณต(clean ํ•จ์ˆ˜)\n", "Fail-safe ์›์น™: ์†์ƒ ๋ ˆ์ฝ”๋“œ๋Š” ๋ฒ„๋ฆฌ์ง€ ์•Š๊ณ  ๊ฒฉ๋ฆฌ ๋ณด๊ด€, ํŒ์ •์€ ์ง์ „ ์ •์ƒ๊ฐ’์œผ๋กœ ์ด์–ด๊ฐ€๋ฉฐ, ๊ณผ๋ถ€ํ•˜ ์‹œ ์ •์ œยทํŠน์ง• ๊ณ„์‚ฐ์„ ๋ฏธ๋ฃจ๊ณ  ๊ฒฝ๋ณด ํŒ์ •์€ ์œ ์ง€\n", "```\n", "
\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "---\n", "\n", "## ๐ŸŽฏ ๋ฏธ์…˜ 09. AAR ๊ธฐ๋ฐ˜ ์ˆ˜์งˆ ์ด์ƒ ์˜คํŒ ์‚ฌ๋ก€ ๋ถ„์„ ๋ฐ ์ˆ˜์งˆ ์œ„ํ—˜ ํŒ๋‹จ ์ž„๊ณ„์น˜ ์žฌ์กฐ์ •\n", "**์†Œ์š”์‹œ๊ฐ„**: ์•ฝ 3.0์‹œ๊ฐ„\n", "\n", "### ๐Ÿ“‹ ๋ฏธ์…˜ ๋ฐฐ๊ฒฝ ๋ฐ ์ˆ˜ํ–‰ ์ง€์นจ\n", "#### ๋ฏธ์…˜ ๋ฐฐ๊ฒฝ ๋ฐ ๋‹น์œ„์„ฑ\n", "\n", "AAR ๊ธฐ๋ฐ˜ ์ˆ˜์งˆ ์ด์ƒ ์˜คํŒ ์‚ฌ๋ก€ ๋ถ„์„ ๋ฐ ํŒ๋‹จ ์ž„๊ณ„์น˜๋ฅผ ์žฌ์กฐ์ •ํ•œ๋‹ค. ์‹ค์ œ ์ „์ˆ  ํ˜„์žฅ์—์„œ๋Š” ๋‹จ์ˆœ ํƒ์ง€์— ๊ทธ์น˜์ง€ ์•Š๊ณ  ์‹ ์†ํ•œ ์œ„ํ˜‘ ํŒ๋‹จ๊ณผ ์‹ ๋ขฐ๋„ ๋†’์€ ์˜์‚ฌ๊ฒฐ์ •์ด ํ•„์ˆ˜์ ์ด๋ฏ€๋กœ, **AAR๋กœ ์ˆ˜์งˆ ์ด์ƒ ์˜คํŒ ์‚ฌ๋ก€๋ฅผ ๋ถ„์„ํ•˜๊ณ  ํŒ๋‹จ ์ž„๊ณ„์น˜๋ฅผ ์žฌ์กฐ์ •ํ•ด ํšจ๊ณผ๋ฅผ ๋ณ„๋„ ๊ฒ€์ฆ ์„ธํŠธ๋กœ ํ™•์ธํ•œ๋‹ค.** ๊ณผ์—…์„ ์™„์ˆ˜ํ•˜์—ฌ ์˜คํŒ์„ ๋ฐฉ์ง€ํ•˜๊ณ  ์ž‘์ „ ๋Œ€์‘๋ ฅ์„ ๊ทน๋Œ€ํ™”ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.\n", "\n", "์ด ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด ๋ณธ ๋ฏธ์…˜์—์„œ๋Š” **ํŠœ๋‹์šฉ ๊ฐ€์ƒ ์ž๋ฃŒ์—์„œ Baseline ์˜คํŒ์„ ์˜ค๊ฒฝ๋ณด(์„ผ์„œ ๋“œ๋ฆฌํ”„ํŠธ, ๋‹จ๋ฐœ ๋Œ€์žฅ๊ท ), ๊ฒฝ๊ณ„ ํ˜ผ๋™, ๋†“์นจ(ํ™”ํ•™, ์ค‘๊ธˆ์† ์™„๋งŒ ์ฆ๊ฐ€) ๋“ฑ ์›์ธ๋ณ„๋กœ ๋ถ„๋ฅ˜ ์ˆ˜ํ–‰ํ•˜๊ณ  ๊ฒ€์ฆํ•˜๋Š”** ์‹ค์Šต์„ ์ฒด๊ณ„์ ์œผ๋กœ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค.\n", "\n", "ํ•™์Šต์ž๋Š” ์‹ค์Šต๊ฐ€์ด๋“œ์— ์ œ์‹œ๋œ ์ด 4๊ฐœ ์„ธ๋ถ€ ํ™œ๋™(๊ธฐ๋ณธ ๊ธฐ๋Šฅ ๊ตฌํ˜„ โž” ๊ฒฐ๊ณผ ์ง€ํ‘œ ๊ฒ€์ฆ โž” ํŒŒ๋ผ๋ฏธํ„ฐ ํŠœ๋‹ ๋ฐ ๋„์ „๊ณผ์ œ)์„ ์ˆœ์ฐจ์ ์œผ๋กœ ์ง„ํ–‰ํ•˜๋ฉฐ ํ•ต์‹ฌ ํŒ์ • ๊ธฐ์ค€์„ ๋‹ฌ์„ฑํ•ฉ๋‹ˆ๋‹ค.\n", "\n", "**์ด๋ฒˆ ๊ฒฐ๊ณผ๋ฌผ:** `docs/wel03_aar_report.md ๋ฐ configs/wel03_tuned_params.yaml` ํ˜•ํƒœ์˜ ๊ฒฐ๊ณผ๋ฌผ์„ ์™„์„ฑํ•œ๋‹ค.\n", "\n", "### ๐Ÿ” ํ‰๊ฐ€ ๋ฐ ๊ฒ€์ฆ ๊ธฐ์ค€ (PASS ์กฐ๊ฑด)\n", "#### ๋ฏธ์…˜ ํŒ์ •\n", "\n", "**ํ‰๊ฐ€ ๊ตฌ๋ถ„:** ํ•„์ˆ˜\n", "\n", "**ํ•ต์‹ฌ ํ‰๊ฐ€์˜์—ญ:** ์˜ค๋ฅ˜ ์›์ธ ๊ทœ๋ช…์˜ ๊ฐ๊ด€์„ฑ ๋ฐ ๊ฐœ์„  ์กฐ์น˜์˜ ๋…ผ๋ฆฌ์  ํƒ€๋‹น์„ฑ\n", "\n", "#### ๋ฏธ์…˜ PASS ์กฐ๊ฑด\n", "\n", "- ์ •๋Ÿ‰ ๊ธฐ์ค€: AAR ํ”ผ๋“œ๋ฐฑ ์ ์šฉ ํ›„ ์ข…ํ•ฉ ์ •ํ™•๋„ 10%p ์ด์ƒ ํ–ฅ์ƒ, ์žฌ๋ฐœ ์˜ค๋ฅ˜ 0๊ฑด\n", "\n", "\n", "- ์ง„ํ–‰ ๊ธฐ์ค€: AAR ๋ณด๊ณ ์„œ ์ž‘์„ฑ ๋ฐ ํŒŒ๋ผ๋ฏธํ„ฐ ํŠœ๋‹ ์ „ํ›„ ํšŒ๊ท€ ํ…Œ์ŠคํŠธ ํ†ต๊ณผ\n", "\n", "\n", "- ๊ฒ€์ฆ ๋ฐฉ๋ฒ•: ํšŒ๊ท€ ํ…Œ์ŠคํŠธ(Regression Test) ์Šค์œ„ํŠธ ์ „์ฒด ์ž๋™ ์‹คํ–‰ ๋ฐ ๋น„๊ต\n", "\n", "\n", "#### ์ฑ„์ ์ž ํŒ์ • ๋ฉ”๋ชจ\n", "\n", "> ๋‹จ์ˆœ ์ฝ”๋“œ ์‹คํ–‰ ์—ฌ๋ถ€๋ฟ๋งŒ ์•„๋‹ˆ๋ผ ์ถœ๋ ฅ๋œ ์ˆ˜์น˜์™€ ๊ทผ๊ฑฐ ์ž๋ฃŒ๊ฐ€ ๊ตฐ ์‹ค๋ฌด ์š”๊ตฌ์‚ฌํ•ญ์— ๋ถ€ํ•ฉํ•˜๋Š”์ง€ ๋ฉด๋ฐ€ํžˆ ํ™•์ธํ•œ๋‹ค.\n", "\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "\"\"\"AAR(์‚ฌํ›„ ๊ฒ€ํ† )๋กœ ๊ธฐ์ค€์น˜ Baseline์˜ ์ˆ˜์งˆ ์ด์ƒ ์˜คํŒ ์‚ฌ๋ก€๋ฅผ ์›์ธ๋ณ„๋กœ ๋ถ„๋ฅ˜ํ•˜๊ณ , ๋‹จ๋ฐœ ๋Œ€์žฅ๊ท  ๊ฒ€์ถœยท์™„๋งŒํ•œ ์ค‘๊ธˆ์† ์ฆ๊ฐ€์— ๋Œ€ํ•œ ํŒ๋‹จ ์ž„๊ณ„์น˜๋ฅผ ํŠœ๋‹ ์„ธํŠธ์—์„œ ์žฌ์กฐ์ •ํ•œ ๋’ค ๊ฒ€์ฆ ์„ธํŠธยท๋ฒค์น˜๋งˆํฌ ํšŒ๊ท€ ํ…Œ์ŠคํŠธ๋กœ ์ „ํ›„๋ฅผ ๋น„๊ตํ•˜๊ณ  ์กฐ์ • ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ YAML๋กœ ์ถœ๋ ฅํ•œ๋‹ค.\"\"\"\n", "import itertools\n", "import numpy as np\n", "import pandas as pd\n", "import matplotlib.pyplot as plt\n", "\n", "# ---------------------------------------------------------------------------\n", "# [๊ต์œก์šฉ๊ฐ€์ƒ์ž๋ฃŒ] ์•ผ์ „ ๊ธ‰์ˆ˜์› ๊ฐ€์ƒ ์ˆ˜์งˆ ์„ผ์„œ ๋ฐ์ดํ„ฐ ์ƒ์„ฑ๊ธฐ (์‹ค์ œ ๋ถ€๋Œ€ยท์œ„์น˜ยท๊ฒ€์‚ฌ ๊ฒฐ๊ณผ ์—†์Œ)\n", "# ๊ฐ€์ƒ ๊ธ‰์ˆ˜์› 8๊ณณ x 14์ผ x 1์‹œ๊ฐ„ ๊ฐ„๊ฒฉ. ์‹ค์ œ ์ˆ˜์งˆ(true_*)ยท์„ผ์„œ ์ธก์ •๊ฐ’ยท์˜ค์—ผ ์‚ฌ๊ฑดยท์„ผ์„œ ๊ณ ์žฅ ์ •๋‹ต์„ ํ•จ๊ป˜ ๋งŒ๋“ ๋‹ค.\n", "# ๊ธฐ์ค€์น˜๋Š” ๊ต์œก์šฉ ๊ฐ€์ •๊ฐ’์ด๋ฉฐ, ์‹ค์ œ ์Œ์šฉ ํŒ์ •์€ ๊ณต์ธ ๊ฒ€์‚ฌ ๊ฒฐ๊ณผ์™€ ์˜๋ฌด(์˜ˆ๋ฐฉ์˜ํ•™) ๋‹ด๋‹น ๊ธฐ์ค€์— ๋”ฐ๋ฅธ๋‹ค.\n", "# ---------------------------------------------------------------------------\n", "PARAMS = [\"ph\", \"turbidity\", \"chlorine\", \"metal\", \"ecoli\"]\n", "SOURCES = [(\"W01\", \"water_truck\", 1.0, 2.0), (\"W02\", \"water_truck\", 3.5, 1.2), (\"W03\", \"purification_unit\", 2.2, 4.1),\n", " (\"W04\", \"purification_unit\", 5.0, 3.3), (\"W05\", \"well\", 0.8, 5.6), (\"W06\", \"well\", 4.4, 6.0),\n", " (\"W07\", \"river_intake\", 6.3, 1.8), (\"W08\", \"river_intake\", 6.9, 4.9)]\n", "BASE = {\"water_truck\": (7.2, 0.25, 0.75, 1.5), \"purification_unit\": (7.0, 0.20, 0.85, 1.2),\n", " \"well\": (6.8, 0.45, 0.50, 3.0), \"river_intake\": (7.3, 0.55, 0.60, 2.0)} # pH, ํƒ๋„(NTU), ์ž”๋ฅ˜์—ผ์†Œ(mg/L), ์ค‘๊ธˆ์†(ug/L)\n", "LIMITS = {\"ph\": (5.8, 8.5), \"turbidity\": (1.0, 2.0), \"chlorine\": (0.2, 0.1), \"metal\": (5.0, 10.0), \"ecoli\": (1, 1)} # (์ฃผ์˜, ์œ„ํ—˜) ๊ฐ€์ • ๊ธฐ์ค€\n", "EVENT_TYPES = { # ์˜ค์—ผ ์‚ฌ๊ฑด: ๋ณ€์ˆ˜๋ณ„ ์ตœ๋Œ€ ๋ณ€ํ™”๋Ÿ‰, ์ตœ๋Œ€์น˜ ๋„๋‹ฌ ์‹œ๊ฐ„(h)\n", " \"sewage\": ({\"turbidity\": 2.8, \"chlorine\": -0.5, \"ecoli\": 30, \"ph\": -0.3}, 4),\n", " \"metal\": ({\"metal\": 22.0, \"ph\": -0.5}, 4),\n", " \"disinfect_fail\": ({\"chlorine\": -0.8, \"ecoli\": 6}, 6),\n", " \"chemical\": ({\"ph\": 1.7, \"turbidity\": 1.0}, 3),\n", " \"creeping_metal\": ({\"metal\": 13.0}, 18),\n", "}\n", "FAULT_TYPES = [\"stuck\", \"spike\", \"drift\", \"dropout\"]\n", "NOISE = {\"ph\": 0.04, \"turbidity\": 0.06, \"chlorine\": 0.03, \"metal\": 0.35}\n", "DIGITS = {\"ph\": 2, \"turbidity\": 2, \"chlorine\": 2, \"metal\": 1}\n", "\n", "\n", "def make_dataset(days=14, seed=7, n_events=6, n_faults=10, events=None, faults=None, sources=None):\n", " \"\"\"events: [(๊ธ‰์ˆ˜์› ๋ฒˆํ˜ธ, ์‚ฌ๊ฑด ์œ ํ˜•, ์‹œ์ž‘ ์‹œ๊ฐ h, ์ง€์† h, ๊ฐ•๋„)], faults: [(๊ธ‰์ˆ˜์› ๋ฒˆํ˜ธ, ๊ณ ์žฅ ์œ ํ˜•, ํ•ญ๋ชฉ, ์‹œ์ž‘ h, ์ง€์† h)]\"\"\"\n", " rng = np.random.default_rng(seed)\n", " sources = sources or SOURCES\n", " T = days * 24\n", " tt = np.arange(T)\n", " hour = tt % 24\n", " night = (hour < 6) | (hour >= 20)\n", " rain = np.zeros(T)\n", " for _ in range(max(1, days // 4)):\n", " s0, d = int(rng.integers(0, T - 14)), int(rng.integers(6, 14))\n", " rain[s0:s0 + d] = np.maximum(rain[s0:s0 + d], rng.uniform(0.6, 1.0))\n", " if events is None:\n", " events = [(int(rng.integers(len(sources))), str(rng.choice(list(EVENT_TYPES))), int(rng.integers(min(24, T // 4), max(T // 4 + 1, T - 48))),\n", " int(rng.integers(10, 30)), 1.0) for _ in range(n_events)]\n", " if faults is None:\n", " faults = [(int(rng.integers(len(sources))), str(rng.choice(FAULT_TYPES)), str(rng.choice([\"ph\", \"turbidity\", \"chlorine\", \"metal\"])),\n", " int(rng.integers(12, max(13, T - 30))), int(rng.integers(6, 24))) for _ in range(n_faults)]\n", " out = []\n", " for si, (sid, stype, x, y) in enumerate(sources):\n", " ph0, tb0, cl0, mt0 = BASE[stype]\n", " tr = {\"ph\": ph0 + 0.08 * np.sin(2 * np.pi * hour / 24) + rng.normal(0, 0.03, T),\n", " \"turbidity\": tb0 * (1 + 0.1 * rng.normal(0, 1, T)),\n", " \"chlorine\": (cl0 * np.exp(-0.022 * hour) if stype == \"water_truck\" else cl0 + rng.normal(0, 0.03, T)),\n", " \"metal\": mt0 + rng.normal(0, 0.2, T), \"ecoli\": np.zeros(T)}\n", " if stype in (\"river_intake\", \"well\"):\n", " tr[\"turbidity\"] = tr[\"turbidity\"] + rain * (0.9 if stype == \"river_intake\" else 0.45)\n", " f_all, etype = np.zeros(T), np.array([\"none\"] * T, dtype=object)\n", " for (s_i, et, onset, dur, scale) in events:\n", " if s_i != si:\n", " continue\n", " delta, ramp = EVENT_TYPES[et]\n", " f = np.clip((tt - onset + 1) / ramp, 0, 1) * ((tt >= onset) & (tt < onset + dur + ramp))\n", " for k, v in delta.items():\n", " tr[k] = tr[k] + v * f * scale\n", " etype[f > 0] = et\n", " f_all = np.maximum(f_all, f)\n", " tr[\"chlorine\"] = np.clip(tr[\"chlorine\"], 0.02, None)\n", " tr[\"turbidity\"] = np.clip(tr[\"turbidity\"], 0.05, None)\n", " tr[\"metal\"] = np.clip(tr[\"metal\"], 0.1, None)\n", " grade = np.where(f_all >= 0.2, 2, 0)\n", " mild = (tr[\"turbidity\"] > LIMITS[\"turbidity\"][0]) | (tr[\"chlorine\"] < LIMITS[\"chlorine\"][0]) | (tr[\"metal\"] > LIMITS[\"metal\"][0])\n", " grade = np.where((grade == 0) & mild, 1, grade)\n", " ms = {}\n", " for p in [\"ph\", \"turbidity\", \"chlorine\", \"metal\"]:\n", " sd = NOISE[p] * np.where(night, 1.5, 1.0) * (tr[p] if p == \"turbidity\" else 1)\n", " ms[p] = np.round(np.clip(tr[p] + rng.normal(0, 1, T) * sd, 0, None), DIGITS[p])\n", " ms[\"ecoli\"] = (rng.poisson(tr[\"ecoli\"]) + (rng.random(T) < 0.004)).astype(float)\n", " fault, fparam = np.array([\"none\"] * T, dtype=object), np.array([\"-\"] * T, dtype=object)\n", " for (s_i, ft, p, s0, d) in faults:\n", " if s_i != si:\n", " continue\n", " seg = slice(s0, min(T, s0 + d))\n", " if ft == \"stuck\":\n", " ms[p][seg] = ms[p][s0]\n", " elif ft == \"spike\":\n", " seg = slice(s0, s0 + 1)\n", " ms[p][s0] = {\"ph\": 11.8, \"turbidity\": 38.0, \"chlorine\": 0.0, \"metal\": 75.0}[p]\n", " elif ft == \"drift\":\n", " ramp = np.linspace(0, 1, seg.stop - s0)\n", " ms[p][seg] = np.clip(ms[p][seg] + ramp * {\"ph\": -1.6, \"turbidity\": 2.6, \"chlorine\": -0.75, \"metal\": 12.0}[p], 0, None).round(DIGITS[p])\n", " elif ft == \"dropout\":\n", " for q in PARAMS:\n", " ms[q][seg] = np.nan\n", " p = \"all\"\n", " fault[seg], fparam[seg] = ft, p\n", " df = pd.DataFrame({\"source_id\": sid, \"source_type\": stype, \"x_km\": x, \"y_km\": y, \"t\": tt,\n", " \"cond\": np.where(rain > 0, \"storm\", np.where(night, \"night\", \"day\")),\n", " **{p: ms[p] for p in PARAMS}, **{f\"true_{p}\": tr[p] for p in PARAMS},\n", " \"event\": etype, \"fault\": fault, \"fault_param\": fparam, \"grade\": grade})\n", " out.append(df)\n", " return pd.concat(out, ignore_index=True)\n", "# ---------------------------------------------------------------------------\n", "\n", "# ---- ์ „์ฒ˜๋ฆฌ ๊ณตํ†ต ํ•จ์ˆ˜: ๋ฌผ๋ฆฌ ๋ฒ”์œ„ ์ ๊ฒ€ -> ๊ณ ์ •๊ฐ’(stuck) ํ‘œ์‹œ -> ๋‹จ๋ฐœ ์ŠคํŒŒ์ดํฌ ์ œ๊ฑฐ -> ๊ฒฐ์ธก ๋ณด์ • ----\n", "PHYS = {\"ph\": (0, 14), \"turbidity\": (0, 1000), \"chlorine\": (0, 10), \"metal\": (0, 1000), \"ecoli\": (0, 100000)}\n", "SPIKE_K = {\"day\": 6.0, \"night\": 7.0, \"storm\": 9.0} # ์ƒํ™ฉ๋ณ„(์ฃผ๊ฐ„/์•ผ๊ฐ„/์•…์ฒœํ›„) ์ŠคํŒŒ์ดํฌ ํŒ์ • ๋ฐฐ์ˆ˜\n", "MIN_JUMP = {\"ph\": 0.8, \"turbidity\": 1.5, \"chlorine\": 0.3, \"metal\": 6.0}\n", "STUCK_RUN, SHORT_GAP = 6, 3\n", "\n", "\n", "def clean(df, spike_k=None, min_jump=None):\n", " \"\"\"๊ธ‰์ˆ˜์›๋ณ„ ์‹œ๊ณ„์—ด์„ ์ •์ œํ•˜๊ณ  ํ•ญ๋ชฉ๋ณ„ ์ฒ˜๋ฆฌ ๋‚ด์—ญ(<ํ•ญ๋ชฉ>_flag: range/stuck/spike/interp/long_gap)์„ ๋‚จ๊ธด๋‹ค.\"\"\"\n", " spike_k = spike_k or SPIKE_K\n", " min_jump = MIN_JUMP if min_jump is None else min_jump\n", " out = df.sort_values([\"source_id\", \"t\"]).reset_index(drop=True)\n", " for sid, idx in out.groupby(\"source_id\").groups.items():\n", " idx = np.asarray(idx)\n", " kk = out.loc[idx, \"cond\"].map(spike_k).to_numpy(float)\n", " for p in PARAMS:\n", " v = out.loc[idx, p].to_numpy(float).copy()\n", " fl = np.array([\"\"] * len(v), dtype=object)\n", " lo, hi = PHYS[p]\n", " bad = (v < lo) | (v > hi)\n", " v[bad], fl[bad] = np.nan, \"range\"\n", " if p != \"ecoli\":\n", " chg = np.r_[True, v[1:] != v[:-1]] # ๊ฐ™์€ ๊ฐ’์ด STUCK_RUN์‹œ๊ฐ„ ์ด์ƒ ๋ฐ˜๋ณต = ๊ณ ์ •๊ฐ’ ๊ณ ์žฅ ์˜์‹ฌ\n", " rid = np.cumsum(chg)\n", " rlen = np.bincount(rid)[rid]\n", " fl[(rlen >= STUCK_RUN) & ~chg & ~np.isnan(v)] = \"stuck\"\n", " d = np.abs(np.diff(v[~np.isnan(v)]))\n", " sig = 1.4826 * np.median(d) + 1e-6 if len(d) else 1.0\n", " up, dn = v - np.r_[np.nan, v[:-1]], v - np.r_[v[1:], np.nan]\n", " thr = np.maximum(kk * sig, min_jump[p])\n", " spike = (np.abs(up) > thr) & (np.abs(dn) > thr) & (np.sign(up) == np.sign(dn)) # ํŠ€์—ˆ๋‹ค๊ฐ€ ๋ฐ”๋กœ ๋Œ์•„์˜จ ๊ฐ’๋งŒ\n", " v[spike], fl[spike] = np.nan, \"spike\"\n", " s = pd.Series(v)\n", " na = s.isna().to_numpy()\n", " grp = np.cumsum(np.r_[True, na[1:] != na[:-1]])\n", " glen = pd.Series(na.astype(int)).groupby(grp).transform(\"sum\").to_numpy()\n", " lin, ff = s.interpolate(limit_area=\"inside\").to_numpy(), s.ffill().bfill().to_numpy()\n", " short, longg = na & (glen <= SHORT_GAP), na & (glen > SHORT_GAP)\n", " v = np.where(short, np.where(np.isnan(lin), ff, lin), np.where(longg, ff, v))\n", " fl[short & (fl == \"\")] = \"interp\"\n", " fl[longg] = \"long_gap\"\n", " out.loc[idx, p] = v\n", " out.loc[idx, f\"{p}_flag\"] = fl\n", " return out\n", "\n", "# ---- ๊ฐ€์ • ๊ธฐ์ค€์น˜ ํŒ์ •(Baseline)๊ณผ ์‹œ๊ณ„์—ด ํŠน์ง• ๊ณตํ†ต ํ•จ์ˆ˜ ----\n", "GRADE_NAME = {0: \"์•ˆ์ „\", 1: \"์ฃผ์˜\", 2: \"์œ„ํ—˜\"}\n", "\n", "\n", "def rule_grade(df):\n", " \"\"\"๊ธฐ์ค€์น˜ 1์ฐจ ํ•„ํ„ฐ: ์œ„ํ—˜(์Œ์šฉ ๋ถ€์ ํ•ฉ ์˜์‹ฌ) = ํ•œ ํ•ญ๋ชฉ์ด๋ผ๋„ ์œ„ํ—˜ ๊ธฐ์ค€ ์ดˆ๊ณผ, ์ฃผ์˜ = ์ฃผ์˜ ๊ธฐ์ค€ ์ดˆ๊ณผ.\"\"\"\n", " ph, tb, cl, mt, ec = (df[p].to_numpy(float) for p in PARAMS)\n", " danger = (ph < LIMITS[\"ph\"][0]) | (ph > LIMITS[\"ph\"][1]) | (tb > LIMITS[\"turbidity\"][1]) | (cl < LIMITS[\"chlorine\"][1]) \\\n", " | (mt > LIMITS[\"metal\"][1]) | (ec >= LIMITS[\"ecoli\"][1])\n", " caution = (tb > LIMITS[\"turbidity\"][0]) | (cl < LIMITS[\"chlorine\"][0]) | (mt > LIMITS[\"metal\"][0])\n", " return np.where(danger, 2, np.where(caution, 1, 0))\n", "\n", "\n", "def reasons(row):\n", " r = []\n", " if not LIMITS[\"ph\"][0] <= row[\"ph\"] <= LIMITS[\"ph\"][1]:\n", " r.append(f\"pH {row['ph']:.2f}\")\n", " if row[\"turbidity\"] > LIMITS[\"turbidity\"][0]:\n", " r.append(f\"ํƒ๋„ {row['turbidity']:.2f}NTU\")\n", " if row[\"chlorine\"] < LIMITS[\"chlorine\"][0]:\n", " r.append(f\"์ž”๋ฅ˜์—ผ์†Œ {row['chlorine']:.2f}mg/L\")\n", " if row[\"metal\"] > LIMITS[\"metal\"][0]:\n", " r.append(f\"์ค‘๊ธˆ์† {row['metal']:.1f}ug/L\")\n", " if row[\"ecoli\"] >= 1:\n", " r.append(f\"๋Œ€์žฅ๊ท  {row['ecoli']:.0f}\")\n", " return \", \".join(r) or \"-\"\n", "\n", "\n", "FEATS = [f\"{p}{s}\" for p in [\"ph\", \"turbidity\", \"chlorine\", \"metal\"] for s in [\"\", \"_d1\", \"_d3\", \"_dev\", \"_sd6\"]] + \\\n", " [\"ecoli\", \"ecoli_3h\", \"storm\", \"night\", \"n_flag\", \"truck\", \"purif\", \"well\", \"river\"]\n", "\n", "\n", "def add_features(c):\n", " \"\"\"์‹œ๊ฐ„ ์ˆœ์„œ๋ฅผ ์ง€ํ‚ค๋Š”(๊ณผ๊ฑฐ ๊ฐ’๋งŒ ์“ฐ๋Š”) ๊ธ‰์ˆ˜์›๋ณ„ ํŠน์ง•: ๋ณ€ํ™”๋Ÿ‰, 24์‹œ๊ฐ„ ๊ธฐ์ค€์„  ๋Œ€๋น„ ํŽธ์ฐจ, 6์‹œ๊ฐ„ ๋ณ€๋™์„ฑ, ๋Œ€์žฅ๊ท  ๋ˆ„์  ๋“ฑ.\"\"\"\n", " c = c.sort_values([\"source_id\", \"t\"]).reset_index(drop=True)\n", " g = c.groupby(\"source_id\")\n", " for p in [\"ph\", \"turbidity\", \"chlorine\", \"metal\"]:\n", " c[f\"{p}_d1\"] = g[p].diff().fillna(0)\n", " c[f\"{p}_d3\"] = g[p].diff(3).fillna(0)\n", " base = g[p].transform(lambda s: s.shift(1).rolling(24, min_periods=4).median())\n", " c[f\"{p}_dev\"] = (c[p] - base).fillna(0)\n", " c[f\"{p}_sd6\"] = g[p].transform(lambda s: s.rolling(6, min_periods=2).std()).fillna(0)\n", " c[\"ecoli_3h\"] = g[\"ecoli\"].transform(lambda s: s.rolling(3, min_periods=1).sum())\n", " c[\"storm\"], c[\"night\"] = (c[\"cond\"] == \"storm\").astype(int), (c[\"cond\"] == \"night\").astype(int)\n", " flags = [f\"{p}_flag\" for p in PARAMS if f\"{p}_flag\" in c]\n", " c[\"n_flag\"] = sum(c[f].isin([\"stuck\", \"spike\", \"long_gap\", \"range\"]).astype(int) for f in flags) if flags else 0\n", " for k, name in [(\"truck\", \"water_truck\"), (\"purif\", \"purification_unit\"), (\"well\", \"well\"), (\"river\", \"river_intake\")]:\n", " c[k] = (c[\"source_type\"] == name).astype(int)\n", " return c\n", "\n", "BEFORE = {\"ecoli_3h_min\": 1, \"metal_dev\": 99.0, \"metal_min\": 99.0} # ํ˜„ํ–‰: ๋Œ€์žฅ๊ท  1ํšŒ ๊ฒ€์ถœ ์ฆ‰์‹œ ์œ„ํ—˜, ์ถ”์„ธ ๊ทœ์น™ ์—†์Œ\n", "\n", "\n", "def tuned_grade(c, P):\n", " \"\"\"์žฌ์กฐ์ • ๊ทœ์น™: ๋Œ€์žฅ๊ท ์€ 3์‹œ๊ฐ„ ๋ˆ„์  ๊ธฐ์ค€(๋˜๋Š” ํƒ๋„ยท์—ผ์†Œ ์ด์ƒ ๋™๋ฐ˜ ์‹œ ์ฆ‰์‹œ), ์ค‘๊ธˆ์†์€ ๊ธฐ์ค€์„  ๋Œ€๋น„ ์ƒ์Šน ์ถ”์„ธ ๊ทœ์น™ ์ถ”๊ฐ€.\"\"\"\n", " ph, tb, cl, mt, ec = (c[p].to_numpy(float) for p in PARAMS)\n", " if P[\"ecoli_3h_min\"] <= 1: # ํ˜„ํ–‰ ๊ทœ์น™: ๊ทธ ์‹œ๊ฐ 1ํšŒ ๊ฒ€์ถœ์ด๋ฉด ์œ„ํ—˜\n", " eco = ec >= 1\n", " else:\n", " eco = (c[\"ecoli_3h\"].to_numpy() >= P[\"ecoli_3h_min\"]) | ((ec >= 1) & ((tb > LIMITS[\"turbidity\"][0]) | (cl < LIMITS[\"chlorine\"][0])))\n", " trend = (c[\"metal_dev\"].to_numpy() > P[\"metal_dev\"]) & (mt > P[\"metal_min\"])\n", " danger = (ph < LIMITS[\"ph\"][0]) | (ph > LIMITS[\"ph\"][1]) | (tb > LIMITS[\"turbidity\"][1]) | (cl < LIMITS[\"chlorine\"][1]) \\\n", " | (mt > LIMITS[\"metal\"][1]) | eco | trend\n", " caution = (tb > LIMITS[\"turbidity\"][0]) | (cl < LIMITS[\"chlorine\"][0]) | (mt > LIMITS[\"metal\"][0]) | (ec >= 1)\n", " return np.where(danger, 2, np.where(caution, 1, 0))\n", "\n", "\n", "def cause(r, pred):\n", " if r[\"grade\"] == 2 and pred != 2:\n", " return {\"creeping_metal\": \"๋†“์นจ-์ค‘๊ธˆ์† ์™„๋งŒ ์ฆ๊ฐ€\", \"chemical\": \"๋†“์นจ-ํ™”ํ•™(์ดˆ๊ธฐ pH ๋ณ€ํ™”)\"}.get(r[\"event\"], f\"๋†“์นจ-{r['event']}\")\n", " if r[\"grade\"] != 2 and pred == 2:\n", " if r[\"fault\"] != \"none\":\n", " return f\"์˜ค๊ฒฝ๋ณด-์„ผ์„œ {r['fault']}\"\n", " return \"์˜ค๊ฒฝ๋ณด-๋‹จ๋ฐœ ๋Œ€์žฅ๊ท \" if r[\"ecoli\"] >= 1 else \"์˜ค๊ฒฝ๋ณด-๊ธฐํƒ€\"\n", " return \"์ฃผ์˜/์•ˆ์ „ ๊ฒฝ๊ณ„ ํ˜ผ๋™\"\n", "\n", "\n", "D = {name: add_features(clean(make_dataset(seed=s, n_events=10, n_faults=14))) for name, s in\n", " [(\"ํŠœ๋‹(seed31)\", 31), (\"๊ฒ€์ฆ(seed41)\", 41), (\"๋ฒค์น˜๋งˆํฌ(seed11)\", 11)]}\n", "\n", "# 1) AAR: ํŠœ๋‹ ์„ธํŠธ์˜ ์˜คํŒ ์›์ธ ๋ถ„๋ฅ˜\n", "tune = D[\"ํŠœ๋‹(seed31)\"]\n", "p0 = tuned_grade(tune, BEFORE)\n", "err = tune[p0 != tune[\"grade\"]]\n", "causes = pd.Series([cause(r, p) for (_, r), p in zip(err.iterrows(), p0[p0 != tune[\"grade\"]])]).value_counts()\n", "print(f\"[๊ต์œก์šฉ๊ฐ€์ƒ์ž๋ฃŒ] AAR ๋Œ€์ƒ ์˜คํŒ {len(err)}๊ฑด(ํŠœ๋‹ ์„ธํŠธ {len(tune):,}๊ฑด) ์›์ธ ๋ถ„๋ฅ˜\")\n", "print(causes.to_string())\n", "\n", "# 2) ์žฌ์กฐ์ •: ๊ณ ์น  ์ˆ˜ ์žˆ๋Š” ์›์ธ(๋‹จ๋ฐœ ๋Œ€์žฅ๊ท , ์ค‘๊ธˆ์† ์™„๋งŒ ์ฆ๊ฐ€)์— ๋Œ€ํ•œ ์ž„๊ณ„์น˜ ๊ฒฉ์ž ํƒ์ƒ‰\n", "# AAR ๊ฒฐ์ •: ๋Œ€์žฅ๊ท  ๋‹จ๋ฐœ 1ํšŒ ๊ฒ€์ถœ์€ '์ฃผ์˜ + ์žฌ์ฑ„์ˆ˜'๋กœ ๋‚ฎ์ถ”๊ณ (3์‹œ๊ฐ„ ๋ˆ„์  2ํšŒ ์ด์ƒ๋ถ€ํ„ฐ ์œ„ํ—˜), ๊ทธ ๊ธฐ์ค€๊ฐ’๋งŒ ํƒ์ƒ‰ํ•œ๋‹ค\n", "best = None\n", "for e3, md, mm in itertools.product([2, 3], [1.0, 1.5, 2.0, 3.0], [4.0, 5.0, 6.0]):\n", " P = {\"ecoli_3h_min\": e3, \"metal_dev\": md, \"metal_min\": mm}\n", " acc = (tuned_grade(tune, P) == tune[\"grade\"]).mean()\n", " if best is None or acc > best[0]:\n", " best = (acc, P)\n", "AFTER = best[1]\n", "print(\"\\n์žฌ์กฐ์ • ํŒŒ๋ผ๋ฏธํ„ฐ:\", AFTER)\n", "\n", "# 3) ํšŒ๊ท€ ํ…Œ์ŠคํŠธ: ์„ธ ๋ฐ์ดํ„ฐ์…‹์—์„œ ์ „ํ›„ ๋น„๊ต\n", "rows = []\n", "for name, c in D.items():\n", " b, a = tuned_grade(c, BEFORE), tuned_grade(c, AFTER)\n", " d = c[\"grade\"] == 2\n", " rows.append({\"๋ฐ์ดํ„ฐ\": name, \"์ •ํ™•๋„ ์ „\": f\"{(b == c['grade']).mean():.1%}\", \"์ •ํ™•๋„ ํ›„\": f\"{(a == c['grade']).mean():.1%}\",\n", " \"ํ–ฅ์ƒ(%p)\": round(((a == c[\"grade\"]).mean() - (b == c[\"grade\"]).mean()) * 100, 1),\n", " \"์˜คํŒ ์ „โ†’ํ›„\": f\"{int((b != c['grade']).sum())}โ†’{int((a != c['grade']).sum())}\",\n", " \"์ƒˆ๋กœ ํ‹€๋ฆผ\": int(((b == c[\"grade\"]) & (a != c[\"grade\"])).sum()),\n", " \"์œ„ํ—˜ ์žฌํ˜„์œจ ํ›„\": f\"{((a == 2) & d).sum() / d.sum():.1%}\"})\n", "reg = pd.DataFrame(rows)\n", "print(\"\\n[ํšŒ๊ท€ ํ…Œ์ŠคํŠธ]\")\n", "print(reg.to_string(index=False))\n", "gain = reg.loc[1, \"ํ–ฅ์ƒ(%p)\"]\n", "print(f\"๊ฒ€์ฆ ์„ธํŠธ ์ข…ํ•ฉ ์ •ํ™•๋„ ํ–ฅ์ƒ {gain}%p (๋ชฉํ‘œ 10%p ์ด์ƒ: {'๋‹ฌ์„ฑ' if gain >= 10 else '๋ฏธ๋‹ฌ'} - Baseline ์ •ํ™•๋„๊ฐ€ ์ด๋ฏธ 97% ์ˆ˜์ค€์ด๋ผ 10%p ์—ฌ์ง€๊ฐ€ ์—†์Œ)\")\n", "\n", "# 4) ์žฌ๋ฐœ ์˜ค๋ฅ˜: ๊ณ ์นœ ์›์ธ ์œ ํ˜•์ด ๊ฒ€์ฆ ์„ธํŠธ์—์„œ ๋‹ค์‹œ ๋‚˜์˜ค๋Š”๊ฐ€\n", "val = D[\"๊ฒ€์ฆ(seed41)\"]\n", "pa = tuned_grade(val, AFTER)\n", "ev = val[pa != val[\"grade\"]]\n", "vc = pd.Series([cause(r, p) for (_, r), p in zip(ev.iterrows(), pa[pa != val[\"grade\"]])]).value_counts()\n", "fixed_types = [\"์˜ค๊ฒฝ๋ณด-๋‹จ๋ฐœ ๋Œ€์žฅ๊ท \", \"๋†“์นจ-์ค‘๊ธˆ์† ์™„๋งŒ ์ฆ๊ฐ€\"]\n", "rec = {k: int(vc.get(k, 0)) for k in fixed_types}\n", "print(f\"\\n์žฌ๋ฐœ ์˜ค๋ฅ˜(๊ณ ์นœ ์œ ํ˜•์˜ ๊ฒ€์ฆ ์„ธํŠธ ์žฌ๋ฐœ): {rec} -> ํ•ฉ๊ณ„ {sum(rec.values())}๊ฑด(๋ชฉํ‘œ 0๊ฑด: {'๋‹ฌ์„ฑ' if sum(rec.values()) == 0 else '๋ฏธ๋‹ฌ'})\")\n", "print(\"๋‚จ์€ ์›์ธ ์ƒ์œ„:\", vc.head(3).to_dict(), \"-> ์„ผ์„œ ๊ณ ์žฅ(๋“œ๋ฆฌํ”„ํŠธยทํ†ต์‹  ๋‹จ์ ˆ) ์˜ค๊ฒฝ๋ณด๋Š” ๊ต์ฐจ ์„ผ์„œยท๊ต์ • ์ฃผ๊ธฐ์™€ ๋ฏธ์…˜ 5 ์‹ ๋ขฐ๋„ ์ง€์ˆ˜๋กœ, ํ™”ํ•™ ์ดˆ๊ธฐ ๋ณ€ํ™”๋Š” pH ๋ณ€ํ™”์œจ ๊ทœ์น™ ํ›„๋ณด๋กœ ๋‹ค์Œ AAR์— ๋„˜๊ธด๋‹ค\")\n", "\n", "print(\"\\n# configs/wel03_tuned_params.yaml\")\n", "print(f\"ecoli:\\n danger_3h_count: {AFTER['ecoli_3h_min']} # ๋‹จ๋ฐœ 1ํšŒ๋Š” '์ฃผ์˜ + ์žฌ์ฑ„์ˆ˜'\\nmetal_trend:\\n\"\n", " f\" dev_from_24h_baseline_ugL: {AFTER['metal_dev']}\\n min_level_ugL: {AFTER['metal_min']}\\nreviewed_by: ์˜ˆ๋ฐฉ์˜ํ•™ ๋‹ด๋‹น(๊ฐ€์ƒ) # ์‹ค์ œ ๊ธฐ์ค€์€ ๊ณต์‹ ์ˆ˜์งˆ ๊ธฐ์ค€ ํ™•์ธ\")\n", "\n", "fig, ax = plt.subplots(figsize=(8, 3.5))\n", "x = np.arange(len(reg))\n", "ax.bar(x - 0.2, [(tuned_grade(c, BEFORE) != c[\"grade\"]).sum() for c in D.values()], 0.4, label=\"before\", color=\"tab:gray\")\n", "ax.bar(x + 0.2, [(tuned_grade(c, AFTER) != c[\"grade\"]).sum() for c in D.values()], 0.4, label=\"after\", color=\"tab:blue\")\n", "ax.set_xticks(x, [\"tune(31)\", \"valid(41)\", \"bench(11)\"]); ax.set_ylabel(\"misjudged hours\"); ax.legend()\n", "ax.set_title(\"AAR retuning: misjudgments before/after (synthetic)\")\n", "plt.tight_layout(); plt.show()\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "
\n", "๐Ÿ’ก [์ฐธ๊ณ ] ๋ฏธ์…˜ 09 ์˜ˆ์ƒ ์‹คํ–‰ ๊ฒฐ๊ณผ (Output)\n", "\n", "```text\n", "[๊ต์œก์šฉ๊ฐ€์ƒ์ž๋ฃŒ] AAR ๋Œ€์ƒ ์˜คํŒ 110๊ฑด(ํŠœ๋‹ ์„ธํŠธ 2,688๊ฑด) ์›์ธ ๋ถ„๋ฅ˜\n", "์˜ค๊ฒฝ๋ณด-์„ผ์„œ drift 38\n", "์ฃผ์˜/์•ˆ์ „ ๊ฒฝ๊ณ„ ํ˜ผ๋™ 28\n", "๋†“์นจ-ํ™”ํ•™(์ดˆ๊ธฐ pH ๋ณ€ํ™”) 20\n", "๋†“์นจ-์ค‘๊ธˆ์† ์™„๋งŒ ์ฆ๊ฐ€ 16\n", "์˜ค๊ฒฝ๋ณด-๋‹จ๋ฐœ ๋Œ€์žฅ๊ท  7\n", "๋†“์นจ-metal 1\n", "\n", "์žฌ์กฐ์ • ํŒŒ๋ผ๋ฏธํ„ฐ: {'ecoli_3h_min': 2, 'metal_dev': 1.0, 'metal_min': 5.0}\n", "\n", "[ํšŒ๊ท€ ํ…Œ์ŠคํŠธ]\n", " ๋ฐ์ดํ„ฐ ์ •ํ™•๋„ ์ „ ์ •ํ™•๋„ ํ›„ ํ–ฅ์ƒ(%p) ์˜คํŒ ์ „โ†’ํ›„ ์ƒˆ๋กœ ํ‹€๋ฆผ ์œ„ํ—˜ ์žฌํ˜„์œจ ํ›„\n", " ํŠœ๋‹(seed31) 95.9% 96.2% 0.3 110โ†’102 8 91.8%\n", " ๊ฒ€์ฆ(seed41) 97.1% 97.2% 0.1 78โ†’75 6 92.2%\n", "๋ฒค์น˜๋งˆํฌ(seed11) 97.9% 98.4% 0.5 56โ†’43 6 96.3%\n", "๊ฒ€์ฆ ์„ธํŠธ ์ข…ํ•ฉ ์ •ํ™•๋„ ํ–ฅ์ƒ 0.1%p (๋ชฉํ‘œ 10%p ์ด์ƒ: ๋ฏธ๋‹ฌ - Baseline ์ •ํ™•๋„๊ฐ€ ์ด๋ฏธ 97% ์ˆ˜์ค€์ด๋ผ 10%p ์—ฌ์ง€๊ฐ€ ์—†์Œ)\n", "\n", "์žฌ๋ฐœ ์˜ค๋ฅ˜(๊ณ ์นœ ์œ ํ˜•์˜ ๊ฒ€์ฆ ์„ธํŠธ ์žฌ๋ฐœ): {'์˜ค๊ฒฝ๋ณด-๋‹จ๋ฐœ ๋Œ€์žฅ๊ท ': 0, '๋†“์นจ-์ค‘๊ธˆ์† ์™„๋งŒ ์ฆ๊ฐ€': 1} -> ํ•ฉ๊ณ„ 1๊ฑด(๋ชฉํ‘œ 0๊ฑด: ๋ฏธ๋‹ฌ)\n", "๋‚จ์€ ์›์ธ ์ƒ์œ„: {'์ฃผ์˜/์•ˆ์ „ ๊ฒฝ๊ณ„ ํ˜ผ๋™': 21, '์˜ค๊ฒฝ๋ณด-์„ผ์„œ dropout': 18, '๋†“์นจ-ํ™”ํ•™(์ดˆ๊ธฐ pH ๋ณ€ํ™”)': 15} -> ์„ผ์„œ ๊ณ ์žฅ(๋“œ๋ฆฌํ”„ํŠธยทํ†ต์‹  ๋‹จ์ ˆ) ์˜ค๊ฒฝ๋ณด๋Š” ๊ต์ฐจ ์„ผ์„œยท๊ต์ • ์ฃผ๊ธฐ์™€ ๋ฏธ์…˜ 5 ์‹ ๋ขฐ๋„ ์ง€์ˆ˜๋กœ, ํ™”ํ•™ ์ดˆ๊ธฐ ๋ณ€ํ™”๋Š” pH ๋ณ€ํ™”์œจ ๊ทœ์น™ ํ›„๋ณด๋กœ ๋‹ค์Œ AAR์— ๋„˜๊ธด๋‹ค\n", "\n", "# configs/wel03_tuned_params.yaml\n", "ecoli:\n", " danger_3h_count: 2 # ๋‹จ๋ฐœ 1ํšŒ๋Š” '์ฃผ์˜ + ์žฌ์ฑ„์ˆ˜'\n", "metal_trend:\n", " dev_from_24h_baseline_ugL: 1.0\n", " min_level_ugL: 5.0\n", "reviewed_by: ์˜ˆ๋ฐฉ์˜ํ•™ ๋‹ด๋‹น(๊ฐ€์ƒ) # ์‹ค์ œ ๊ธฐ์ค€์€ ๊ณต์‹ ์ˆ˜์งˆ ๊ธฐ์ค€ ํ™•์ธ\n", "```\n", "
\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "---\n", "\n", "## ๐ŸŽฏ ๋ฏธ์…˜ 10. ๊ฐ€์ƒ ์ˆ˜์งˆ๊ฒ€์‚ฌ ๋ฐ์ดํ„ฐ ์œ„ํ—˜ ์‹ ํ˜ธ ๋Œ€์‹œ๋ณด๋“œ ์‹œ์Šคํ…œ ํ†ตํ•ฉ ํŒจํ‚ค์ง€ ์™„์„ฑ\n", "**์†Œ์š”์‹œ๊ฐ„**: ์•ฝ 3.5์‹œ๊ฐ„\n", "\n", "### ๐Ÿ“‹ ๋ฏธ์…˜ ๋ฐฐ๊ฒฝ ๋ฐ ์ˆ˜ํ–‰ ์ง€์นจ\n", "#### ๋ฏธ์…˜ ๋ฐฐ๊ฒฝ ๋ฐ ๋‹น์œ„์„ฑ\n", "\n", "๊ฐ€์ƒ ์ˆ˜์งˆ๊ฒ€์‚ฌ ๋ฐ์ดํ„ฐ ์œ„ํ—˜ ์‹ ํ˜ธ ๋Œ€์‹œ๋ณด๋“œ ์‹œ์Šคํ…œ ํ†ตํ•ฉ ํŒจํ‚ค์ง€๋ฅผ ์™„์„ฑํ•œ๋‹ค. ์‹ค์ œ ์ „์ˆ  ํ˜„์žฅ์—์„œ๋Š” ๋‹จ์ˆœ ํƒ์ง€์— ๊ทธ์น˜์ง€ ์•Š๊ณ  ์‹ ์†ํ•œ ์œ„ํ˜‘ ํŒ๋‹จ๊ณผ ์‹ ๋ขฐ๋„ ๋†’์€ ์˜์‚ฌ๊ฒฐ์ •์ด ํ•„์ˆ˜์ ์ด๋ฏ€๋กœ, **๊ฐ€์ƒ ์ˆ˜์งˆ๊ฒ€์‚ฌ ๋ฐ์ดํ„ฐ ์œ„ํ—˜ ์‹ ํ˜ธ ๋Œ€์‹œ๋ณด๋“œ๋ฅผ ํ•˜๋‚˜์˜ ํ†ตํ•ฉ ํŒจํ‚ค์ง€๋กœ ์™„์„ฑํ•œ๋‹ค.** ๊ณผ์—…์„ ์™„์ˆ˜ํ•˜์—ฌ ์˜คํŒ์„ ๋ฐฉ์ง€ํ•˜๊ณ  ์ž‘์ „ ๋Œ€์‘๋ ฅ์„ ๊ทน๋Œ€ํ™”ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.\n", "\n", "์ด ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด ๋ณธ ๋ฏธ์…˜์—์„œ๋Š” **์ž…๋ ฅ ๊ฒ€์ฆ, ์ „์ฒ˜๋ฆฌ, ์žฌ์กฐ์ • ๊ธฐ์ค€์น˜ ํŒ์ •, ๋žœ๋ค ํฌ๋ ˆ์ŠคํŠธ ์œ„ํ—˜ ๋ถ„๋ฅ˜, ์‹ ๋ขฐ๋„ ๋“ฑ๊ธ‰, ์‚ฌ๋žŒ ๊ฒ€ํ†  ํ ๋“ฑ๋ก, ์ƒํ™ฉํŒ ์š”์•ฝ์„ ํ•œ ํŒŒ์ผ์˜ ํŒŒ์ดํ”„๋ผ์ธ์œผ๋กœ ๋ฌถ๋Š”๋‹ค ๊ณผ์ •์„ ์ง„ํ–‰ํ•˜๋Š”** ์‹ค์Šต์„ ์ฒด๊ณ„์ ์œผ๋กœ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค.\n", "\n", "ํ•™์Šต์ž๋Š” ์‹ค์Šต๊ฐ€์ด๋“œ์— ์ œ์‹œ๋œ ์ด 4๊ฐœ ์„ธ๋ถ€ ํ™œ๋™(๊ธฐ๋ณธ ๊ธฐ๋Šฅ ๊ตฌํ˜„ โž” ๊ฒฐ๊ณผ ์ง€ํ‘œ ๊ฒ€์ฆ โž” ํŒŒ๋ผ๋ฏธํ„ฐ ํŠœ๋‹ ๋ฐ ๋„์ „๊ณผ์ œ)์„ ์ˆœ์ฐจ์ ์œผ๋กœ ์ง„ํ–‰ํ•˜๋ฉฐ ํ•ต์‹ฌ ํŒ์ • ๊ธฐ์ค€์„ ๋‹ฌ์„ฑํ•ฉ๋‹ˆ๋‹ค.\n", "\n", "**์ด๋ฒˆ ๊ฒฐ๊ณผ๋ฌผ:** `src/main_wel03.py, README.md, docs/wel03_final_report.pdf(md)` ํ˜•ํƒœ์˜ ๊ฒฐ๊ณผ๋ฌผ์„ ์™„์„ฑํ•œ๋‹ค.\n", "\n", "### ๐Ÿ” ํ‰๊ฐ€ ๋ฐ ๊ฒ€์ฆ ๊ธฐ์ค€ (PASS ์กฐ๊ฑด)\n", "#### ๋ฏธ์…˜ ํŒ์ •\n", "\n", "**ํ‰๊ฐ€ ๊ตฌ๋ถ„:** ํ•„์ˆ˜\n", "\n", "**ํ•ต์‹ฌ ํ‰๊ฐ€์˜์—ญ:** ๊ตฐ ์‹ค๋ฌด ์ฆ‰์‹œ ๋„์ž… ๊ฐ€๋Šฅ์„ฑ, ์šด์šฉ ์‹ ๋ขฐ๋„ ๋ฐ ์ „๋žต์  ๊ธฐ๋Œ€ํšจ๊ณผ\n", "\n", "#### ๋ฏธ์…˜ PASS ์กฐ๊ฑด\n", "\n", "- ์ •๋Ÿ‰ ๊ธฐ์ค€: End-to-End ํ†ตํ•ฉ ํŒŒ์ดํ”„๋ผ์ธ ์‹คํ–‰ ์„ฑ๊ณต๋ฅ  100%, ํ…Œ์ŠคํŠธ ์ปค๋ฒ„๋ฆฌ์ง€ 85% ์ด์ƒ\n", "\n", "\n", "- ์ง„ํ–‰ ๊ธฐ์ค€: ์›ํด๋ฆญ ์‹คํ–‰ ์Šคํฌ๋ฆฝํŠธ ๋™์ž‘ ํ™•์ธ ๋ฐ ์ตœ์ข… ๋ฐœํ‘œ ์‚ฐ์ถœ๋ฌผ ์ž‘์„ฑ ์™„๋ฃŒ\n", "\n", "\n", "- ๊ฒ€์ฆ ๋ฐฉ๋ฒ•: ์ตœ์ข… ๊ฒ€์ฆ ์œ„์›ํšŒ(ํ‰๊ฐ€๊ด€) ๋ชจ์˜ ์‹œ์—ฐ ๋ฐ ์ „์ฒด ๊ธฐ๋Šฅ ํ†ตํ•ฉ ๊ฒ€์ฆ\n", "\n", "\n", "#### ์ฑ„์ ์ž ํŒ์ • ๋ฉ”๋ชจ\n", "\n", "> ๋‹จ์ˆœ ์ฝ”๋“œ ์‹คํ–‰ ์—ฌ๋ถ€๋ฟ๋งŒ ์•„๋‹ˆ๋ผ ์ถœ๋ ฅ๋œ ์ˆ˜์น˜์™€ ๊ทผ๊ฑฐ ์ž๋ฃŒ๊ฐ€ ๊ตฐ ์‹ค๋ฌด ์š”๊ตฌ์‚ฌํ•ญ์— ๋ถ€ํ•ฉํ•˜๋Š”์ง€ ๋ฉด๋ฐ€ํžˆ ํ™•์ธํ•œ๋‹ค.\n", "\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "\"\"\"์ž…๋ ฅ ๊ฒ€์ฆ, ์ „์ฒ˜๋ฆฌ, ์žฌ์กฐ์ • ๊ธฐ์ค€์น˜ ํŒ์ •๊ณผ ๋žœ๋ค ํฌ๋ ˆ์ŠคํŠธ ์œ„ํ—˜ ๋ถ„๋ฅ˜, ์‹ ๋ขฐ๋„ ๋“ฑ๊ธ‰, ์ธ๊ฐ„ ๊ฒ€ํ†  ํ ๋“ฑ๋ก, ์ƒํ™ฉํŒ ์š”์•ฝ๊นŒ์ง€ ๊ฐ€์ƒ ์ˆ˜์งˆ๊ฒ€์‚ฌ ์œ„ํ—˜ ์‹ ํ˜ธ ๋Œ€์‹œ๋ณด๋“œ๋ฅผ ํ•œ ํŒŒ์ผ๋กœ ํ†ตํ•ฉ ์‹คํ–‰ํ•˜๊ณ , ์—ฌ๋Ÿฌ ์‹œ๋‚˜๋ฆฌ์˜ค End-to-End ์‹คํ–‰ยท์ž์ฒด ์ ๊ฒ€(ํ•จ์ˆ˜ ์ปค๋ฒ„๋ฆฌ์ง€)ยท์ง€ํœ˜๊ด€ ๋ธŒ๋ฆฌํ•‘ ์š”์•ฝ์„ ์ถœ๋ ฅํ•œ๋‹ค.\"\"\"\n", "import sys\n", "import time\n", "import numpy as np\n", "import pandas as pd\n", "import matplotlib.pyplot as plt\n", "from sklearn.ensemble import RandomForestClassifier\n", "\n", "# ---------------------------------------------------------------------------\n", "# [๊ต์œก์šฉ๊ฐ€์ƒ์ž๋ฃŒ] ์•ผ์ „ ๊ธ‰์ˆ˜์› ๊ฐ€์ƒ ์ˆ˜์งˆ ์„ผ์„œ ๋ฐ์ดํ„ฐ ์ƒ์„ฑ๊ธฐ (์‹ค์ œ ๋ถ€๋Œ€ยท์œ„์น˜ยท๊ฒ€์‚ฌ ๊ฒฐ๊ณผ ์—†์Œ)\n", "# ๊ฐ€์ƒ ๊ธ‰์ˆ˜์› 8๊ณณ x 14์ผ x 1์‹œ๊ฐ„ ๊ฐ„๊ฒฉ. ์‹ค์ œ ์ˆ˜์งˆ(true_*)ยท์„ผ์„œ ์ธก์ •๊ฐ’ยท์˜ค์—ผ ์‚ฌ๊ฑดยท์„ผ์„œ ๊ณ ์žฅ ์ •๋‹ต์„ ํ•จ๊ป˜ ๋งŒ๋“ ๋‹ค.\n", "# ๊ธฐ์ค€์น˜๋Š” ๊ต์œก์šฉ ๊ฐ€์ •๊ฐ’์ด๋ฉฐ, ์‹ค์ œ ์Œ์šฉ ํŒ์ •์€ ๊ณต์ธ ๊ฒ€์‚ฌ ๊ฒฐ๊ณผ์™€ ์˜๋ฌด(์˜ˆ๋ฐฉ์˜ํ•™) ๋‹ด๋‹น ๊ธฐ์ค€์— ๋”ฐ๋ฅธ๋‹ค.\n", "# ---------------------------------------------------------------------------\n", "PARAMS = [\"ph\", \"turbidity\", \"chlorine\", \"metal\", \"ecoli\"]\n", "SOURCES = [(\"W01\", \"water_truck\", 1.0, 2.0), (\"W02\", \"water_truck\", 3.5, 1.2), (\"W03\", \"purification_unit\", 2.2, 4.1),\n", " (\"W04\", \"purification_unit\", 5.0, 3.3), (\"W05\", \"well\", 0.8, 5.6), (\"W06\", \"well\", 4.4, 6.0),\n", " (\"W07\", \"river_intake\", 6.3, 1.8), (\"W08\", \"river_intake\", 6.9, 4.9)]\n", "BASE = {\"water_truck\": (7.2, 0.25, 0.75, 1.5), \"purification_unit\": (7.0, 0.20, 0.85, 1.2),\n", " \"well\": (6.8, 0.45, 0.50, 3.0), \"river_intake\": (7.3, 0.55, 0.60, 2.0)} # pH, ํƒ๋„(NTU), ์ž”๋ฅ˜์—ผ์†Œ(mg/L), ์ค‘๊ธˆ์†(ug/L)\n", "LIMITS = {\"ph\": (5.8, 8.5), \"turbidity\": (1.0, 2.0), \"chlorine\": (0.2, 0.1), \"metal\": (5.0, 10.0), \"ecoli\": (1, 1)} # (์ฃผ์˜, ์œ„ํ—˜) ๊ฐ€์ • ๊ธฐ์ค€\n", "EVENT_TYPES = { # ์˜ค์—ผ ์‚ฌ๊ฑด: ๋ณ€์ˆ˜๋ณ„ ์ตœ๋Œ€ ๋ณ€ํ™”๋Ÿ‰, ์ตœ๋Œ€์น˜ ๋„๋‹ฌ ์‹œ๊ฐ„(h)\n", " \"sewage\": ({\"turbidity\": 2.8, \"chlorine\": -0.5, \"ecoli\": 30, \"ph\": -0.3}, 4),\n", " \"metal\": ({\"metal\": 22.0, \"ph\": -0.5}, 4),\n", " \"disinfect_fail\": ({\"chlorine\": -0.8, \"ecoli\": 6}, 6),\n", " \"chemical\": ({\"ph\": 1.7, \"turbidity\": 1.0}, 3),\n", " \"creeping_metal\": ({\"metal\": 13.0}, 18),\n", "}\n", "FAULT_TYPES = [\"stuck\", \"spike\", \"drift\", \"dropout\"]\n", "NOISE = {\"ph\": 0.04, \"turbidity\": 0.06, \"chlorine\": 0.03, \"metal\": 0.35}\n", "DIGITS = {\"ph\": 2, \"turbidity\": 2, \"chlorine\": 2, \"metal\": 1}\n", "\n", "\n", "def make_dataset(days=14, seed=7, n_events=6, n_faults=10, events=None, faults=None, sources=None):\n", " \"\"\"events: [(๊ธ‰์ˆ˜์› ๋ฒˆํ˜ธ, ์‚ฌ๊ฑด ์œ ํ˜•, ์‹œ์ž‘ ์‹œ๊ฐ h, ์ง€์† h, ๊ฐ•๋„)], faults: [(๊ธ‰์ˆ˜์› ๋ฒˆํ˜ธ, ๊ณ ์žฅ ์œ ํ˜•, ํ•ญ๋ชฉ, ์‹œ์ž‘ h, ์ง€์† h)]\"\"\"\n", " rng = np.random.default_rng(seed)\n", " sources = sources or SOURCES\n", " T = days * 24\n", " tt = np.arange(T)\n", " hour = tt % 24\n", " night = (hour < 6) | (hour >= 20)\n", " rain = np.zeros(T)\n", " for _ in range(max(1, days // 4)):\n", " s0, d = int(rng.integers(0, T - 14)), int(rng.integers(6, 14))\n", " rain[s0:s0 + d] = np.maximum(rain[s0:s0 + d], rng.uniform(0.6, 1.0))\n", " if events is None:\n", " events = [(int(rng.integers(len(sources))), str(rng.choice(list(EVENT_TYPES))), int(rng.integers(min(24, T // 4), max(T // 4 + 1, T - 48))),\n", " int(rng.integers(10, 30)), 1.0) for _ in range(n_events)]\n", " if faults is None:\n", " faults = [(int(rng.integers(len(sources))), str(rng.choice(FAULT_TYPES)), str(rng.choice([\"ph\", \"turbidity\", \"chlorine\", \"metal\"])),\n", " int(rng.integers(12, max(13, T - 30))), int(rng.integers(6, 24))) for _ in range(n_faults)]\n", " out = []\n", " for si, (sid, stype, x, y) in enumerate(sources):\n", " ph0, tb0, cl0, mt0 = BASE[stype]\n", " tr = {\"ph\": ph0 + 0.08 * np.sin(2 * np.pi * hour / 24) + rng.normal(0, 0.03, T),\n", " \"turbidity\": tb0 * (1 + 0.1 * rng.normal(0, 1, T)),\n", " \"chlorine\": (cl0 * np.exp(-0.022 * hour) if stype == \"water_truck\" else cl0 + rng.normal(0, 0.03, T)),\n", " \"metal\": mt0 + rng.normal(0, 0.2, T), \"ecoli\": np.zeros(T)}\n", " if stype in (\"river_intake\", \"well\"):\n", " tr[\"turbidity\"] = tr[\"turbidity\"] + rain * (0.9 if stype == \"river_intake\" else 0.45)\n", " f_all, etype = np.zeros(T), np.array([\"none\"] * T, dtype=object)\n", " for (s_i, et, onset, dur, scale) in events:\n", " if s_i != si:\n", " continue\n", " delta, ramp = EVENT_TYPES[et]\n", " f = np.clip((tt - onset + 1) / ramp, 0, 1) * ((tt >= onset) & (tt < onset + dur + ramp))\n", " for k, v in delta.items():\n", " tr[k] = tr[k] + v * f * scale\n", " etype[f > 0] = et\n", " f_all = np.maximum(f_all, f)\n", " tr[\"chlorine\"] = np.clip(tr[\"chlorine\"], 0.02, None)\n", " tr[\"turbidity\"] = np.clip(tr[\"turbidity\"], 0.05, None)\n", " tr[\"metal\"] = np.clip(tr[\"metal\"], 0.1, None)\n", " grade = np.where(f_all >= 0.2, 2, 0)\n", " mild = (tr[\"turbidity\"] > LIMITS[\"turbidity\"][0]) | (tr[\"chlorine\"] < LIMITS[\"chlorine\"][0]) | (tr[\"metal\"] > LIMITS[\"metal\"][0])\n", " grade = np.where((grade == 0) & mild, 1, grade)\n", " ms = {}\n", " for p in [\"ph\", \"turbidity\", \"chlorine\", \"metal\"]:\n", " sd = NOISE[p] * np.where(night, 1.5, 1.0) * (tr[p] if p == \"turbidity\" else 1)\n", " ms[p] = np.round(np.clip(tr[p] + rng.normal(0, 1, T) * sd, 0, None), DIGITS[p])\n", " ms[\"ecoli\"] = (rng.poisson(tr[\"ecoli\"]) + (rng.random(T) < 0.004)).astype(float)\n", " fault, fparam = np.array([\"none\"] * T, dtype=object), np.array([\"-\"] * T, dtype=object)\n", " for (s_i, ft, p, s0, d) in faults:\n", " if s_i != si:\n", " continue\n", " seg = slice(s0, min(T, s0 + d))\n", " if ft == \"stuck\":\n", " ms[p][seg] = ms[p][s0]\n", " elif ft == \"spike\":\n", " seg = slice(s0, s0 + 1)\n", " ms[p][s0] = {\"ph\": 11.8, \"turbidity\": 38.0, \"chlorine\": 0.0, \"metal\": 75.0}[p]\n", " elif ft == \"drift\":\n", " ramp = np.linspace(0, 1, seg.stop - s0)\n", " ms[p][seg] = np.clip(ms[p][seg] + ramp * {\"ph\": -1.6, \"turbidity\": 2.6, \"chlorine\": -0.75, \"metal\": 12.0}[p], 0, None).round(DIGITS[p])\n", " elif ft == \"dropout\":\n", " for q in PARAMS:\n", " ms[q][seg] = np.nan\n", " p = \"all\"\n", " fault[seg], fparam[seg] = ft, p\n", " df = pd.DataFrame({\"source_id\": sid, \"source_type\": stype, \"x_km\": x, \"y_km\": y, \"t\": tt,\n", " \"cond\": np.where(rain > 0, \"storm\", np.where(night, \"night\", \"day\")),\n", " **{p: ms[p] for p in PARAMS}, **{f\"true_{p}\": tr[p] for p in PARAMS},\n", " \"event\": etype, \"fault\": fault, \"fault_param\": fparam, \"grade\": grade})\n", " out.append(df)\n", " return pd.concat(out, ignore_index=True)\n", "# ---------------------------------------------------------------------------\n", "\n", "# ---- ์ „์ฒ˜๋ฆฌ ๊ณตํ†ต ํ•จ์ˆ˜: ๋ฌผ๋ฆฌ ๋ฒ”์œ„ ์ ๊ฒ€ -> ๊ณ ์ •๊ฐ’(stuck) ํ‘œ์‹œ -> ๋‹จ๋ฐœ ์ŠคํŒŒ์ดํฌ ์ œ๊ฑฐ -> ๊ฒฐ์ธก ๋ณด์ • ----\n", "PHYS = {\"ph\": (0, 14), \"turbidity\": (0, 1000), \"chlorine\": (0, 10), \"metal\": (0, 1000), \"ecoli\": (0, 100000)}\n", "SPIKE_K = {\"day\": 6.0, \"night\": 7.0, \"storm\": 9.0} # ์ƒํ™ฉ๋ณ„(์ฃผ๊ฐ„/์•ผ๊ฐ„/์•…์ฒœํ›„) ์ŠคํŒŒ์ดํฌ ํŒ์ • ๋ฐฐ์ˆ˜\n", "MIN_JUMP = {\"ph\": 0.8, \"turbidity\": 1.5, \"chlorine\": 0.3, \"metal\": 6.0}\n", "STUCK_RUN, SHORT_GAP = 6, 3\n", "\n", "\n", "def clean(df, spike_k=None, min_jump=None):\n", " \"\"\"๊ธ‰์ˆ˜์›๋ณ„ ์‹œ๊ณ„์—ด์„ ์ •์ œํ•˜๊ณ  ํ•ญ๋ชฉ๋ณ„ ์ฒ˜๋ฆฌ ๋‚ด์—ญ(<ํ•ญ๋ชฉ>_flag: range/stuck/spike/interp/long_gap)์„ ๋‚จ๊ธด๋‹ค.\"\"\"\n", " spike_k = spike_k or SPIKE_K\n", " min_jump = MIN_JUMP if min_jump is None else min_jump\n", " out = df.sort_values([\"source_id\", \"t\"]).reset_index(drop=True)\n", " for sid, idx in out.groupby(\"source_id\").groups.items():\n", " idx = np.asarray(idx)\n", " kk = out.loc[idx, \"cond\"].map(spike_k).to_numpy(float)\n", " for p in PARAMS:\n", " v = out.loc[idx, p].to_numpy(float).copy()\n", " fl = np.array([\"\"] * len(v), dtype=object)\n", " lo, hi = PHYS[p]\n", " bad = (v < lo) | (v > hi)\n", " v[bad], fl[bad] = np.nan, \"range\"\n", " if p != \"ecoli\":\n", " chg = np.r_[True, v[1:] != v[:-1]] # ๊ฐ™์€ ๊ฐ’์ด STUCK_RUN์‹œ๊ฐ„ ์ด์ƒ ๋ฐ˜๋ณต = ๊ณ ์ •๊ฐ’ ๊ณ ์žฅ ์˜์‹ฌ\n", " rid = np.cumsum(chg)\n", " rlen = np.bincount(rid)[rid]\n", " fl[(rlen >= STUCK_RUN) & ~chg & ~np.isnan(v)] = \"stuck\"\n", " d = np.abs(np.diff(v[~np.isnan(v)]))\n", " sig = 1.4826 * np.median(d) + 1e-6 if len(d) else 1.0\n", " up, dn = v - np.r_[np.nan, v[:-1]], v - np.r_[v[1:], np.nan]\n", " thr = np.maximum(kk * sig, min_jump[p])\n", " spike = (np.abs(up) > thr) & (np.abs(dn) > thr) & (np.sign(up) == np.sign(dn)) # ํŠ€์—ˆ๋‹ค๊ฐ€ ๋ฐ”๋กœ ๋Œ์•„์˜จ ๊ฐ’๋งŒ\n", " v[spike], fl[spike] = np.nan, \"spike\"\n", " s = pd.Series(v)\n", " na = s.isna().to_numpy()\n", " grp = np.cumsum(np.r_[True, na[1:] != na[:-1]])\n", " glen = pd.Series(na.astype(int)).groupby(grp).transform(\"sum\").to_numpy()\n", " lin, ff = s.interpolate(limit_area=\"inside\").to_numpy(), s.ffill().bfill().to_numpy()\n", " short, longg = na & (glen <= SHORT_GAP), na & (glen > SHORT_GAP)\n", " v = np.where(short, np.where(np.isnan(lin), ff, lin), np.where(longg, ff, v))\n", " fl[short & (fl == \"\")] = \"interp\"\n", " fl[longg] = \"long_gap\"\n", " out.loc[idx, p] = v\n", " out.loc[idx, f\"{p}_flag\"] = fl\n", " return out\n", "\n", "# ---- ๊ฐ€์ • ๊ธฐ์ค€์น˜ ํŒ์ •(Baseline)๊ณผ ์‹œ๊ณ„์—ด ํŠน์ง• ๊ณตํ†ต ํ•จ์ˆ˜ ----\n", "GRADE_NAME = {0: \"์•ˆ์ „\", 1: \"์ฃผ์˜\", 2: \"์œ„ํ—˜\"}\n", "\n", "\n", "def rule_grade(df):\n", " \"\"\"๊ธฐ์ค€์น˜ 1์ฐจ ํ•„ํ„ฐ: ์œ„ํ—˜(์Œ์šฉ ๋ถ€์ ํ•ฉ ์˜์‹ฌ) = ํ•œ ํ•ญ๋ชฉ์ด๋ผ๋„ ์œ„ํ—˜ ๊ธฐ์ค€ ์ดˆ๊ณผ, ์ฃผ์˜ = ์ฃผ์˜ ๊ธฐ์ค€ ์ดˆ๊ณผ.\"\"\"\n", " ph, tb, cl, mt, ec = (df[p].to_numpy(float) for p in PARAMS)\n", " danger = (ph < LIMITS[\"ph\"][0]) | (ph > LIMITS[\"ph\"][1]) | (tb > LIMITS[\"turbidity\"][1]) | (cl < LIMITS[\"chlorine\"][1]) \\\n", " | (mt > LIMITS[\"metal\"][1]) | (ec >= LIMITS[\"ecoli\"][1])\n", " caution = (tb > LIMITS[\"turbidity\"][0]) | (cl < LIMITS[\"chlorine\"][0]) | (mt > LIMITS[\"metal\"][0])\n", " return np.where(danger, 2, np.where(caution, 1, 0))\n", "\n", "\n", "def reasons(row):\n", " r = []\n", " if not LIMITS[\"ph\"][0] <= row[\"ph\"] <= LIMITS[\"ph\"][1]:\n", " r.append(f\"pH {row['ph']:.2f}\")\n", " if row[\"turbidity\"] > LIMITS[\"turbidity\"][0]:\n", " r.append(f\"ํƒ๋„ {row['turbidity']:.2f}NTU\")\n", " if row[\"chlorine\"] < LIMITS[\"chlorine\"][0]:\n", " r.append(f\"์ž”๋ฅ˜์—ผ์†Œ {row['chlorine']:.2f}mg/L\")\n", " if row[\"metal\"] > LIMITS[\"metal\"][0]:\n", " r.append(f\"์ค‘๊ธˆ์† {row['metal']:.1f}ug/L\")\n", " if row[\"ecoli\"] >= 1:\n", " r.append(f\"๋Œ€์žฅ๊ท  {row['ecoli']:.0f}\")\n", " return \", \".join(r) or \"-\"\n", "\n", "\n", "FEATS = [f\"{p}{s}\" for p in [\"ph\", \"turbidity\", \"chlorine\", \"metal\"] for s in [\"\", \"_d1\", \"_d3\", \"_dev\", \"_sd6\"]] + \\\n", " [\"ecoli\", \"ecoli_3h\", \"storm\", \"night\", \"n_flag\", \"truck\", \"purif\", \"well\", \"river\"]\n", "\n", "\n", "def add_features(c):\n", " \"\"\"์‹œ๊ฐ„ ์ˆœ์„œ๋ฅผ ์ง€ํ‚ค๋Š”(๊ณผ๊ฑฐ ๊ฐ’๋งŒ ์“ฐ๋Š”) ๊ธ‰์ˆ˜์›๋ณ„ ํŠน์ง•: ๋ณ€ํ™”๋Ÿ‰, 24์‹œ๊ฐ„ ๊ธฐ์ค€์„  ๋Œ€๋น„ ํŽธ์ฐจ, 6์‹œ๊ฐ„ ๋ณ€๋™์„ฑ, ๋Œ€์žฅ๊ท  ๋ˆ„์  ๋“ฑ.\"\"\"\n", " c = c.sort_values([\"source_id\", \"t\"]).reset_index(drop=True)\n", " g = c.groupby(\"source_id\")\n", " for p in [\"ph\", \"turbidity\", \"chlorine\", \"metal\"]:\n", " c[f\"{p}_d1\"] = g[p].diff().fillna(0)\n", " c[f\"{p}_d3\"] = g[p].diff(3).fillna(0)\n", " base = g[p].transform(lambda s: s.shift(1).rolling(24, min_periods=4).median())\n", " c[f\"{p}_dev\"] = (c[p] - base).fillna(0)\n", " c[f\"{p}_sd6\"] = g[p].transform(lambda s: s.rolling(6, min_periods=2).std()).fillna(0)\n", " c[\"ecoli_3h\"] = g[\"ecoli\"].transform(lambda s: s.rolling(3, min_periods=1).sum())\n", " c[\"storm\"], c[\"night\"] = (c[\"cond\"] == \"storm\").astype(int), (c[\"cond\"] == \"night\").astype(int)\n", " flags = [f\"{p}_flag\" for p in PARAMS if f\"{p}_flag\" in c]\n", " c[\"n_flag\"] = sum(c[f].isin([\"stuck\", \"spike\", \"long_gap\", \"range\"]).astype(int) for f in flags) if flags else 0\n", " for k, name in [(\"truck\", \"water_truck\"), (\"purif\", \"purification_unit\"), (\"well\", \"well\"), (\"river\", \"river_intake\")]:\n", " c[k] = (c[\"source_type\"] == name).astype(int)\n", " return c\n", "\n", "CONFIG = {\"ecoli_3h_min\": 2, \"metal_dev\": 1.0, \"metal_min\": 5.0, # ๋ฏธ์…˜ 9 ์žฌ์กฐ์ • ๊ฐ’\n", " \"conf_high\": 60, \"scale\": {\"ph\": 0.35, \"turbidity\": 0.6, \"chlorine\": 0.2, \"metal\": 3.0}}\n", "README = {\"1. ๋ชฉ์ \": \"๊ฐ€์ƒ ์ˆ˜์งˆ ์„ผ์„œ ์ž๋ฃŒ๋กœ ๊ธ‰์ˆ˜์› ์œ„ํ—˜ ์‹ ํ˜ธ๋ฅผ ์กฐ๊ธฐ์— ์•Œ๋ฆฌ๋Š” ํŒ๋‹จ ๋ณด์กฐ ๋„๊ตฌ\",\n", " \"2. ์‹คํ–‰ ๋ฐฉ๋ฒ•\": \"python main_wel03.py (์›ํด๋ฆญ ์‹คํ–‰, ์™ธ๋ถ€ ํŒŒ์ผยท๋„คํŠธ์›Œํฌ ๋ถˆํ•„์š”)\",\n", " \"3. ์ž…๋ ฅ ํ˜•์‹\": \"๊ธ‰์ˆ˜์› IDยท์œ ํ˜•ยท์‹œ๊ฐยทpHยทํƒ๋„ยท์ž”๋ฅ˜์—ผ์†Œยท์ค‘๊ธˆ์†ยท๋Œ€์žฅ๊ท (๋ฏธ์…˜ 1 ์Šคํ‚ค๋งˆ)\",\n", " \"4. ํŒ์ • ๊ทœ์น™\": \"๊ฐ€์ • ๊ธฐ์ค€์น˜ + ์žฌ์กฐ์ • ๊ทœ์น™ + ๋ชจ๋ธ, ์‹ ๋ขฐ๋„ High๋งŒ ๊ธ‰์ˆ˜ ์ค‘๋‹จ ๊ฒ€ํ†  ์ƒ์‹ \",\n", " \"5. ์ธ๊ฐ„ ๊ฒ€ํ† \": \"๊ธ‰์ˆ˜ ์ค‘๋‹จยท์žฌ๊ฐœ๋Š” ์˜ˆ๋ฐฉ์˜ํ•™ ๋‹ด๋‹น ๊ฒ€ํ†  ํ›„ ์ง€ํœ˜๊ด€ ์Šน์ธ, ์ž๋™ ํ™•์ • ์—†์Œ\",\n", " \"6. ํ•œ๊ณ„์™€ ์ฃผ์˜\": \"๊ต์œก์šฉ ๊ฐ€์ƒ ์ž๋ฃŒ, ์‹ค์ œ ํŒ์ •์€ ๊ณต์‹ ์ˆ˜์งˆ ๊ธฐ์ค€๊ณผ ์˜๋ฌดยทํ™˜๊ฒฝ ๋‹ด๋‹น ํ™•์ธ\",\n", " \"7. ์—ฐ๋™ ํ™•์žฅ\": \"์ธ์ ‘ ๋ถ€๋Œ€ ๊ธ‰์ˆ˜ ์ •๋ณด ๊ตํ™˜์šฉ ํ‘œ์ค€ ํ•„๋“œ(JSON) API๋ฅผ ์ฐจ๊ธฐ ๊ณผ์ œ๋กœ ๋‘”๋‹ค\"}\n", "\n", "\n", "def validate(df):\n", " ok = df[\"source_id\"].astype(str).str.match(r\"^W\\d{2}$\") & df[\"t\"].ge(0)\n", " return df[ok].copy(), int((~ok).sum())\n", "\n", "\n", "def tuned_grade(c):\n", " ph, tb, cl, mt, ec = (c[p].to_numpy(float) for p in PARAMS)\n", " eco = (c[\"ecoli_3h\"].to_numpy() >= CONFIG[\"ecoli_3h_min\"]) | ((ec >= 1) & ((tb > LIMITS[\"turbidity\"][0]) | (cl < LIMITS[\"chlorine\"][0])))\n", " trend = (c[\"metal_dev\"].to_numpy() > CONFIG[\"metal_dev\"]) & (mt > CONFIG[\"metal_min\"])\n", " danger = (ph < LIMITS[\"ph\"][0]) | (ph > LIMITS[\"ph\"][1]) | (tb > LIMITS[\"turbidity\"][1]) | (cl < LIMITS[\"chlorine\"][1]) \\\n", " | (mt > LIMITS[\"metal\"][1]) | eco | trend\n", " caution = (tb > LIMITS[\"turbidity\"][0]) | (cl < LIMITS[\"chlorine\"][0]) | (mt > LIMITS[\"metal\"][0]) | (ec >= 1)\n", " return np.where(danger, 2, np.where(caution, 1, 0))\n", "\n", "\n", "def confidence(c, alert):\n", " S = CONFIG[\"scale\"]\n", " n_dev = sum(((c[f\"{p}_dev\"].abs() > S[p]) | (c[f\"{p}_d3\"].abs() > S[p])).astype(int) for p in S) + (c[\"ecoli_3h\"] >= 2)\n", " a = pd.Series(alert.astype(int), index=c.index)\n", " run = a.groupby([c[\"source_id\"], (a != a.groupby(c[\"source_id\"]).shift()).cumsum()]).cumsum()\n", " g = c.groupby(\"source_id\")\n", " flagged = g[\"n_flag\"].transform(lambda s: s.rolling(6, min_periods=1).max()) > 0\n", " # ๋“œ๋ฆฌํ”„ํŠธ ์˜์‹ฌ: ํ•œ ํ•ญ๋ชฉ๋งŒ 6์‹œ๊ฐ„ ์ค‘ 5์‹œ๊ฐ„ ์ด์ƒ ๊ฐ™์€ ๋ฐฉํ–ฅ์œผ๋กœ ๊พธ์ค€ํžˆ ๋ณ€ํ•จ(์žก์Œ์ด ์ปค์ง€๋Š” ์•ผ๊ฐ„์—๋„ ์œ ์ง€๋˜๋Š” ๊ธฐ์ค€)\n", " mono = pd.concat([g[f\"{p}_d1\"].transform(lambda s: np.sign(s).rolling(6, min_periods=3).sum()).abs() >= 4 for p in S], axis=1).any(axis=1)\n", " drift = (n_dev <= 1) & mono & (c[\"ecoli_3h\"] < 2)\n", " return 40 * np.minimum(n_dev, 3) / 3 + 25 * np.minimum(run, 3) / 3 + 20 * (~flagged) + 15 * (~drift)\n", "\n", "\n", "def train_model():\n", " tr = pd.concat([add_features(clean(make_dataset(seed=s, n_events=14, n_faults=14))) for s in (7, 8)], ignore_index=True)\n", " return RandomForestClassifier(n_estimators=150, max_depth=10, class_weight=\"balanced_subsample\", random_state=0, n_jobs=1).fit(tr[FEATS], tr[\"grade\"])\n", "\n", "\n", "def run_pipeline(raw, model):\n", " data, n_bad = validate(raw)\n", " c = add_features(clean(data))\n", " c[\"rule\"] = tuned_grade(c)\n", " c[\"ml\"] = model.predict(c[FEATS])\n", " c[\"final\"] = np.maximum(c[\"rule\"], c[\"ml\"]) # ๋‘˜ ์ค‘ ํ•˜๋‚˜๋ผ๋„ ์œ„ํ—˜์ด๋ฉด ์œ„ํ—˜(๋†“์นจ ๋ฐฉ์ง€)\n", " alert = (c[\"final\"] == 2).to_numpy()\n", " c[\"conf\"] = np.where(alert, confidence(c, alert), np.nan)\n", " queue = c[alert & (c[\"conf\"] >= CONFIG[\"conf_high\"])].groupby(\"source_id\").agg(first=(\"t\", \"min\"), hours=(\"t\", \"size\"))\n", " queue[\"status\"] = \"PENDING(์ง€ํœ˜๊ด€ ๊ฒฐ์ • ๋Œ€๊ธฐ)\"\n", " return c, queue, n_bad\n", "\n", "\n", "def self_tests(model):\n", " res = {}\n", " clean_df = make_dataset(days=2, seed=50, events=[], faults=[])\n", " c, q, _ = run_pipeline(clean_df, model)\n", " res[\"์˜ค์—ผ ์—†๋Š” ์ž๋ฃŒ์—์„œ ๊ธ‰์ˆ˜ ์ค‘๋‹จ ์ƒ์‹  0๊ฑด\"] = len(q) == 0\n", " ev = make_dataset(days=2, seed=50, events=[(0, \"sewage\", 20, 12, 1.0)], faults=[])\n", " c, q, _ = run_pipeline(ev, model)\n", " res[\"ํ•˜์ˆ˜ ์œ ์ž… ๊ธ‰์ˆ˜์›(W01) ์ƒ์‹ \"] = \"W01\" in q.index\n", " res[\"์ƒ์‹  ๊ฑด์€ ๋ชจ๋‘ PENDING(์ž๋™ ํ™•์ • ์—†์Œ)\"] = q[\"status\"].str.startswith(\"PENDING\").all()\n", " bad = ev.copy(); bad.loc[3, \"source_id\"] = \"??\"\n", " res[\"์†์ƒ ๋ ˆ์ฝ”๋“œ ๊ฒฉ๋ฆฌ\"] = run_pipeline(bad, model)[2] == 1\n", " dr = make_dataset(days=2, seed=50, events=[], faults=[(1, \"drift\", \"turbidity\", 10, 18)])\n", " c, q, _ = run_pipeline(dr, model)\n", " n_alert = int((c.loc[c[\"source_id\"] == \"W02\", \"final\"] == 2).sum()) # ๋“œ๋ฆฌํ”„ํŠธ ๊ตฌ๊ฐ„ ์œ„ํ—˜ ํŒ์ • ์‹œ๊ฐ„ ์ค‘ High ์ƒ์‹  ์‹œ๊ฐ„์ด ์ ˆ๋ฐ˜ ๋ฏธ๋งŒ์ด๋ฉด ์–ต์ œ ์ž‘๋™\n", " res[\"์„ผ์„œ ๋“œ๋ฆฌํ”„ํŠธ ์ƒ์‹  ์‹œ๊ฐ„ ์ ˆ๋ฐ˜ ๋ฏธ๋งŒ์œผ๋กœ ์–ต์ œ\"] = (\"W02\" not in q.index) or q.loc[\"W02\", \"hours\"] < 0.5 * n_alert\n", " res[\"๊ธฐ์ค€์น˜ ํ•„ํ„ฐยทํŒ์ • ๊ทผ๊ฑฐ ๋ฌธ๊ตฌ ์ƒ์„ฑ\"] = len(reasons(c[c[\"source_id\"] == \"W02\"].iloc[40])) > 0 and rule_grade(c).shape[0] == len(c)\n", " res[\"๊ฒฐ์ธก ๋ณด์ • ํ›„ ๋‚จ์€ ๊ฒฐ์ธก 0\"] = int(c[PARAMS].isna().sum().sum()) == 0\n", " res[\"์šด์˜ ๋ฌธ์„œ 7๊ฐœ ์žฅ ์ž‘์„ฑ\"] = all(len(v) > 10 for v in README.values())\n", " return res\n", "\n", "\n", "# ---- ์ž์ฒด ์ ๊ฒ€(ํ•จ์ˆ˜ ํ˜ธ์ถœ ์ถ”์ ์œผ๋กœ ์ปค๋ฒ„๋ฆฌ์ง€ ์ธก์ •) ----\n", "t_all = time.perf_counter()\n", "called = set()\n", "own = {n for n, f in list(globals().items()) if callable(f) and getattr(f, \"__module__\", \"\") == \"__main__\" and hasattr(f, \"__code__\")}\n", "sys.setprofile(lambda fr, ev, arg: called.add(fr.f_code.co_name) if ev == \"call\" else None)\n", "model = train_model()\n", "tests = self_tests(model)\n", "e2e = []\n", "for s in (11, 21, 31, 41, 51):\n", " try:\n", " c, q, nb = run_pipeline(make_dataset(seed=s, n_events=10, n_faults=14), model)\n", " e2e.append((s, True, (c[\"final\"] == c[\"grade\"]).mean(), len(q)))\n", " except Exception as e:\n", " e2e.append((s, False, np.nan, 0))\n", "sys.setprofile(None)\n", "cov = len(own & called) / len(own)\n", "print(f\"[๊ต์œก์šฉ๊ฐ€์ƒ์ž๋ฃŒ] ํ†ตํ•ฉ ์‹คํ–‰ {time.perf_counter() - t_all:.1f}์ดˆ (๋ชจ๋ธ ํ•™์Šต ํฌํ•จ)\")\n", "print(\"[End-to-End ์‹œ๋‚˜๋ฆฌ์˜ค 5์ข…]\")\n", "for s, ok, acc, nq in e2e:\n", " print(f\" seed {s}: {'์„ฑ๊ณต' if ok else '์‹คํŒจ'}, ์ตœ์ข… ํŒ์ • ์ •ํ™•๋„ {acc:.1%}, ๊ธ‰์ˆ˜ ์ค‘๋‹จ ๊ฒ€ํ†  ์ƒ์‹  {nq}๊ณณ\")\n", "rate = np.mean([x[1] for x in e2e])\n", "print(f\"E2E ์„ฑ๊ณต๋ฅ  {rate:.0%} (๋ชฉํ‘œ 100%: {'๋‹ฌ์„ฑ' if rate == 1 else '๋ฏธ๋‹ฌ'})\")\n", "print(\"[์ž์ฒด ์ ๊ฒ€]\")\n", "for k, v in tests.items():\n", " print(f\" [{'PASS' if v else 'FAIL'}] {k}\")\n", "print(f\"์ž์ฒด ์ ๊ฒ€ {sum(tests.values())}/{len(tests)} ํ†ต๊ณผ, ํ•จ์ˆ˜ ์ปค๋ฒ„๋ฆฌ์ง€ {cov:.0%}({len(own & called)}/{len(own)}, ๋ชฉํ‘œ 85% ์ด์ƒ: {'๋‹ฌ์„ฑ' if cov >= 0.85 else '๋ฏธ๋‹ฌ'})\")\n", "print(f\"์šด์˜ ๋ฌธ์„œ(README) {len(README)}๊ฐœ ์žฅ: \" + \", \".join(README))\n", "\n", "# ---- ๋ธŒ๋ฆฌํ•‘ ์š”์•ฝ(๋ฒค์น˜๋งˆํฌ seed 11, ์šด์šฉ ์‹œ์  ๊ธฐ์ค€) ----\n", "c, q, _ = run_pipeline(make_dataset(seed=11, n_events=10, n_faults=14), model)\n", "d = c[\"grade\"] == 2\n", "print(\"\\n[์ง€ํœ˜๊ด€ ๋ธŒ๋ฆฌํ•‘ ์š”์•ฝ]\")\n", "print(f\"1. ๊ธ‰์ˆ˜์› 8๊ณณ 14์ผ ๋ชจ์˜: ์œ„ํ—˜ ์žฌํ˜„์œจ {((c['final'] == 2) & d).sum() / d.sum():.1%}, ์ข…ํ•ฉ ์ •ํ™•๋„ {(c['final'] == c['grade']).mean():.1%}\")\n", "print(f\"2. ๊ธ‰์ˆ˜ ์ค‘๋‹จ ๊ฒ€ํ†  ์ƒ์‹  {len(q)}๊ณณ({', '.join(q.index)}): ๋ชจ๋‘ ์˜ˆ๋ฐฉ์˜ํ•™ ๋‹ด๋‹น ๊ฒ€ํ† ยท์ง€ํœ˜๊ด€ ๊ฒฐ์ • ๋Œ€๊ธฐ\")\n", "print(\"3. ์„ผ์„œ ์ด์ƒ(๋“œ๋ฆฌํ”„ํŠธยท์ŠคํŒŒ์ดํฌยท๋‹จ์ ˆ)์€ ์žฌ์ธก์ •ยท๊ต์ • ์š”์ฒญ์œผ๋กœ ๋ถ„๋ฆฌํ•ด ๋ถˆํ•„์š”ํ•œ ๋‹จ์ˆ˜๋ฅผ ์ค„์ธ๋‹ค\")\n", "print(\"4. ์‹ค์ œ ์Œ์šฉ ํŒ์ •์€ ๊ณต์‹ ์ˆ˜์งˆ ๊ธฐ์ค€๊ณผ ์˜๋ฌด(์˜ˆ๋ฐฉ์˜ํ•™)ยทํ™˜๊ฒฝ ๋‹ด๋‹น ํ™•์ธ์— ๋”ฐ๋ฅธ๋‹ค(๋ณธ ๋„๊ตฌ๋Š” ํŒ๋‹จ ๋ณด์กฐ)\")\n", "\n", "last = c[c[\"t\"] == c[\"t\"].max()].set_index(\"source_id\")\n", "fig, ax = plt.subplots(1, 2, figsize=(11, 4))\n", "ax[0].bar(last.index, last[\"final\"], color=[{0: \"tab:green\", 1: \"gold\", 2: \"red\"}[g] for g in last[\"final\"]])\n", "ax[0].set_yticks([0, 1, 2], [\"safe\", \"caution\", \"danger\"]); ax[0].set_title(\"Final status by source (last hour)\")\n", "ax[1].bar([str(x[0]) for x in e2e], [x[2] for x in e2e], color=\"tab:blue\"); ax[1].set_ylim(0.9, 1.0)\n", "ax[1].set_title(\"E2E accuracy by scenario seed\"); ax[1].set_xlabel(\"seed\")\n", "plt.suptitle(\"Integrated water risk dashboard package (synthetic)\"); plt.tight_layout(); plt.show()\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "
\n", "๐Ÿ’ก [์ฐธ๊ณ ] ๋ฏธ์…˜ 10 ์˜ˆ์ƒ ์‹คํ–‰ ๊ฒฐ๊ณผ (Output)\n", "\n", "```text\n", "[๊ต์œก์šฉ๊ฐ€์ƒ์ž๋ฃŒ] ํ†ตํ•ฉ ์‹คํ–‰ 6.6์ดˆ (๋ชจ๋ธ ํ•™์Šต ํฌํ•จ)\n", "[End-to-End ์‹œ๋‚˜๋ฆฌ์˜ค 5์ข…]\n", " seed 11: ์„ฑ๊ณต, ์ตœ์ข… ํŒ์ • ์ •ํ™•๋„ 98.6%, ๊ธ‰์ˆ˜ ์ค‘๋‹จ ๊ฒ€ํ†  ์ƒ์‹  6๊ณณ\n", " seed 21: ์„ฑ๊ณต, ์ตœ์ข… ํŒ์ • ์ •ํ™•๋„ 99.1%, ๊ธ‰์ˆ˜ ์ค‘๋‹จ ๊ฒ€ํ†  ์ƒ์‹  6๊ณณ\n", " seed 31: ์„ฑ๊ณต, ์ตœ์ข… ํŒ์ • ์ •ํ™•๋„ 97.0%, ๊ธ‰์ˆ˜ ์ค‘๋‹จ ๊ฒ€ํ†  ์ƒ์‹  6๊ณณ\n", " seed 41: ์„ฑ๊ณต, ์ตœ์ข… ํŒ์ • ์ •ํ™•๋„ 97.7%, ๊ธ‰์ˆ˜ ์ค‘๋‹จ ๊ฒ€ํ†  ์ƒ์‹  7๊ณณ\n", " seed 51: ์„ฑ๊ณต, ์ตœ์ข… ํŒ์ • ์ •ํ™•๋„ 98.0%, ๊ธ‰์ˆ˜ ์ค‘๋‹จ ๊ฒ€ํ†  ์ƒ์‹  7๊ณณ\n", "E2E ์„ฑ๊ณต๋ฅ  100% (๋ชฉํ‘œ 100%: ๋‹ฌ์„ฑ)\n", "[์ž์ฒด ์ ๊ฒ€]\n", " [PASS] ์˜ค์—ผ ์—†๋Š” ์ž๋ฃŒ์—์„œ ๊ธ‰์ˆ˜ ์ค‘๋‹จ ์ƒ์‹  0๊ฑด\n", " [PASS] ํ•˜์ˆ˜ ์œ ์ž… ๊ธ‰์ˆ˜์›(W01) ์ƒ์‹ \n", " [PASS] ์ƒ์‹  ๊ฑด์€ ๋ชจ๋‘ PENDING(์ž๋™ ํ™•์ • ์—†์Œ)\n", " [PASS] ์†์ƒ ๋ ˆ์ฝ”๋“œ ๊ฒฉ๋ฆฌ\n", " [PASS] ์„ผ์„œ ๋“œ๋ฆฌํ”„ํŠธ ์ƒ์‹  ์‹œ๊ฐ„ ์ ˆ๋ฐ˜ ๋ฏธ๋งŒ์œผ๋กœ ์–ต์ œ\n", " [PASS] ๊ธฐ์ค€์น˜ ํ•„ํ„ฐยทํŒ์ • ๊ทผ๊ฑฐ ๋ฌธ๊ตฌ ์ƒ์„ฑ\n", " [PASS] ๊ฒฐ์ธก ๋ณด์ • ํ›„ ๋‚จ์€ ๊ฒฐ์ธก 0\n", " [PASS] ์šด์˜ ๋ฌธ์„œ 7๊ฐœ ์žฅ ์ž‘์„ฑ\n", "์ž์ฒด ์ ๊ฒ€ 8/8 ํ†ต๊ณผ, ํ•จ์ˆ˜ ์ปค๋ฒ„๋ฆฌ์ง€ 100%(11/11, ๋ชฉํ‘œ 85% ์ด์ƒ: ๋‹ฌ์„ฑ)\n", "์šด์˜ ๋ฌธ์„œ(README) 7๊ฐœ ์žฅ: 1. ๋ชฉ์ , 2. ์‹คํ–‰ ๋ฐฉ๋ฒ•, 3. ์ž…๋ ฅ ํ˜•์‹, 4. ํŒ์ • ๊ทœ์น™, 5. ์ธ๊ฐ„ ๊ฒ€ํ† , 6. ํ•œ๊ณ„์™€ ์ฃผ์˜, 7. ์—ฐ๋™ ํ™•์žฅ\n", "\n", "[์ง€ํœ˜๊ด€ ๋ธŒ๋ฆฌํ•‘ ์š”์•ฝ]\n", "1. ๊ธ‰์ˆ˜์› 8๊ณณ 14์ผ ๋ชจ์˜: ์œ„ํ—˜ ์žฌํ˜„์œจ 98.4%, ์ข…ํ•ฉ ์ •ํ™•๋„ 98.6%\n", "2. ๊ธ‰์ˆ˜ ์ค‘๋‹จ ๊ฒ€ํ†  ์ƒ์‹  6๊ณณ(W02, W03, W04, W05, W07, W08): ๋ชจ๋‘ ์˜ˆ๋ฐฉ์˜ํ•™ ๋‹ด๋‹น ๊ฒ€ํ† ยท์ง€ํœ˜๊ด€ ๊ฒฐ์ • ๋Œ€๊ธฐ\n", "3. ์„ผ์„œ ์ด์ƒ(๋“œ๋ฆฌํ”„ํŠธยท์ŠคํŒŒ์ดํฌยท๋‹จ์ ˆ)์€ ์žฌ์ธก์ •ยท๊ต์ • ์š”์ฒญ์œผ๋กœ ๋ถ„๋ฆฌํ•ด ๋ถˆํ•„์š”ํ•œ ๋‹จ์ˆ˜๋ฅผ ์ค„์ธ๋‹ค\n", "4. ์‹ค์ œ ์Œ์šฉ ํŒ์ •์€ ๊ณต์‹ ์ˆ˜์งˆ ๊ธฐ์ค€๊ณผ ์˜๋ฌด(์˜ˆ๋ฐฉ์˜ํ•™)ยทํ™˜๊ฒฝ ๋‹ด๋‹น ํ™•์ธ์— ๋”ฐ๋ฅธ๋‹ค(๋ณธ ๋„๊ตฌ๋Š” ํŒ๋‹จ ๋ณด์กฐ)\n", "```\n", "
\n" ] } ], "metadata": { "colab": { "provenance": [] }, "kernelspec": { "display_name": "Python 3", "name": "python3" }, "language_info": { "name": "python" } }, "nbformat": 4, "nbformat_minor": 0 }