{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# ๐ฏ [ISR03] ๋๋ก ๋นํ๋ก๊ทธ ํจํด ์ด์ ํ์ง์ ์ด์ฉ ์ฃผ์ฒด ๋จ์ ๋ฐฉ์ง\n",
"### Google Colab ์ค์ต ํ๊ฒฝ (์ ๋ณดยท๊ฐ์ยท์ ์ฐฐ(ISR) / Level 3)\n",
"\n",
"[](https://colab.research.google.com/github/edunlab/milicontent2/blob/main/uploads/ISR03/ISR03_solution.ipynb)\n",
"\n",
"---\n",
"\n",
"## ๐ 1. ํ๋ก์ ํธ ๊ฐ์ ๋ฐ ๋ฐฐ๊ฒฝ\n",
"\n",
"| ํญ๋ชฉ | ๋ด์ฉ |\n",
"| :--- | :--- |\n",
"| **๊ณผ์ ๋ถ๋ฅ** | ์ ๋ณดยท๊ฐ์ยท์ ์ฐฐ(ISR) (Level 3) |\n",
"| **ํ์ต ์์ ์๊ฐ** | 10๊ฐ ๋ฏธ์
(5์ผ / ์ด 27.5์๊ฐ) |\n",
"| **ํ๊ฒฝ ์ฌ์** | Python 3.10+, numpy, pandas, matplotlib, scipy, scikit-learn |\n",
"\n",
"### ๐ ํ๋ก์ ํธ ๋ชฉํ\n",
"๋ฌ์์-์ฐํฌ๋ผ์ด๋ ์ ์์์๋ ์์ฉ ๋๋ก ์ด ์ ์ฐฐ๊ณผ ๊ณต๊ฒฉ์ ํญ๋๊ฒ ์ฐ์ด๋ฉด์, ๋
ธํํ๊ฑฐ๋ ์์งํ ๋๋ก ๋นํ๋ก๊ทธ๋ฅผ ๋ถ์ํด ์ด์ ๋นํ์ ์ฐพ์๋ด๋ ์ผ์ด ์ค์ํด์ก๋ค. ๊ทธ๋ฌ๋ ๊ฐ์ ์์ฉ ๊ธฐ์ฒด๋ฅผ ์๊ตฐยท๋ฏผ๊ฐยท์ ๋ ์ธ๋ ฅ์ด ๋ชจ๋ ์ฐ๊ธฐ ๋๋ฌธ์, ๋นํ ํน์ฑ๋ง ๋ณด๊ณ ์ด์ฉ ์ฃผ์ฒด๋ฅผ ์ฑ๊ธํ๊ฒ ๋จ์ ํ๋ฉด ์คํ์ผ๋ก ์ด์ด์ง ์ ์๋ค. ๋ณธ ํ๋ก์ ํธ์์๋ ๊ต์ก์ฉ ๊ฐ์ ๋นํ๋ก๊ทธ๋ฅผ ์ฝ๋๋ก ์ง์ ๋ง๋ค์ด ์คํค๋ง ์ ์, GPS ์คํธํ ๋ณด์ ์ ์ฒ๋ฆฌ, ๊ท์น ๊ธฐ๋ฐ 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. ์์ฉ ๋ฐ ๊ตฐ์ฉ ๋๋ก ๋นํ๋ก๊ทธ(GPS, ์๋, ๊ณ ๋, RC์ ์ด) ๋ฐ์ดํฐ ์คํค๋ง ๋ฐ ์์ง ํ๊ฒฝ ๋ถ์\n",
"**์์์๊ฐ**: ์ฝ 1.5์๊ฐ\n",
"\n",
"### ๐ ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ์ํ ์ง์นจ\n",
"#### ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ๋น์์ฑ\n",
"\n",
"์ ์ฅ์์ ๋
ธํ ๋ฐ ์์ง๋ ์์ฉ/๊ตฐ์ฉ ๋๋ก ์ ๋นํ๋ก๊ทธ(GPS ํ
๋ ๋ฉํธ๋ฆฌ, ์๋, ๊ณ ๋, ์กฐ์ข
์ ํธ) ๋ฐ์ดํฐ ๊ตฌ์กฐ๋ฅผ ๋ถ์ํ๊ณ ์คํค๋ง๋ฅผ ์ ์ํ๋ค. ์ค์ ์ ์ ํ์ฅ์์๋ ๋จ์ ํ์ง์ ๊ทธ์น์ง ์๊ณ ์ ์ํ ์ํ ํ๋จ๊ณผ ์ ๋ขฐ๋ ๋์ ์์ฌ๊ฒฐ์ ์ด ํ์์ ์ด๋ฏ๋ก, **๋
ธํยท์์ง๋ ์์ฉ/๊ตฐ์ฉ ๋๋ก ๋นํ๋ก๊ทธ(GPS ์์น, ์๋, ๊ณ ๋, RC ์กฐ์ข
์ ํธ)์ ๊ตฌ์กฐ๋ฅผ ๋ถ์ํ๊ณ , ์์ง ๊ฒฝ์์ ์ ์ ํ๋ผ๋ฏธํฐ๊น์ง ํฌํจํ ๋ฐ์ดํฐ ์คํค๋ง์ ๊ฒ์ฆ๊ธฐ๋ฅผ ๋ง๋ ๋ค.** ๊ณผ์
์ ์์ํ์ฌ ์คํ์ ๋ฐฉ์งํ๊ณ ์์ ๋์๋ ฅ์ ๊ทน๋ํํด์ผ ํฉ๋๋ค.\n",
"\n",
"์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ณธ ๋ฏธ์
์์๋ **1์ด ๋จ์ ๋นํ ๊ธฐ๋ก(์์นยท๊ณ ๋ยท์๋ยท๋ฐฉ์ยท์กฐ์ข
์ ํธ ํ์งยท์์ฑ ์ยทHDOP), ์์ง ๊ฒฝ์(์์ง ๋ฐฉ์ยท์์ง์ยท์๋ณธ ํด์), ๊ธฐ์ฒด ๊ตฌ๋ถ, ์ ์ ํ๋ผ๋ฏธํฐ(๊ธฐ์ยท์์น์ค์ฐจยทํต์ ์ง์ฐ) ํ๋ ์ ์ํ๊ณ ๊ฒ์ฆํ๋** ์ค์ต์ ์ฒด๊ณ์ ์ผ๋ก ์ํํฉ๋๋ค.\n",
"\n",
"ํ์ต์๋ ์ค์ต๊ฐ์ด๋์ ์ ์๋ ์ด 4๊ฐ ์ธ๋ถ ํ๋(๊ธฐ๋ณธ ๊ธฐ๋ฅ ๊ตฌํ โ ๊ฒฐ๊ณผ ์งํ ๊ฒ์ฆ โ ํ๋ผ๋ฏธํฐ ํ๋ ๋ฐ ๋์ ๊ณผ์ )์ ์์ฐจ์ ์ผ๋ก ์งํํ๋ฉฐ ํต์ฌ ํ์ ๊ธฐ์ค์ ๋ฌ์ฑํฉ๋๋ค.\n",
"\n",
"**์ด๋ฒ ๊ฒฐ๊ณผ๋ฌผ:** `data/schema/isr03_input_schema.json ๋ฐ docs/isr03_data_spec.md` ํํ์ ๊ฒฐ๊ณผ๋ฌผ์ ์์ฑํ๋ค.\n",
"\n",
"### ๐ ํ๊ฐ ๋ฐ ๊ฒ์ฆ ๊ธฐ์ค (PASS ์กฐ๊ฑด)\n",
"#### ๋ฏธ์
ํ์ \n",
"\n",
"**ํ๊ฐ ๊ตฌ๋ถ:** ํ์\n",
"\n",
"**ํต์ฌ ํ๊ฐ์์ญ:** ์ ๋ณดยท๊ฐ์ยท์ ์ฐฐ(ISR) ์ค๋ฌด ๋ฐ ๋ฌ์์-์ฐํฌ๋ผ์ด๋ ์ ์ ์ ์ฅ ํ๊ฒฝ๊ณผ์ ๊ตฐ์ฌ์ ๋ถํฉ์ฑ\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": [
"\"\"\"์์ฉยท๊ตฐ์ฉ ๋๋ก ๋นํ๋ก๊ทธ(GPS ์์น, ์๋, ๊ณ ๋, RC ์กฐ์ข
์ ํธ)์ ์์ง ๊ฒฝ์ยท์ ์ ํ๋ผ๋ฏธํฐ์ ๋ฐ์ดํฐ ์คํค๋ง๋ฅผ ์ ์ํ๊ณ , ๊ฐ์ ์ํ ๋ก๊ทธ๋ฅผ ๊ฒ์ฆํ๋ค.\"\"\"\n",
"import hashlib\n",
"import json\n",
"import re\n",
"import numpy as np\n",
"import pandas as pd\n",
"\n",
"# 1) ์คํค๋ง ์ ์: ์์ง ๊ฒฝ์(์ฆ๊ฑฐ ๊ด๋ฆฌ), ๊ธฐ์ฒด ์ ๋ณด, ์ ์ ํ๋ผ๋ฏธํฐ, ๋นํ ๊ธฐ๋ก(1์ด ๋จ์), ๊ท์ ํ๋จ ์ํ\n",
"SCHEMA = {\n",
" \"required\": [\"flight_id\", \"n_records\", \"collection\", \"tactical\", \"attribution\", \"records\"],\n",
" \"fields\": {\n",
" \"flight_id\": {\"type\": str, \"pattern\": r\"^FL\\d{4}$\"},\n",
" \"n_records\": {\"type\": int, \"min\": 1, \"max\": 100000},\n",
" },\n",
" \"collection\": {\n",
" \"source\": {\"type\": str, \"enum\": [\"captured_device\", \"intercepted_telemetry\", \"open_source\"]},\n",
" \"collected_by\": {\"type\": str, \"pattern\": r\"^COL-\\d{2}$\"},\n",
" \"platform_type\": {\"type\": str, \"enum\": [\"commercial\", \"military\", \"unknown\"]},\n",
" \"sha256\": {\"type\": str, \"pattern\": r\"^[0-9a-f]{64}$\"},\n",
" },\n",
" \"tactical\": {\n",
" \"weather\": {\"type\": str, \"enum\": [\"clear\", \"night\", \"windy\", \"rain\"]},\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",
" \"attribution\": {\n",
" \"status\": {\"type\": str, \"enum\": [\"unassessed\"]},\n",
" },\n",
" \"record\": {\n",
" \"t\": {\"type\": int, \"min\": 0},\n",
" \"x_m\": {\"type\": (int, float), \"min\": -20000, \"max\": 20000},\n",
" \"y_m\": {\"type\": (int, float), \"min\": -20000, \"max\": 20000},\n",
" \"alt_m\": {\"type\": (int, float), \"min\": -50, \"max\": 6000},\n",
" \"speed_mps\": {\"type\": (int, float), \"min\": 0, \"max\": 60},\n",
" \"heading_deg\": {\"type\": (int, float), \"min\": 0, \"max\": 360},\n",
" \"rc_link\": {\"type\": (int, float), \"min\": 0, \"max\": 100},\n",
" \"gps_sats\": {\"type\": int, \"min\": 0, \"max\": 40},\n",
" \"hdop\": {\"type\": (int, float), \"min\": 0, \"max\": 50},\n",
" },\n",
"}\n",
"\n",
"\n",
"def check(name, value, rule):\n",
" \"\"\"๊ฐ ํ๋๋ฅผ ๊ท์น(type, enum, min, max, 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 \"pattern\" in rule and not re.match(rule[\"pattern\"], value):\n",
" errs.append(f\"{name}: ํ์ ๋ถ์ผ์น '{value}'\")\n",
" return errs\n",
"\n",
"\n",
"def digest(records):\n",
" return hashlib.sha256(json.dumps(records, sort_keys=True).encode()).hexdigest()\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 (\"collection\", \"tactical\", \"attribution\"):\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",
" for i, r in enumerate(rec.get(\"records\", [])):\n",
" for k, rule in SCHEMA[\"record\"].items():\n",
" if k not in r:\n",
" errs.append(f\"records[{i}].{k}: ํ์ ํ๋ ๋๋ฝ\")\n",
" else:\n",
" errs += check(f\"records[{i}].{k}\", r[k], rule)\n",
" if \"records\" in rec and rec.get(\"collection\", {}).get(\"sha256\") not in (None, digest(rec[\"records\"])):\n",
" errs.append(\"collection.sha256: ๋ก๊ทธ ํด์ ๋ถ์ผ์น(์์ง ํ ๋ณ๊ฒฝ ์์ฌ)\")\n",
" return errs\n",
"\n",
"\n",
"# 2) [๊ต์ก์ฉ๊ฐ์์๋ฃ] ๋นํ๋ก๊ทธ ์ํ 20๊ฑด ์์ฑ (๊ฐ์ ์ขํ, 1์ด ๊ฐ๊ฒฉ 5๊ฐ ๊ธฐ๋ก)\n",
"rng = np.random.default_rng(1)\n",
"logs = []\n",
"for i in range(20):\n",
" x, y, alt, hdg = rng.uniform(-3000, 3000), rng.uniform(-3000, 3000), rng.uniform(80, 150), rng.uniform(0, 360)\n",
" recs = []\n",
" for t in range(5):\n",
" v = float(round(rng.uniform(10, 16), 1))\n",
" x += v * np.sin(np.radians(hdg)); y += v * np.cos(np.radians(hdg))\n",
" recs.append({\"t\": t, \"x_m\": round(float(x), 1), \"y_m\": round(float(y), 1), \"alt_m\": round(float(alt), 1),\n",
" \"speed_mps\": v, \"heading_deg\": round(float(hdg), 1), \"rc_link\": float(round(rng.uniform(70, 95), 1)),\n",
" \"gps_sats\": int(rng.integers(9, 15)), \"hdop\": float(round(rng.uniform(0.6, 1.4), 2))})\n",
" logs.append({\"flight_id\": f\"FL{i:04d}\", \"n_records\": len(recs),\n",
" \"collection\": {\"source\": str(rng.choice(SCHEMA[\"collection\"][\"source\"][\"enum\"])),\n",
" \"collected_by\": f\"COL-{int(rng.integers(1, 20)):02d}\",\n",
" \"platform_type\": str(rng.choice([\"commercial\", \"military\", \"unknown\"])), \"sha256\": digest(recs)},\n",
" \"tactical\": {\"weather\": str(rng.choice([\"clear\", \"night\", \"windy\", \"rain\"])),\n",
" \"position_error_m\": float(round(rng.uniform(2, 30), 1)), \"comm_delay_ms\": int(rng.integers(20, 400))},\n",
" \"attribution\": {\"status\": \"unassessed\"}, \"records\": recs})\n",
"\n",
"# 3) ์ ์ ๋ฐ์ดํฐ ๊ฒ์ฆ\n",
"ok = sum(1 for r in logs if not validate(r))\n",
"print(f\"์ ์ ์ํ {len(logs)}๊ฑด ์ค ๊ฒ์ฆ ํต๊ณผ {ok}๊ฑด, ์ค๋ฅ {len(logs) - ok}๊ฑด (๋ชฉํ: ์ค๋ฅ 0๊ฑด {'๋ฌ์ฑ' if ok == len(logs) else '๋ฏธ๋ฌ'})\")\n",
"\n",
"# 4) ์ค๋ฅ ์ฃผ์
์ํ: ํ์ยท๋ฒ์ยท๋๋ฝยท๋จ์ ๊ธฐ์ฌยทํด์ ๋ถ์ผ์น\n",
"bad = json.loads(json.dumps(logs[0]))\n",
"bad[\"flight_id\"] = \"F-12\"\n",
"bad[\"records\"][0][\"speed_mps\"] = 95.0\n",
"del bad[\"tactical\"][\"comm_delay_ms\"]\n",
"bad[\"attribution\"][\"status\"] = \"hostile\"\n",
"del bad[\"records\"][1][\"rc_link\"]\n",
"print(\"\\n[์ค๋ฅ ์ฃผ์
๊ฒฐ๊ณผ] ์ฃผ์
5์ข
\")\n",
"found = validate(bad)\n",
"for e in found:\n",
" print(\" -\", e)\n",
"print(f\"๊ฒ์ถ {len(found)}๊ฑด (ํด์ ๋ถ์ผ์น๋ ์์ง ํ ๋ก๊ทธ๊ฐ ๋ฐ๋์์์ ์๋ ค ์ฃผ๋ ์ถ๊ฐ ๊ฒ์ถ)\")\n",
"\n",
"# 5) ์คํค๋ง ํ๋ ์ ์์(๋ช
์ธ์ ์ด์)\n",
"rows = [(\"๊ธฐ๋ณธ\", k, r[\"type\"].__name__, str({x: y for x, y in r.items() if x != \"type\"})) for k, r in SCHEMA[\"fields\"].items()]\n",
"for g in (\"collection\", \"tactical\", \"attribution\", \"record\"):\n",
" for k, r in SCHEMA[g].items():\n",
" t = r[\"type\"].__name__ if isinstance(r[\"type\"], type) else \"number\"\n",
" rows.append((g, k, t, str({x: y for x, y in r.items() if x != \"type\"})))\n",
"spec = pd.DataFrame(rows, columns=[\"๊ทธ๋ฃน\", \"ํ๋\", \"์๋ฃํ\", \"๊ท์น\"])\n",
"print(\"\\n[๋ฐ์ดํฐ ๋ช
์ธ ์์ฝ]\")\n",
"print(spec.to_string(index=False))\n",
"print(f\"\\nํ๋ ์ ์ {len(spec)}๊ฐ(์ค๋ช
ยท์๋ฃํยท๊ท์น ๋ชจ๋ ๊ธฐ์ฌ, ์์ฑ๋ 100%). ์ด์ฉ ์ฃผ์ฒด๋ ์์ง ๋จ๊ณ์์ 'unassessed'๋ง ํ์ฉํ๋ค.\")\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"๐ก [์ฐธ๊ณ ] ๋ฏธ์
01 ์์ ์คํ ๊ฒฐ๊ณผ (Output)
\n",
"\n",
"```text\n",
"์ ์ ์ํ 20๊ฑด ์ค ๊ฒ์ฆ ํต๊ณผ 20๊ฑด, ์ค๋ฅ 0๊ฑด (๋ชฉํ: ์ค๋ฅ 0๊ฑด ๋ฌ์ฑ)\n",
"\n",
"[์ค๋ฅ ์ฃผ์
๊ฒฐ๊ณผ] ์ฃผ์
5์ข
\n",
" - flight_id: ํ์ ๋ถ์ผ์น 'F-12'\n",
" - tactical.comm_delay_ms: ํ์ ํ๋ ๋๋ฝ\n",
" - attribution.status: ํ์ฉ๋์ง ์์ ๊ฐ 'hostile'\n",
" - records[0].speed_mps: ์ต๋๊ฐ 60 ์ด๊ณผ\n",
" - records[1].rc_link: ํ์ ํ๋ ๋๋ฝ\n",
" - collection.sha256: ๋ก๊ทธ ํด์ ๋ถ์ผ์น(์์ง ํ ๋ณ๊ฒฝ ์์ฌ)\n",
"๊ฒ์ถ 6๊ฑด (ํด์ ๋ถ์ผ์น๋ ์์ง ํ ๋ก๊ทธ๊ฐ ๋ฐ๋์์์ ์๋ ค ์ฃผ๋ ์ถ๊ฐ ๊ฒ์ถ)\n",
"\n",
"[๋ฐ์ดํฐ ๋ช
์ธ ์์ฝ]\n",
" ๊ทธ๋ฃน ํ๋ ์๋ฃํ ๊ท์น\n",
" ๊ธฐ๋ณธ flight_id str {'pattern': '^FL\\\\d{4}$'}\n",
" ๊ธฐ๋ณธ n_records int {'min': 1, 'max': 100000}\n",
" collection source str {'enum': ['captured_device', 'intercepted_telemetry', 'open_source']}\n",
" collection collected_by str {'pattern': '^COL-\\\\d{2}$'}\n",
" collection platform_type str {'enum': ['commercial', 'military', 'unknown']}\n",
" collection sha256 str {'pattern': '^[0-9a-f]{64}$'}\n",
" tactical weather str {'enum': ['clear', 'night', 'windy', 'rain']}\n",
" tactical position_error_m number {'min': 0, 'max': 500}\n",
" tactical comm_delay_ms number {'min': 0, 'max': 10000}\n",
"attribution status str {'enum': ['unassessed']}\n",
" record t int {'min': 0}\n",
" record x_m number {'min': -20000, 'max': 20000}\n",
" record y_m number {'min': -20000, 'max': 20000}\n",
" record alt_m number {'min': -50, 'max': 6000}\n",
" record speed_mps number {'min': 0, 'max': 60}\n",
" record heading_deg number {'min': 0, 'max': 360}\n",
" record rc_link number {'min': 0, 'max': 100}\n",
" record gps_sats int {'min': 0, 'max': 40}\n",
" record hdop number {'min': 0, 'max': 50}\n",
"\n",
"ํ๋ ์ ์ 19๊ฐ(์ค๋ช
ยท์๋ฃํยท๊ท์น ๋ชจ๋ ๊ธฐ์ฌ, ์์ฑ๋ 100%). ์ด์ฉ ์ฃผ์ฒด๋ ์์ง ๋จ๊ณ์์ 'unassessed'๋ง ํ์ฉํ๋ค.\n",
"```\n",
" \n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## ๐ฏ ๋ฏธ์
02. ๋นํ๋ก๊ทธ ๋ด GPS ์คํธํ ์๊ณก๊ฐ ๋ณด์ ๋ฐ ์๊ณ์ด ํ
๋ ๋ฉํธ๋ฆฌ ์ ์ฒ๋ฆฌ ๋ชจ๋ ๊ตฌํ\n",
"**์์์๊ฐ**: ์ฝ 2.0์๊ฐ\n",
"\n",
"### ๐ ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ์ํ ์ง์นจ\n",
"#### ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ๋น์์ฑ\n",
"\n",
"๋นํ๋ก๊ทธ ๋ด์ GPS ์คํธํ ๋ฐ ์ ์๊ธฐ ๊ฐ์ญ์ ๋ฐ๋ฅธ ์ด์ ์ขํ๋ฅผ ๋ณด์ ํ๊ณ ์๊ณ์ด ๋นํ ๊ถค์ ๋ฐ์ดํฐ๋ฅผ ์ ๊ทํํ๋ ์ ์ฒ๋ฆฌ ๋ชจ๋์ ๊ตฌํํ๋ค. ์ค์ ์ ์ ํ์ฅ์์๋ ๋จ์ ํ์ง์ ๊ทธ์น์ง ์๊ณ ์ ์ํ ์ํ ํ๋จ๊ณผ ์ ๋ขฐ๋ ๋์ ์์ฌ๊ฒฐ์ ์ด ํ์์ ์ด๋ฏ๋ก, **GPS ์คํธํยท์ ์๊ธฐ ๊ฐ์ญยท์ผ์ ๋จ์ ๋ก ์๊ณก๋ ๋นํ๋ก๊ทธ๋ฅผ ๋ณด์ ํ๊ณ ์๊ณ์ด์ 1์ด ๊ฐ๊ฒฉ์ผ๋ก ํ์คํยท์ ๊ทํํ๋ ์ ์ฒ๋ฆฌ ๋ชจ๋์ ๊ตฌํํ๋ค.** ๊ณผ์
์ ์์ํ์ฌ ์คํ์ ๋ฐฉ์งํ๊ณ ์์ ๋์๋ ฅ์ ๊ทน๋ํํด์ผ ํฉ๋๋ค.\n",
"\n",
"์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ณธ ๋ฏธ์
์์๋ **์ค๋ณต ๊ธฐ๋ก ์ ๊ฑฐ์ 1์ด ์ฌํ๋ณธํ, ๊ฒฐ์ธก ๋ณด๊ฐ, ์ด๋ ์ค๊ฐ๊ฐ ๊ธฐ๋ฐ ๊ณ ๋ ์คํ์ดํฌ ์ ๊ฑฐ ๊ตฌํํ๊ณ ๋์์ ๊ฒ์ฆํ๋** ์ค์ต์ ์ฒด๊ณ์ ์ผ๋ก ์ํํฉ๋๋ค.\n",
"\n",
"ํ์ต์๋ ์ค์ต๊ฐ์ด๋์ ์ ์๋ ์ด 5๊ฐ ์ธ๋ถ ํ๋(๊ธฐ๋ณธ ๊ธฐ๋ฅ ๊ตฌํ โ ๊ฒฐ๊ณผ ์งํ ๊ฒ์ฆ โ ํ๋ผ๋ฏธํฐ ํ๋ ๋ฐ ๋์ ๊ณผ์ )์ ์์ฐจ์ ์ผ๋ก ์งํํ๋ฉฐ ํต์ฌ ํ์ ๊ธฐ์ค์ ๋ฌ์ฑํฉ๋๋ค.\n",
"\n",
"**์ด๋ฒ ๊ฒฐ๊ณผ๋ฌผ:** `src/preprocess/isr03_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": [
"\"\"\"๋นํ๋ก๊ทธ์ GPS ์คํธํ ์์น ์ ํยทํ๋ฅ, ์ ์๊ธฐ ๊ฐ์ญ ๊ณ ๋ ์คํ์ดํฌ, ์ผ์ ๋จ์ ๊ฒฐ์ธก, ์ค๋ณต ๊ธฐ๋ก์ ๋ณด์ ํ๊ณ ์๊ณ์ด์ 1์ด ๊ฐ๊ฒฉ์ผ๋ก ํ์คํยท์ ๊ทํํ๋ ์ ์ฒ๋ฆฌ ๋ชจ๋์ ๊ตฌํํด ์ฒ๋ฆฌ ์๋์ ๋ณด์ ํจ๊ณผ๋ฅผ ๊ฒ์ฆํ๋ค.\"\"\"\n",
"import math\n",
"import time\n",
"import numpy as np\n",
"import pandas as pd\n",
"import matplotlib.pyplot as plt\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# [๊ต์ก์ฉ๊ฐ์์๋ฃ] ํฉ์ฑ ๋๋ก ๋นํ๋ก๊ทธ ์์ฑ๊ธฐ (์ค์ ์ขํยท๊ธฐ์ฒดยท๋ถ๋ ์ ๋ณด ์์)\n",
"# ๊ฐ์ ๊ตฌ์ญ(์์ ๊ธฐ์ค ๋ยท๋ถ ๋ฐฉํฅ m ์ขํ)์์ 1์ด ๊ฐ๊ฒฉ ๋นํ๋ก๊ทธ์ ์ ๋ต(์ด์ ๊ตฌ๊ฐยท์ด์ฉ ์ฃผ์ฒด)์ ๋ง๋ ๋ค.\n",
"# ---------------------------------------------------------------------------\n",
"NFZ = {\"x\": 2500.0, \"y\": 2500.0, \"r\": 700.0} # ๊ฐ์ ๋น์ธ๊ฐ ๋นํ๊ตฌ์ญ\n",
"WATCH = {\"x\": -2000.0, \"y\": 1500.0, \"r\": 600.0} # ๊ฐ์ ๊ด์ฌ ๊ตฌ์ญ\n",
"SCENARIOS = [\"sharp_turn\", \"hover\", \"dive\", \"deviation\", \"nfz_entry\",\n",
" \"low_level\", \"rc_silent\", \"infiltration\", \"loiter\", \"speed_burst\"]\n",
"OPERATORS = [\"friendly\", \"civil\", \"hostile\"]\n",
"# ์๋๋ฆฌ์ค๋ณ ์ด์ฉ ์ฃผ์ฒด ๋ถํฌ: ์ผ๋ถ๋ฌ ํฌ๊ฒ ๊ฒน์น๊ฒ ์ค๊ณํ๋ค(๋นํ ํน์ฑ๋ง์ผ๋ก ์ด์ฉ ์ฃผ์ฒด๋ฅผ ๋จ์ ํ ์ ์๋ค).\n",
"OPERATOR_P = {\"normal\": (.45, .47, .08), \"survey\": (.15, .82, .03), \"sharp_turn\": (.40, .35, .25),\n",
" \"hover\": (.25, .45, .30), \"dive\": (.25, .25, .50), \"deviation\": (.30, .40, .30),\n",
" \"nfz_entry\": (.10, .50, .40), \"low_level\": (.35, .15, .50), \"rc_silent\": (.30, .15, .55),\n",
" \"infiltration\": (.12, .08, .80), \"loiter\": (.30, .20, .50), \"speed_burst\": (.30, .20, .50)}\n",
"PLATFORM_P = {\"friendly\": (.40, .55, .05), \"civil\": (.88, .02, .10), \"hostile\": (.55, .30, .15)} # commercial/military/unknown\n",
"FIRMWARE_P = {\"friendly\": (.30, .65, .05), \"civil\": (.85, .10, .05), \"hostile\": (.20, .50, .30)} # stock/custom/unknown\n",
"\n",
"\n",
"def _wrap(a):\n",
" return (a + 180.0) % 360.0 - 180.0\n",
"\n",
"\n",
"def _bearing(p, q):\n",
" return math.degrees(math.atan2(q[0] - p[0], q[1] - p[1])) % 360.0\n",
"\n",
"\n",
"def _away_from_nfz(rng, min_d):\n",
" while True:\n",
" c = rng.uniform(-3800, 3800, 2)\n",
" if np.hypot(c[0] - NFZ[\"x\"], c[1] - NFZ[\"y\"]) > min_d:\n",
" return c\n",
"\n",
"\n",
"def make_flight(fid, kind, rng, n=300, weather=None):\n",
" \"\"\"๋นํ 1ํ = (๋ก๊ทธ DataFrame, ๋ฉํ dict). kind: normal / survey / SCENARIOS ์ค ํ๋\"\"\"\n",
" weather = weather or str(rng.choice([\"clear\", \"night\", \"windy\", \"rain\"], p=[.4, .25, .2, .15]))\n",
" wind = {\"clear\": 1.0, \"night\": 1.0, \"windy\": 2.5, \"rain\": 1.8}[weather]\n",
" v0, alt0 = rng.uniform(11, 17), rng.uniform(70, 150)\n",
" nfz_c = np.array([NFZ[\"x\"], NFZ[\"y\"]])\n",
" if kind == \"infiltration\": # ์๊ฑฐ๋ฆฌ์์ ๋น์ธ๊ฐ ๊ตฌ์ญ ๋ฐฉํฅ์ผ๋ก ์ ๊ณ ๋ ์ง์ง\n",
" ang = rng.uniform(0, 2 * np.pi)\n",
" start = nfz_c + 4000 * np.array([np.cos(ang), np.sin(ang)])\n",
" plan = np.array([start, nfz_c])\n",
" v0, alt0 = rng.uniform(9, 11), rng.uniform(25, 35)\n",
" else:\n",
" if kind == \"nfz_entry\": # ๋น์ธ๊ฐ ๊ตฌ์ญ 1.6km ๋ฐ์์ ์์ฐฐํ๋ค๊ฐ ์ง์
\n",
" ang = rng.uniform(0, 2 * np.pi)\n",
" c = nfz_c + 1600 * np.array([np.cos(ang), np.sin(ang)])\n",
" else:\n",
" c = _away_from_nfz(rng, 2600)\n",
" if kind == \"survey\": # ๋ฏผ๊ฐ ์ธก๋ํ ์๋ณต(์ง๊ทธ์ฌ๊ทธ) ๊ฒฝ๋ก\n",
" w, h = rng.uniform(500, 700), 120.0\n",
" plan = np.array([c + [(-w if i % 4 in (0, 3) else w), i // 2 * h - 360] for i in range(8)])\n",
" else: # ์ฌ๊ฐ ์์ฐฐ ๊ฒฝ๋ก(์จ์ดํฌ์ธํธ 4๊ฐ)\n",
" a, b, th = rng.uniform(300, 600), rng.uniform(200, 400), rng.uniform(0, np.pi)\n",
" R = np.array([[np.cos(th), -np.sin(th)], [np.sin(th), np.cos(th)]])\n",
" plan = c + np.array([[-a, -b], [a, -b], [a, b], [-a, b]]) @ R.T\n",
" s0 = int(rng.integers(80, 160))\n",
" span = {\"sharp_turn\": 30, \"hover\": 60, \"dive\": 8, \"deviation\": 70, \"nfz_entry\": 120, \"low_level\": 120,\n",
" \"rc_silent\": 120, \"infiltration\": n - 20, \"loiter\": 90, \"speed_burst\": 25}.get(kind, 0)\n",
" if kind == \"infiltration\":\n",
" s0 = 20\n",
" win = (s0, s0 + span)\n",
" turns = set(rng.choice(np.arange(s0, s0 + 30), 3, replace=False).tolist()) if kind == \"sharp_turn\" else set()\n",
" # ๋์๋ฅผ ๋ฏธ๋ฆฌ ํ ๋ฒ์ ๋ฝ์ ๋๊ณ (์๋ ํฅ์) 1์ด ๋จ์๋ก ๊ธฐ์ฒด ์ด๋์ ์ ๋ถํ๋ค.\n",
" e_h, e_v, e_a = rng.normal(0, 0.8 * wind, n), rng.normal(0, 0.3 * wind, n), rng.normal(0, 0.4 * wind, n)\n",
" rc_arr = np.clip(rng.normal(rng.uniform(60, 92), 6, n), 0, 100)\n",
" gust = (rng.random(n) < (0.004 if weather == \"windy\" else 0.0)) * rng.uniform(20, 35, n)\n",
" burst, turn_amt = rng.uniform(23, 28), rng.choice([-1, 1], n) * rng.uniform(100, 160, n)\n",
" side = float(rng.choice([-90, 90]))\n",
" low_alt, rng_rc = rng.uniform(35, 55), rng.uniform(0, 20, n) # ์ ๊ณ ๋ ๋ชฉํ, ์กฐ์ข
์ ํธ ๋์ ์ ์์ฌ ์์ ๊ฐ\n",
" x, y, wi = float(plan[0][0]), float(plan[0][1]), 1\n",
" hdg, alt = _bearing((x, y), plan[1]), alt0\n",
" max_rate = 40.0 if kind == \"survey\" else 12.0\n",
" dev_target, X, Y, A, V, HD = None, np.zeros(n), np.zeros(n), np.zeros(n), np.zeros(n), np.zeros(n)\n",
" for t in range(n):\n",
" inw = win[0] <= t < win[1]\n",
" tx, ty = plan[1] if kind == \"infiltration\" else plan[wi % len(plan)]\n",
" spd_cmd, alt_cmd = v0, alt0\n",
" if inw:\n",
" if kind == \"deviation\":\n",
" if dev_target is None:\n",
" r = math.radians(hdg + side)\n",
" dev_target = (x + 900 * math.sin(r), y + 900 * math.cos(r))\n",
" tx, ty = dev_target\n",
" elif kind == \"nfz_entry\":\n",
" tx, ty = NFZ[\"x\"], NFZ[\"y\"]\n",
" elif kind == \"hover\":\n",
" spd_cmd = 0.0\n",
" elif kind == \"speed_burst\":\n",
" spd_cmd = burst\n",
" elif kind == \"low_level\":\n",
" alt_cmd = low_alt\n",
" if kind in (\"rc_silent\", \"infiltration\"):\n",
" rc_arr[t] = rng_rc[t]\n",
" if inw and kind == \"loiter\":\n",
" hdg = (hdg + 9.0 + e_h[t] / wind) % 360\n",
" else:\n",
" d = _wrap(math.degrees(math.atan2(tx - x, ty - y)) - hdg)\n",
" hdg = (hdg + max(-max_rate, min(max_rate, d)) + e_h[t]) % 360\n",
" if t in turns:\n",
" hdg = (hdg + turn_amt[t]) % 360\n",
" v = abs(spd_cmd + e_v[t]) if spd_cmd > 0 else abs(e_v[t] / wind)\n",
" x += v * math.sin(math.radians(hdg)); y += v * math.cos(math.radians(hdg))\n",
" if kind != \"infiltration\" and math.hypot(x - tx, y - ty) < 40 and not (inw and kind in (\"deviation\", \"nfz_entry\")):\n",
" wi += 1\n",
" if inw and kind == \"dive\":\n",
" alt -= 9.0 + e_v[t]\n",
" else:\n",
" alt += max(-3.0, min(3.0, alt_cmd - alt)) + e_a[t]\n",
" alt -= gust[t] # ๋ํ ํ๊ฐ๊ธฐ๋ฅ(์ ์ ๋นํ์์๋ ๋ฐ์)\n",
" if kind == \"dive\" and t >= win[1]:\n",
" alt0 = min(alt0, alt)\n",
" alt = max(alt, 3.0)\n",
" X[t], Y[t], A[t], V[t], HD[t] = x, y, alt, v, hdg\n",
" gps_sd = {\"clear\": 2.0, \"night\": 2.0, \"windy\": 3.0, \"rain\": 4.5}[weather] # ์
์ฒํ์ผ์๋ก GPS ์ก์(๋ค์ค๊ฒฝ๋ก) ์ฆ๊ฐ\n",
" df = pd.DataFrame({\"t\": np.arange(n), \"x\": X + rng.normal(0, gps_sd, n), \"y\": Y + rng.normal(0, gps_sd, n),\n",
" \"alt\": A + rng.normal(0, 0.5, n), \"speed\": V + rng.normal(0, 0.2, n),\n",
" \"heading\": (HD + rng.normal(0, 1.5, n)) % 360, \"rc\": rc_arr.round(1),\n",
" \"sats\": rng.integers(10, 15, n), \"hdop\": rng.uniform(0.6, 1.2, n).round(2),\n",
" \"x_true\": X, \"y_true\": Y})\n",
" df[\"anomaly\"] = (df[\"t\"] >= win[0]) & (df[\"t\"] < win[1]) & (kind in SCENARIOS)\n",
" op = OPERATORS[int(rng.choice(3, p=OPERATOR_P[kind]))]\n",
" meta = {\"id\": fid, \"kind\": kind, \"weather\": weather, \"operator\": op, \"plan\": plan, \"window\": win if kind in SCENARIOS else None,\n",
" \"platform\": [\"commercial\", \"military\", \"unknown\"][int(rng.choice(3, p=PLATFORM_P[op]))],\n",
" \"firmware\": [\"stock\", \"custom\", \"unknown\"][int(rng.choice(3, p=FIRMWARE_P[op]))]}\n",
" return df, meta\n",
"\n",
"\n",
"def corrupt(df, rng, spoof=True, emi=True, drop=True, dup=True):\n",
" \"\"\"์ ์ฅ ์ก์ ์ฃผ์
: GPS ์คํธํ(์์น ์ ํยทํ๋ฅ), ์ ์๊ธฐ ๊ฐ์ญ(๊ณ ๋ ์คํ์ดํฌ), ์ผ์ ๋จ์ (๊ฒฐ์ธก), ์ค๋ณต ๊ธฐ๋ก.\"\"\"\n",
" d = df.copy()\n",
" d[\"spoof_true\"] = False\n",
" if spoof:\n",
" a = int(rng.integers(40, len(d) - 60)); L = int(rng.integers(25, 45))\n",
" th = rng.uniform(0, 2 * np.pi)\n",
" drift = rng.uniform(22, 28) * np.arange(L)[:, None] * np.array([np.cos(th), np.sin(th)])\n",
" jump = rng.uniform(250, 400) * np.array([np.cos(th + 1), np.sin(th + 1)])\n",
" d.loc[a:a + L - 1, [\"x\", \"y\"]] += drift + jump\n",
" d.loc[a:a + L - 1, \"spoof_true\"] = True\n",
" if emi:\n",
" idx = rng.choice(len(d), 6, replace=False)\n",
" d.loc[idx, \"alt\"] += rng.choice([-1, 1], 6) * rng.uniform(60, 150, 6)\n",
" if drop:\n",
" a = int(rng.integers(10, len(d) - 20))\n",
" d.loc[a:a + int(rng.integers(4, 9)), [\"x\", \"y\", \"alt\", \"speed\", \"heading\"]] = np.nan\n",
" d = d.drop(index=rng.choice(len(d), 5, replace=False))\n",
" if dup:\n",
" d = pd.concat([d, d.sample(5, random_state=int(rng.integers(1e6)))]).sort_values(\"t\", kind=\"stable\")\n",
" return d.reset_index(drop=True)\n",
"\n",
"\n",
"def make_dataset(n=200, seed=7, anomaly_ratio=0.5, scenarios=SCENARIOS):\n",
" rng = np.random.default_rng(seed)\n",
" out = []\n",
" for i in range(n):\n",
" if i % 10 < anomaly_ratio * 10:\n",
" kind = scenarios[(i // 10 * int(anomaly_ratio * 10) + i % 10) % len(scenarios)]\n",
" else:\n",
" kind = \"survey\" if i % 10 == 9 else \"normal\"\n",
" df, meta = make_flight(f\"FL{i:04d}\", kind, rng)\n",
" out.append((df, meta))\n",
" return out\n",
"# ---------------------------------------------------------------------------\n",
"\n",
"# ---- ์ ์ฒ๋ฆฌยท๊ท์น ํ์ง ๊ณตํต ํจ์ ----\n",
"SPOOF_THR = {\"clear\": 12.0, \"night\": 12.0, \"windy\": 15.0, \"rain\": 19.0} # ๊ธฐ์๋ณ GPS-์ถ์ธกํญ๋ฒ ๋ถ์ผ์น ํ์ฉ์น(m)\n",
"\n",
"\n",
"def clean(df, weather=\"clear\"):\n",
" \"\"\"์ ์ฒ๋ฆฌ: ์ค๋ณต ์ ๊ฑฐยท์ ๋ ฌ -> 1์ด ์ฌํ๋ณธํ -> ๊ฒฐ์ธก ๋ณด๊ฐ -> ๊ณ ๋ ์คํ์ดํฌ ์ ๊ฑฐ -> GPS ์คํธํ ๊ตฌ๊ฐ ์ถ์ธกํญ๋ฒ ๋ณด์ .\"\"\"\n",
" d = df.drop_duplicates(\"t\").sort_values(\"t\").set_index(\"t\")\n",
" d = d.reindex(np.arange(int(d.index.min()), int(d.index.max()) + 1))\n",
" n_missing = int(d[\"x\"].isna().sum())\n",
" num = [\"x\", \"y\", \"alt\", \"speed\", \"rc\", \"sats\", \"hdop\"]\n",
" d[num] = d[num].interpolate(limit_direction=\"both\")\n",
" hd = d[\"heading\"].dropna() # ๋ฐฉ์๋ 0/360 ๊ฒฝ๊ณ๋ฅผ ํ์ด์ ๋ณด๊ฐ\n",
" un = pd.Series(np.degrees(np.unwrap(np.radians(hd.to_numpy(float)))), index=hd.index)\n",
" d[\"heading\"] = un.reindex(d.index).interpolate(limit_direction=\"both\").to_numpy() % 360\n",
" med = d[\"alt\"].rolling(5, center=True, min_periods=1).median()\n",
" spike = (d[\"alt\"] - med).abs() > 25\n",
" d.loc[spike, \"alt\"] = med[spike]\n",
" x, y = d[\"x\"].to_numpy(float), d[\"y\"].to_numpy(float)\n",
" r = np.radians(d[\"heading\"].to_numpy(float)); v = d[\"speed\"].to_numpy(float)\n",
" dr = np.c_[v * np.sin(r), v * np.cos(r)] # ๋ณด๊ณ ์๋ยท๋ฐฉ์๋ก ๊ณ์ฐํ 1์ด ์ด๋๋(์ถ์ธกํญ๋ฒ)\n",
" xc, yc, flag = x.copy(), y.copy(), np.zeros(len(x), bool)\n",
" thr = SPOOF_THR.get(weather, 12.0)\n",
" ok_run = 0\n",
" for i in range(1, len(x)):\n",
" px, py = xc[i - 1] + dr[i, 0], yc[i - 1] + dr[i, 1] # ์ถ์ธกํญ๋ฒ ์์ ์์น\n",
" step_ok = np.hypot(x[i] - x[i - 1] - dr[i, 0], y[i] - y[i - 1] - dr[i, 1]) <= thr\n",
" ok_run = ok_run + 1 if step_ok else 0\n",
" if flag[i - 1]:\n",
" # ์คํธํ ๊ตฌ๊ฐ: GPS๊ฐ ์์ ์์น๋ก ๋์์ค๊ฑฐ๋, GPS ์ด๋๋์ด 5์ด ์ฐ์ ๋ฌผ๋ฆฌ์ ์ผ๋ก ๋ง์ผ๋ฉด GPS๋ก ๋ณต๊ท\n",
" if np.hypot(x[i] - px, y[i] - py) < thr * 2 or ok_run >= 5:\n",
" xc[i], yc[i] = x[i], y[i]\n",
" else:\n",
" xc[i], yc[i], flag[i] = px, py, True\n",
" elif not step_ok:\n",
" xc[i], yc[i], flag[i] = px, py, True # GPS ์ด๋๋์ด ๋ฌผ๋ฆฌ์ ์ผ๋ก ๋ง์ง ์์ -> ์คํธํ ์์ฌ\n",
" else:\n",
" xc[i], yc[i] = xc[i - 1] + x[i] - x[i - 1], yc[i - 1] + y[i] - y[i - 1]\n",
" d[\"x\"], d[\"y\"], d[\"spoof_flag\"] = xc, yc, flag\n",
" d = d.reset_index()\n",
" d.attrs.update({\"missing\": n_missing, \"spikes\": int(spike.sum()), \"spoof_pts\": int(flag.sum())})\n",
" return d\n",
"\n",
"\n",
"def xtrack(px, py, plan):\n",
" \"\"\"๊ณํ ๊ฒฝ๋ก(์จ์ดํฌ์ธํธ ํ๊ณก์ ) ๋๋น ์ดํ ๊ฑฐ๋ฆฌ(m).\"\"\"\n",
" P = np.asarray(plan, float)\n",
" segs = [(P[i], P[(i + 1) % len(P)]) for i in range(len(P) if len(P) > 2 else 1)]\n",
" best = np.full(len(px), np.inf)\n",
" for a, b in segs:\n",
" ab = b - a\n",
" u = np.clip(((px - a[0]) * ab[0] + (py - a[1]) * ab[1]) / max(ab @ ab, 1e-9), 0, 1)\n",
" best = np.minimum(best, np.hypot(px - a[0] - u * ab[0], py - a[1] - u * ab[1]))\n",
" return best\n",
"\n",
"\n",
"RULES = {\"turn_rate\": 45.0, \"hover_speed\": 1.5, \"hover_sec\": 20, \"dive_m\": 30.0, \"xtrack_m\": 250.0}\n",
"\n",
"\n",
"def rule_flags(d, plan, R=RULES):\n",
" \"\"\"๊ท์น ๊ธฐ๋ฐ ์ด์ ๋นํ ํ์ (๊ธฐ๋ก ๋จ์). ๊ธ์ ํยทํธ๋ฒ๋งยท๊ธ๊ฐํยท๊ฒฝ๋ก ์ดํยท๋น์ธ๊ฐ ๊ตฌ์ญ ์ง์
.\"\"\"\n",
" hr = np.abs(_wrap(np.diff(d[\"heading\"].to_numpy(float), prepend=d[\"heading\"].iloc[0])))\n",
" slow = (d[\"speed\"] < R[\"hover_speed\"]).to_numpy()\n",
" run = np.zeros(len(slow), int)\n",
" for i in range(len(slow)):\n",
" run[i] = run[i - 1] + 1 if slow[i] and i else int(slow[i])\n",
" hover = np.zeros(len(slow), bool)\n",
" for i in np.where(run >= R[\"hover_sec\"])[0]:\n",
" hover[i - R[\"hover_sec\"] + 1:i + 1] = True\n",
" alt = d[\"alt\"].to_numpy(float)\n",
" drop = alt - np.r_[alt[5:], np.full(5, alt[-1])]\n",
" return {\"๊ธ์ ํ\": hr > R[\"turn_rate\"], \"ํธ๋ฒ๋ง\": hover, \"๊ธ๊ฐํ\": drop > R[\"dive_m\"],\n",
" \"๊ฒฝ๋ก์ดํ\": xtrack(d[\"x\"].to_numpy(float), d[\"y\"].to_numpy(float), plan) > R[\"xtrack_m\"],\n",
" \"๋น์ธ๊ฐ๊ตฌ์ญ\": np.hypot(d[\"x\"] - NFZ[\"x\"], d[\"y\"] - NFZ[\"y\"]).to_numpy() < NFZ[\"r\"]}\n",
"\n",
"\n",
"def normalize(d, stats):\n",
" \"\"\"ํ์ต ๊ตฌ๊ฐ ํต๊ณ๋ก z-์ ๊ทํ(๋ชจ๋ธ ์
๋ ฅ์ฉ). ์๋ณธ ๋จ์ ๊ฐ์ ๊ทธ๋๋ก ๋๋ค.\"\"\"\n",
" out = d.copy()\n",
" for c, (m, s) in stats.items():\n",
" out[c + \"_z\"] = (out[c] - m) / s\n",
" return out\n",
"\n",
"\n",
"def pos_err(clean_df, raw_df):\n",
" tr = raw_df.drop_duplicates(\"t\").set_index(\"t\")[[\"x_true\", \"y_true\"]].reindex(clean_df[\"t\"])\n",
" return np.hypot(clean_df[\"x\"].to_numpy() - tr[\"x_true\"].to_numpy(), clean_df[\"y\"].to_numpy() - tr[\"y_true\"].to_numpy())\n",
"\n",
"\n",
"data = make_dataset(100, seed=7)\n",
"rng = np.random.default_rng(3)\n",
"noisy = [(corrupt(df, rng), df, m) for df, m in data]\n",
"\n",
"# 1) ์ฒ๋ฆฌ ์๋์ ์ค๋ฅ์จ\n",
"t0, fails, cleaned = time.perf_counter(), 0, []\n",
"for c, df, m in noisy:\n",
" try:\n",
" cleaned.append(clean(c, m[\"weather\"]))\n",
" except Exception:\n",
" fails += 1\n",
"dt = time.perf_counter() - t0\n",
"n_rec = sum(len(c) for c, _, _ in noisy)\n",
"nan_left = sum(int(d[[\"x\", \"y\", \"alt\", \"speed\", \"heading\"]].isna().sum().sum()) for d in cleaned)\n",
"print(f\"์ ์ฒ๋ฆฌ {n_rec:,}๊ฑด, {n_rec / dt:,.0f}๊ฑด/์ด (๋ชฉํ 1,000๊ฑด/์ด ์ด์), ์ฒ๋ฆฌ ์คํจ {fails}๊ฑด, ๋จ์ ๊ฒฐ์ธก {nan_left}๊ฑด -> ์ ์ฒ๋ฆฌ ์ค๋ฅ์จ {fails / len(noisy):.0%}\")\n",
"\n",
"# 2) ๋ณด์ ํจ๊ณผ: ์ ๋ต ์์น์์ ๊ฑฐ๋ฆฌ ์ค์ฐจ, ์คํธํ ๊ตฌ๊ฐ ๊ฒ์ถ\n",
"raw_err = np.concatenate([np.hypot(c[\"x\"] - c[\"x_true\"], c[\"y\"] - c[\"y_true\"]).dropna() for c, _, _ in noisy])\n",
"cln_err = np.concatenate([pos_err(d, c) for d, (c, _, _) in zip(cleaned, noisy)])\n",
"cln_err = cln_err[~np.isnan(cln_err)] # ์ญ์ ๋ ๊ธฐ๋ก(์ ๋ต ์์)์ ํ๊ฐ์์ ์ ์ธ\n",
"tp = fp = fn = 0\n",
"for d, (c, _, _) in zip(cleaned, noisy):\n",
" truth = c.drop_duplicates(\"t\").set_index(\"t\")[\"spoof_true\"].reindex(d[\"t\"]).fillna(False).to_numpy(bool)\n",
" tp += int((d[\"spoof_flag\"].to_numpy() & truth).sum()); fp += int((d[\"spoof_flag\"].to_numpy() & ~truth).sum()); fn += int((~d[\"spoof_flag\"].to_numpy() & truth).sum())\n",
"print(f\"์์น ์ค์ฐจ(ํ๊ท /95%): ๋ณด์ ์ {raw_err.mean():.1f} / {np.percentile(raw_err, 95):.1f} m -> ๋ณด์ ํ {cln_err.mean():.1f} / {np.percentile(cln_err, 95):.1f} m\")\n",
"print(f\"์คํธํ ๊ตฌ๊ฐ ๊ฒ์ถ: ์ฌํ์จ {tp / (tp + fn):.1%}, ์ ๋ฐ๋ {tp / max(tp + fp, 1):.1%}\")\n",
"miss = sum(d.attrs[\"missing\"] for d in cleaned)\n",
"print(f\"๊ฒฐ์ธก(์ผ์ ๋จ์ ยท๋๋ฝ) {miss}๊ฑด ์ค ๋ณด์ {miss - 0}๊ฑด, ๊ฒฐ์ธก์น ๋ณด์ ์จ 100%, ๊ณ ๋ ์คํ์ดํฌ ์ ๊ฑฐ {sum(d.attrs['spikes'] for d in cleaned)}๊ฑด\")\n",
"\n",
"# 3) ๊ธฐ์๋ณ ์๊ณ๊ฐ ์กฐ์ ํจ๊ณผ: ์ก์ ๋ก๊ทธ์์ ์คํธํ์ด ์๋ ๊ธฐ๋ก์ ์คํธํ์ผ๋ก ์๋ชป ํ์ํ ๋น์จ\n",
"print(\"\\n๊ธฐ์ ๊ณ ์ ์๊ณ๊ฐ(12m) ์คํ์์จ ๊ธฐ์๋ณ ์๊ณ๊ฐ ์คํ์์จ\")\n",
"for w in [\"clear\", \"night\", \"windy\", \"rain\"]:\n",
" sub = [(c, m) for c, _, m in noisy if m[\"weather\"] == w]\n",
" res = []\n",
" for thr_w in (\"clear\", w):\n",
" bad = tot = 0\n",
" for c, m in sub:\n",
" d = clean(c, thr_w)\n",
" truth = c.drop_duplicates(\"t\").set_index(\"t\")[\"spoof_true\"].reindex(d[\"t\"]).fillna(False).to_numpy(bool)\n",
" bad += int((d[\"spoof_flag\"].to_numpy() & ~truth).sum()); tot += int((~truth).sum())\n",
" res.append(bad / tot)\n",
" print(f\"{w:6s} {res[0]:22.2%} {res[1]:20.2%}\")\n",
"\n",
"# 4) ๋จ์ ํ
์คํธ 5์ข
\n",
"d0 = cleaned[0]; c0, df0, _ = noisy[0]\n",
"spk = c0.drop_duplicates(\"t\").set_index(\"t\")[\"alt\"].sub(df0.set_index(\"t\")[\"alt\"]).abs() > 50\n",
"tests = {\n",
" \"์ค๋ณต ๊ธฐ๋ก ์ ๊ฑฐ\": d0[\"t\"].is_unique,\n",
" \"1์ด ๊ฐ๊ฒฉ ์ฌํ๋ณธํ\": bool((np.diff(d0[\"t\"]) == 1).all()),\n",
" \"๊ฒฐ์ธก 0๊ฑด\": not d0[[\"x\", \"y\", \"alt\", \"speed\", \"heading\"]].isna().any().any(),\n",
" \"๊ณ ๋ ์คํ์ดํฌ ์ ๊ฑฐ(์๊ฐ ๋๋น 10m ์ด๋ด)\": bool((d0.set_index(\"t\")[\"alt\"] - df0.set_index(\"t\")[\"alt\"]).abs()[spk[spk].index].max() < 10),\n",
" \"์คํธํ ๊ตฌ๊ฐ ์์น ์ค์ฐจ 30m ์ด๋ด\": bool(pos_err(d0, c0)[c0.drop_duplicates(\"t\").set_index(\"t\")[\"spoof_true\"].reindex(d0[\"t\"]).fillna(False).to_numpy(bool)].max() < 30),\n",
"}\n",
"for k, v in tests.items():\n",
" print(f\"[{'PASS' if v else 'FAIL'}] {k}\")\n",
"print(f\"๋จ์ ํ
์คํธ {sum(tests.values())}/5 ํต๊ณผ\")\n",
"\n",
"# 5) ์ ๊ทํ(ํ์ต ๊ตฌ๊ฐ ํต๊ณ ๊ธฐ์ค)\n",
"cols = [\"alt\", \"speed\", \"rc\"]\n",
"allc = pd.concat(cleaned[:70])\n",
"stats = {c: (allc[c].mean(), allc[c].std()) for c in cols}\n",
"nz = normalize(cleaned[80], stats)\n",
"print(\"์ ๊ทํ ์์(FL0080) ํ๊ท :\", {c + \"_z\": round(float(nz[c + \"_z\"].mean()), 2) for c in cols})\n",
"\n",
"# 6) ์ ํ ๋น๊ต ๊ทธ๋ฆผ\n",
"fig, ax = plt.subplots(1, 2, figsize=(12, 5))\n",
"ax[0].plot(c0[\"x\"], c0[\"y\"], \".\", ms=3, color=\"tab:red\", label=\"raw GPS (spoofed)\")\n",
"ax[0].plot(d0[\"x\"], d0[\"y\"], \"-\", color=\"tab:blue\", label=\"cleaned\")\n",
"ax[0].plot(df0[\"x_true\"], df0[\"y_true\"], \":\", color=\"k\", label=\"true path\")\n",
"ax[0].set_title(\"GPS spoofing correction (synthetic)\"); ax[0].set_xlabel(\"East (m)\"); ax[0].set_ylabel(\"North (m)\"); ax[0].legend(); ax[0].axis(\"equal\")\n",
"ax[1].plot(c0[\"t\"], c0[\"alt\"], \".\", ms=3, color=\"tab:red\", label=\"raw altitude\")\n",
"ax[1].plot(d0[\"t\"], d0[\"alt\"], \"-\", color=\"tab:blue\", label=\"cleaned\")\n",
"ax[1].set_title(\"Altitude spike / dropout correction\"); ax[1].set_xlabel(\"time (s)\"); ax[1].set_ylabel(\"altitude (m)\"); ax[1].legend()\n",
"plt.tight_layout(); plt.show()\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"๐ก [์ฐธ๊ณ ] ๋ฏธ์
02 ์์ ์คํ ๊ฒฐ๊ณผ (Output)
\n",
"\n",
"```text\n",
"์ ์ฒ๋ฆฌ 30,000๊ฑด, 34,645๊ฑด/์ด (๋ชฉํ 1,000๊ฑด/์ด ์ด์), ์ฒ๋ฆฌ ์คํจ 0๊ฑด, ๋จ์ ๊ฒฐ์ธก 0๊ฑด -> ์ ์ฒ๋ฆฌ ์ค๋ฅ์จ 0%\n",
"์์น ์ค์ฐจ(ํ๊ท /95%): ๋ณด์ ์ 81.8 / 715.8 m -> ๋ณด์ ํ 3.3 / 7.7 m\n",
"์คํธํ ๊ตฌ๊ฐ ๊ฒ์ถ: ์ฌํ์จ 100.0%, ์ ๋ฐ๋ 95.6%\n",
"๊ฒฐ์ธก(์ผ์ ๋จ์ ยท๋๋ฝ) 1198๊ฑด ์ค ๋ณด์ 1198๊ฑด, ๊ฒฐ์ธก์น ๋ณด์ ์จ 100%, ๊ณ ๋ ์คํ์ดํฌ ์ ๊ฑฐ 578๊ฑด\n",
"\n",
"๊ธฐ์ ๊ณ ์ ์๊ณ๊ฐ(12m) ์คํ์์จ ๊ธฐ์๋ณ ์๊ณ๊ฐ ์คํ์์จ\n",
"clear 0.44% 0.44%\n",
"night 0.40% 0.40%\n",
"windy 2.28% 0.51%\n",
"rain 12.88% 1.49%\n",
"[PASS] ์ค๋ณต ๊ธฐ๋ก ์ ๊ฑฐ\n",
"[PASS] 1์ด ๊ฐ๊ฒฉ ์ฌํ๋ณธํ\n",
"[PASS] ๊ฒฐ์ธก 0๊ฑด\n",
"[PASS] ๊ณ ๋ ์คํ์ดํฌ ์ ๊ฑฐ(์๊ฐ ๋๋น 10m ์ด๋ด)\n",
"[PASS] ์คํธํ ๊ตฌ๊ฐ ์์น ์ค์ฐจ 30m ์ด๋ด\n",
"๋จ์ ํ
์คํธ 5/5 ํต๊ณผ\n",
"์ ๊ทํ ์์(FL0080) ํ๊ท : {'alt_z': 0.12, 'speed_z': 1.07, 'rc_z': -0.45}\n",
"```\n",
" \n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## ๐ฏ ๋ฏธ์
03. ๋๋ก ํต์ ๋นํ ํจํด ์ ์ ๋ฐ ๊ท์น ๊ธฐ๋ฐ ์ด์ ๋นํ(๊ธ์ ํยทํธ๋ฒ๋ง) ํ์ง Baseline ๊ตฌํ\n",
"**์์์๊ฐ**: ์ฝ 2.5์๊ฐ\n",
"\n",
"### ๐ ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ์ํ ์ง์นจ\n",
"#### ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ๋น์์ฑ\n",
"\n",
"ํต์์ ์ธ ์ ์ฐฐ ๋นํ ํจํด์ ์ ์ํ๊ณ ์์ ๊ฒฝ๋ก ์ดํ, ๊ธ๊ฒฉํ ๊ณ ๋ ํ๊ฐ ๋ฑ ์ด์ ๊ถค์ ์ 1์ฐจ ์๋ณํ๋ Baseline ๊ท์น ๊ธฐ๋ฐ ๋ชจ๋ธ์ ๊ตฌํํ๋ค. ์ค์ ์ ์ ํ์ฅ์์๋ ๋จ์ ํ์ง์ ๊ทธ์น์ง ์๊ณ ์ ์ํ ์ํ ํ๋จ๊ณผ ์ ๋ขฐ๋ ๋์ ์์ฌ๊ฒฐ์ ์ด ํ์์ ์ด๋ฏ๋ก, **ํต์ ์ ์ฐฐ ๋นํ ํจํด์ ์์น ๊ธฐ์ค์ผ๋ก ์ ์ํ๊ณ ๊ธ์ ํยทํธ๋ฒ๋งยท๊ธ๊ฐํยท๊ฒฝ๋ก ์ดํยท๋น์ธ๊ฐ ๊ตฌ์ญ ์ง์
์ 1์ฐจ ์๋ณํ๋ ๊ท์น ๊ธฐ๋ฐ Baseline์ ๊ตฌํํ๋ค.** ๊ณผ์
์ ์์ํ์ฌ ์คํ์ ๋ฐฉ์งํ๊ณ ์์ ๋์๋ ฅ์ ๊ทน๋ํํด์ผ ํฉ๋๋ค.\n",
"\n",
"์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ณธ ๋ฏธ์
์์๋ **์ ํ์จ, ์ ์ ์ ์ง ์ง์์๊ฐ, 5์ด๊ฐ ๊ณ ๋ ํ๊ฐ๋, ๊ณํ ๊ฒฝ๋ก ๋๋น ์ดํ ๊ฑฐ๋ฆฌ, ๋น์ธ๊ฐ ๊ตฌ์ญ ์ง์
์ฌ๋ถ๋ฅผ ๊ท์น์ผ๋ก ๋ง๋ค์ด ๊ธฐ๋ก ๋จ์๋ก ํ์ ํ๊ณ ๋นํ ๋จ์๋ก ๋ชจ์๋ค ๊ณผ์ ์ ์งํํ๋** ์ค์ต์ ์ฒด๊ณ์ ์ผ๋ก ์ํํฉ๋๋ค.\n",
"\n",
"ํ์ต์๋ ์ค์ต๊ฐ์ด๋์ ์ ์๋ ์ด 5๊ฐ ์ธ๋ถ ํ๋(๊ธฐ๋ณธ ๊ธฐ๋ฅ ๊ตฌํ โ ๊ฒฐ๊ณผ ์งํ ๊ฒ์ฆ โ ํ๋ผ๋ฏธํฐ ํ๋ ๋ฐ ๋์ ๊ณผ์ )์ ์์ฐจ์ ์ผ๋ก ์งํํ๋ฉฐ ํต์ฌ ํ์ ๊ธฐ์ค์ ๋ฌ์ฑํฉ๋๋ค.\n",
"\n",
"**์ด๋ฒ ๊ฒฐ๊ณผ๋ฌผ:** `src/models/isr03_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 math\n",
"import time\n",
"import numpy as np\n",
"import pandas as pd\n",
"import matplotlib.pyplot as plt\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# [๊ต์ก์ฉ๊ฐ์์๋ฃ] ํฉ์ฑ ๋๋ก ๋นํ๋ก๊ทธ ์์ฑ๊ธฐ (์ค์ ์ขํยท๊ธฐ์ฒดยท๋ถ๋ ์ ๋ณด ์์)\n",
"# ๊ฐ์ ๊ตฌ์ญ(์์ ๊ธฐ์ค ๋ยท๋ถ ๋ฐฉํฅ m ์ขํ)์์ 1์ด ๊ฐ๊ฒฉ ๋นํ๋ก๊ทธ์ ์ ๋ต(์ด์ ๊ตฌ๊ฐยท์ด์ฉ ์ฃผ์ฒด)์ ๋ง๋ ๋ค.\n",
"# ---------------------------------------------------------------------------\n",
"NFZ = {\"x\": 2500.0, \"y\": 2500.0, \"r\": 700.0} # ๊ฐ์ ๋น์ธ๊ฐ ๋นํ๊ตฌ์ญ\n",
"WATCH = {\"x\": -2000.0, \"y\": 1500.0, \"r\": 600.0} # ๊ฐ์ ๊ด์ฌ ๊ตฌ์ญ\n",
"SCENARIOS = [\"sharp_turn\", \"hover\", \"dive\", \"deviation\", \"nfz_entry\",\n",
" \"low_level\", \"rc_silent\", \"infiltration\", \"loiter\", \"speed_burst\"]\n",
"OPERATORS = [\"friendly\", \"civil\", \"hostile\"]\n",
"# ์๋๋ฆฌ์ค๋ณ ์ด์ฉ ์ฃผ์ฒด ๋ถํฌ: ์ผ๋ถ๋ฌ ํฌ๊ฒ ๊ฒน์น๊ฒ ์ค๊ณํ๋ค(๋นํ ํน์ฑ๋ง์ผ๋ก ์ด์ฉ ์ฃผ์ฒด๋ฅผ ๋จ์ ํ ์ ์๋ค).\n",
"OPERATOR_P = {\"normal\": (.45, .47, .08), \"survey\": (.15, .82, .03), \"sharp_turn\": (.40, .35, .25),\n",
" \"hover\": (.25, .45, .30), \"dive\": (.25, .25, .50), \"deviation\": (.30, .40, .30),\n",
" \"nfz_entry\": (.10, .50, .40), \"low_level\": (.35, .15, .50), \"rc_silent\": (.30, .15, .55),\n",
" \"infiltration\": (.12, .08, .80), \"loiter\": (.30, .20, .50), \"speed_burst\": (.30, .20, .50)}\n",
"PLATFORM_P = {\"friendly\": (.40, .55, .05), \"civil\": (.88, .02, .10), \"hostile\": (.55, .30, .15)} # commercial/military/unknown\n",
"FIRMWARE_P = {\"friendly\": (.30, .65, .05), \"civil\": (.85, .10, .05), \"hostile\": (.20, .50, .30)} # stock/custom/unknown\n",
"\n",
"\n",
"def _wrap(a):\n",
" return (a + 180.0) % 360.0 - 180.0\n",
"\n",
"\n",
"def _bearing(p, q):\n",
" return math.degrees(math.atan2(q[0] - p[0], q[1] - p[1])) % 360.0\n",
"\n",
"\n",
"def _away_from_nfz(rng, min_d):\n",
" while True:\n",
" c = rng.uniform(-3800, 3800, 2)\n",
" if np.hypot(c[0] - NFZ[\"x\"], c[1] - NFZ[\"y\"]) > min_d:\n",
" return c\n",
"\n",
"\n",
"def make_flight(fid, kind, rng, n=300, weather=None):\n",
" \"\"\"๋นํ 1ํ = (๋ก๊ทธ DataFrame, ๋ฉํ dict). kind: normal / survey / SCENARIOS ์ค ํ๋\"\"\"\n",
" weather = weather or str(rng.choice([\"clear\", \"night\", \"windy\", \"rain\"], p=[.4, .25, .2, .15]))\n",
" wind = {\"clear\": 1.0, \"night\": 1.0, \"windy\": 2.5, \"rain\": 1.8}[weather]\n",
" v0, alt0 = rng.uniform(11, 17), rng.uniform(70, 150)\n",
" nfz_c = np.array([NFZ[\"x\"], NFZ[\"y\"]])\n",
" if kind == \"infiltration\": # ์๊ฑฐ๋ฆฌ์์ ๋น์ธ๊ฐ ๊ตฌ์ญ ๋ฐฉํฅ์ผ๋ก ์ ๊ณ ๋ ์ง์ง\n",
" ang = rng.uniform(0, 2 * np.pi)\n",
" start = nfz_c + 4000 * np.array([np.cos(ang), np.sin(ang)])\n",
" plan = np.array([start, nfz_c])\n",
" v0, alt0 = rng.uniform(9, 11), rng.uniform(25, 35)\n",
" else:\n",
" if kind == \"nfz_entry\": # ๋น์ธ๊ฐ ๊ตฌ์ญ 1.6km ๋ฐ์์ ์์ฐฐํ๋ค๊ฐ ์ง์
\n",
" ang = rng.uniform(0, 2 * np.pi)\n",
" c = nfz_c + 1600 * np.array([np.cos(ang), np.sin(ang)])\n",
" else:\n",
" c = _away_from_nfz(rng, 2600)\n",
" if kind == \"survey\": # ๋ฏผ๊ฐ ์ธก๋ํ ์๋ณต(์ง๊ทธ์ฌ๊ทธ) ๊ฒฝ๋ก\n",
" w, h = rng.uniform(500, 700), 120.0\n",
" plan = np.array([c + [(-w if i % 4 in (0, 3) else w), i // 2 * h - 360] for i in range(8)])\n",
" else: # ์ฌ๊ฐ ์์ฐฐ ๊ฒฝ๋ก(์จ์ดํฌ์ธํธ 4๊ฐ)\n",
" a, b, th = rng.uniform(300, 600), rng.uniform(200, 400), rng.uniform(0, np.pi)\n",
" R = np.array([[np.cos(th), -np.sin(th)], [np.sin(th), np.cos(th)]])\n",
" plan = c + np.array([[-a, -b], [a, -b], [a, b], [-a, b]]) @ R.T\n",
" s0 = int(rng.integers(80, 160))\n",
" span = {\"sharp_turn\": 30, \"hover\": 60, \"dive\": 8, \"deviation\": 70, \"nfz_entry\": 120, \"low_level\": 120,\n",
" \"rc_silent\": 120, \"infiltration\": n - 20, \"loiter\": 90, \"speed_burst\": 25}.get(kind, 0)\n",
" if kind == \"infiltration\":\n",
" s0 = 20\n",
" win = (s0, s0 + span)\n",
" turns = set(rng.choice(np.arange(s0, s0 + 30), 3, replace=False).tolist()) if kind == \"sharp_turn\" else set()\n",
" # ๋์๋ฅผ ๋ฏธ๋ฆฌ ํ ๋ฒ์ ๋ฝ์ ๋๊ณ (์๋ ํฅ์) 1์ด ๋จ์๋ก ๊ธฐ์ฒด ์ด๋์ ์ ๋ถํ๋ค.\n",
" e_h, e_v, e_a = rng.normal(0, 0.8 * wind, n), rng.normal(0, 0.3 * wind, n), rng.normal(0, 0.4 * wind, n)\n",
" rc_arr = np.clip(rng.normal(rng.uniform(60, 92), 6, n), 0, 100)\n",
" gust = (rng.random(n) < (0.004 if weather == \"windy\" else 0.0)) * rng.uniform(20, 35, n)\n",
" burst, turn_amt = rng.uniform(23, 28), rng.choice([-1, 1], n) * rng.uniform(100, 160, n)\n",
" side = float(rng.choice([-90, 90]))\n",
" low_alt, rng_rc = rng.uniform(35, 55), rng.uniform(0, 20, n) # ์ ๊ณ ๋ ๋ชฉํ, ์กฐ์ข
์ ํธ ๋์ ์ ์์ฌ ์์ ๊ฐ\n",
" x, y, wi = float(plan[0][0]), float(plan[0][1]), 1\n",
" hdg, alt = _bearing((x, y), plan[1]), alt0\n",
" max_rate = 40.0 if kind == \"survey\" else 12.0\n",
" dev_target, X, Y, A, V, HD = None, np.zeros(n), np.zeros(n), np.zeros(n), np.zeros(n), np.zeros(n)\n",
" for t in range(n):\n",
" inw = win[0] <= t < win[1]\n",
" tx, ty = plan[1] if kind == \"infiltration\" else plan[wi % len(plan)]\n",
" spd_cmd, alt_cmd = v0, alt0\n",
" if inw:\n",
" if kind == \"deviation\":\n",
" if dev_target is None:\n",
" r = math.radians(hdg + side)\n",
" dev_target = (x + 900 * math.sin(r), y + 900 * math.cos(r))\n",
" tx, ty = dev_target\n",
" elif kind == \"nfz_entry\":\n",
" tx, ty = NFZ[\"x\"], NFZ[\"y\"]\n",
" elif kind == \"hover\":\n",
" spd_cmd = 0.0\n",
" elif kind == \"speed_burst\":\n",
" spd_cmd = burst\n",
" elif kind == \"low_level\":\n",
" alt_cmd = low_alt\n",
" if kind in (\"rc_silent\", \"infiltration\"):\n",
" rc_arr[t] = rng_rc[t]\n",
" if inw and kind == \"loiter\":\n",
" hdg = (hdg + 9.0 + e_h[t] / wind) % 360\n",
" else:\n",
" d = _wrap(math.degrees(math.atan2(tx - x, ty - y)) - hdg)\n",
" hdg = (hdg + max(-max_rate, min(max_rate, d)) + e_h[t]) % 360\n",
" if t in turns:\n",
" hdg = (hdg + turn_amt[t]) % 360\n",
" v = abs(spd_cmd + e_v[t]) if spd_cmd > 0 else abs(e_v[t] / wind)\n",
" x += v * math.sin(math.radians(hdg)); y += v * math.cos(math.radians(hdg))\n",
" if kind != \"infiltration\" and math.hypot(x - tx, y - ty) < 40 and not (inw and kind in (\"deviation\", \"nfz_entry\")):\n",
" wi += 1\n",
" if inw and kind == \"dive\":\n",
" alt -= 9.0 + e_v[t]\n",
" else:\n",
" alt += max(-3.0, min(3.0, alt_cmd - alt)) + e_a[t]\n",
" alt -= gust[t] # ๋ํ ํ๊ฐ๊ธฐ๋ฅ(์ ์ ๋นํ์์๋ ๋ฐ์)\n",
" if kind == \"dive\" and t >= win[1]:\n",
" alt0 = min(alt0, alt)\n",
" alt = max(alt, 3.0)\n",
" X[t], Y[t], A[t], V[t], HD[t] = x, y, alt, v, hdg\n",
" gps_sd = {\"clear\": 2.0, \"night\": 2.0, \"windy\": 3.0, \"rain\": 4.5}[weather] # ์
์ฒํ์ผ์๋ก GPS ์ก์(๋ค์ค๊ฒฝ๋ก) ์ฆ๊ฐ\n",
" df = pd.DataFrame({\"t\": np.arange(n), \"x\": X + rng.normal(0, gps_sd, n), \"y\": Y + rng.normal(0, gps_sd, n),\n",
" \"alt\": A + rng.normal(0, 0.5, n), \"speed\": V + rng.normal(0, 0.2, n),\n",
" \"heading\": (HD + rng.normal(0, 1.5, n)) % 360, \"rc\": rc_arr.round(1),\n",
" \"sats\": rng.integers(10, 15, n), \"hdop\": rng.uniform(0.6, 1.2, n).round(2),\n",
" \"x_true\": X, \"y_true\": Y})\n",
" df[\"anomaly\"] = (df[\"t\"] >= win[0]) & (df[\"t\"] < win[1]) & (kind in SCENARIOS)\n",
" op = OPERATORS[int(rng.choice(3, p=OPERATOR_P[kind]))]\n",
" meta = {\"id\": fid, \"kind\": kind, \"weather\": weather, \"operator\": op, \"plan\": plan, \"window\": win if kind in SCENARIOS else None,\n",
" \"platform\": [\"commercial\", \"military\", \"unknown\"][int(rng.choice(3, p=PLATFORM_P[op]))],\n",
" \"firmware\": [\"stock\", \"custom\", \"unknown\"][int(rng.choice(3, p=FIRMWARE_P[op]))]}\n",
" return df, meta\n",
"\n",
"\n",
"def corrupt(df, rng, spoof=True, emi=True, drop=True, dup=True):\n",
" \"\"\"์ ์ฅ ์ก์ ์ฃผ์
: GPS ์คํธํ(์์น ์ ํยทํ๋ฅ), ์ ์๊ธฐ ๊ฐ์ญ(๊ณ ๋ ์คํ์ดํฌ), ์ผ์ ๋จ์ (๊ฒฐ์ธก), ์ค๋ณต ๊ธฐ๋ก.\"\"\"\n",
" d = df.copy()\n",
" d[\"spoof_true\"] = False\n",
" if spoof:\n",
" a = int(rng.integers(40, len(d) - 60)); L = int(rng.integers(25, 45))\n",
" th = rng.uniform(0, 2 * np.pi)\n",
" drift = rng.uniform(22, 28) * np.arange(L)[:, None] * np.array([np.cos(th), np.sin(th)])\n",
" jump = rng.uniform(250, 400) * np.array([np.cos(th + 1), np.sin(th + 1)])\n",
" d.loc[a:a + L - 1, [\"x\", \"y\"]] += drift + jump\n",
" d.loc[a:a + L - 1, \"spoof_true\"] = True\n",
" if emi:\n",
" idx = rng.choice(len(d), 6, replace=False)\n",
" d.loc[idx, \"alt\"] += rng.choice([-1, 1], 6) * rng.uniform(60, 150, 6)\n",
" if drop:\n",
" a = int(rng.integers(10, len(d) - 20))\n",
" d.loc[a:a + int(rng.integers(4, 9)), [\"x\", \"y\", \"alt\", \"speed\", \"heading\"]] = np.nan\n",
" d = d.drop(index=rng.choice(len(d), 5, replace=False))\n",
" if dup:\n",
" d = pd.concat([d, d.sample(5, random_state=int(rng.integers(1e6)))]).sort_values(\"t\", kind=\"stable\")\n",
" return d.reset_index(drop=True)\n",
"\n",
"\n",
"def make_dataset(n=200, seed=7, anomaly_ratio=0.5, scenarios=SCENARIOS):\n",
" rng = np.random.default_rng(seed)\n",
" out = []\n",
" for i in range(n):\n",
" if i % 10 < anomaly_ratio * 10:\n",
" kind = scenarios[(i // 10 * int(anomaly_ratio * 10) + i % 10) % len(scenarios)]\n",
" else:\n",
" kind = \"survey\" if i % 10 == 9 else \"normal\"\n",
" df, meta = make_flight(f\"FL{i:04d}\", kind, rng)\n",
" out.append((df, meta))\n",
" return out\n",
"# ---------------------------------------------------------------------------\n",
"\n",
"# ---- ์ ์ฒ๋ฆฌยท๊ท์น ํ์ง ๊ณตํต ํจ์ ----\n",
"SPOOF_THR = {\"clear\": 12.0, \"night\": 12.0, \"windy\": 15.0, \"rain\": 19.0} # ๊ธฐ์๋ณ GPS-์ถ์ธกํญ๋ฒ ๋ถ์ผ์น ํ์ฉ์น(m)\n",
"\n",
"\n",
"def clean(df, weather=\"clear\"):\n",
" \"\"\"์ ์ฒ๋ฆฌ: ์ค๋ณต ์ ๊ฑฐยท์ ๋ ฌ -> 1์ด ์ฌํ๋ณธํ -> ๊ฒฐ์ธก ๋ณด๊ฐ -> ๊ณ ๋ ์คํ์ดํฌ ์ ๊ฑฐ -> GPS ์คํธํ ๊ตฌ๊ฐ ์ถ์ธกํญ๋ฒ ๋ณด์ .\"\"\"\n",
" d = df.drop_duplicates(\"t\").sort_values(\"t\").set_index(\"t\")\n",
" d = d.reindex(np.arange(int(d.index.min()), int(d.index.max()) + 1))\n",
" n_missing = int(d[\"x\"].isna().sum())\n",
" num = [\"x\", \"y\", \"alt\", \"speed\", \"rc\", \"sats\", \"hdop\"]\n",
" d[num] = d[num].interpolate(limit_direction=\"both\")\n",
" hd = d[\"heading\"].dropna() # ๋ฐฉ์๋ 0/360 ๊ฒฝ๊ณ๋ฅผ ํ์ด์ ๋ณด๊ฐ\n",
" un = pd.Series(np.degrees(np.unwrap(np.radians(hd.to_numpy(float)))), index=hd.index)\n",
" d[\"heading\"] = un.reindex(d.index).interpolate(limit_direction=\"both\").to_numpy() % 360\n",
" med = d[\"alt\"].rolling(5, center=True, min_periods=1).median()\n",
" spike = (d[\"alt\"] - med).abs() > 25\n",
" d.loc[spike, \"alt\"] = med[spike]\n",
" x, y = d[\"x\"].to_numpy(float), d[\"y\"].to_numpy(float)\n",
" r = np.radians(d[\"heading\"].to_numpy(float)); v = d[\"speed\"].to_numpy(float)\n",
" dr = np.c_[v * np.sin(r), v * np.cos(r)] # ๋ณด๊ณ ์๋ยท๋ฐฉ์๋ก ๊ณ์ฐํ 1์ด ์ด๋๋(์ถ์ธกํญ๋ฒ)\n",
" xc, yc, flag = x.copy(), y.copy(), np.zeros(len(x), bool)\n",
" thr = SPOOF_THR.get(weather, 12.0)\n",
" ok_run = 0\n",
" for i in range(1, len(x)):\n",
" px, py = xc[i - 1] + dr[i, 0], yc[i - 1] + dr[i, 1] # ์ถ์ธกํญ๋ฒ ์์ ์์น\n",
" step_ok = np.hypot(x[i] - x[i - 1] - dr[i, 0], y[i] - y[i - 1] - dr[i, 1]) <= thr\n",
" ok_run = ok_run + 1 if step_ok else 0\n",
" if flag[i - 1]:\n",
" # ์คํธํ ๊ตฌ๊ฐ: GPS๊ฐ ์์ ์์น๋ก ๋์์ค๊ฑฐ๋, GPS ์ด๋๋์ด 5์ด ์ฐ์ ๋ฌผ๋ฆฌ์ ์ผ๋ก ๋ง์ผ๋ฉด GPS๋ก ๋ณต๊ท\n",
" if np.hypot(x[i] - px, y[i] - py) < thr * 2 or ok_run >= 5:\n",
" xc[i], yc[i] = x[i], y[i]\n",
" else:\n",
" xc[i], yc[i], flag[i] = px, py, True\n",
" elif not step_ok:\n",
" xc[i], yc[i], flag[i] = px, py, True # GPS ์ด๋๋์ด ๋ฌผ๋ฆฌ์ ์ผ๋ก ๋ง์ง ์์ -> ์คํธํ ์์ฌ\n",
" else:\n",
" xc[i], yc[i] = xc[i - 1] + x[i] - x[i - 1], yc[i - 1] + y[i] - y[i - 1]\n",
" d[\"x\"], d[\"y\"], d[\"spoof_flag\"] = xc, yc, flag\n",
" d = d.reset_index()\n",
" d.attrs.update({\"missing\": n_missing, \"spikes\": int(spike.sum()), \"spoof_pts\": int(flag.sum())})\n",
" return d\n",
"\n",
"\n",
"def xtrack(px, py, plan):\n",
" \"\"\"๊ณํ ๊ฒฝ๋ก(์จ์ดํฌ์ธํธ ํ๊ณก์ ) ๋๋น ์ดํ ๊ฑฐ๋ฆฌ(m).\"\"\"\n",
" P = np.asarray(plan, float)\n",
" segs = [(P[i], P[(i + 1) % len(P)]) for i in range(len(P) if len(P) > 2 else 1)]\n",
" best = np.full(len(px), np.inf)\n",
" for a, b in segs:\n",
" ab = b - a\n",
" u = np.clip(((px - a[0]) * ab[0] + (py - a[1]) * ab[1]) / max(ab @ ab, 1e-9), 0, 1)\n",
" best = np.minimum(best, np.hypot(px - a[0] - u * ab[0], py - a[1] - u * ab[1]))\n",
" return best\n",
"\n",
"\n",
"RULES = {\"turn_rate\": 45.0, \"hover_speed\": 1.5, \"hover_sec\": 20, \"dive_m\": 30.0, \"xtrack_m\": 250.0}\n",
"\n",
"\n",
"def rule_flags(d, plan, R=RULES):\n",
" \"\"\"๊ท์น ๊ธฐ๋ฐ ์ด์ ๋นํ ํ์ (๊ธฐ๋ก ๋จ์). ๊ธ์ ํยทํธ๋ฒ๋งยท๊ธ๊ฐํยท๊ฒฝ๋ก ์ดํยท๋น์ธ๊ฐ ๊ตฌ์ญ ์ง์
.\"\"\"\n",
" hr = np.abs(_wrap(np.diff(d[\"heading\"].to_numpy(float), prepend=d[\"heading\"].iloc[0])))\n",
" slow = (d[\"speed\"] < R[\"hover_speed\"]).to_numpy()\n",
" run = np.zeros(len(slow), int)\n",
" for i in range(len(slow)):\n",
" run[i] = run[i - 1] + 1 if slow[i] and i else int(slow[i])\n",
" hover = np.zeros(len(slow), bool)\n",
" for i in np.where(run >= R[\"hover_sec\"])[0]:\n",
" hover[i - R[\"hover_sec\"] + 1:i + 1] = True\n",
" alt = d[\"alt\"].to_numpy(float)\n",
" drop = alt - np.r_[alt[5:], np.full(5, alt[-1])]\n",
" return {\"๊ธ์ ํ\": hr > R[\"turn_rate\"], \"ํธ๋ฒ๋ง\": hover, \"๊ธ๊ฐํ\": drop > R[\"dive_m\"],\n",
" \"๊ฒฝ๋ก์ดํ\": xtrack(d[\"x\"].to_numpy(float), d[\"y\"].to_numpy(float), plan) > R[\"xtrack_m\"],\n",
" \"๋น์ธ๊ฐ๊ตฌ์ญ\": np.hypot(d[\"x\"] - NFZ[\"x\"], d[\"y\"] - NFZ[\"y\"]).to_numpy() < NFZ[\"r\"]}\n",
"\n",
"\n",
"def run_baseline(data, R=RULES):\n",
" out = []\n",
" for df, m in data:\n",
" t0 = time.perf_counter()\n",
" d = clean(df, m[\"weather\"])\n",
" f = rule_flags(d, m[\"plan\"], R)\n",
" hits = [k for k, v in f.items() if v.any()]\n",
" out.append({\"id\": m[\"id\"], \"kind\": m[\"kind\"], \"truth\": m[\"kind\"] in SCENARIOS, \"pred\": bool(hits),\n",
" \"rules\": \",\".join(hits) or \"-\", \"ms\": (time.perf_counter() - t0) * 1000})\n",
" return pd.DataFrame(out)\n",
"\n",
"\n",
"# ํต์ ๋นํ ํจํด ์ ์(ํ๋จ ๊ธฐ์ค์ ์ซ์๋ก ๊ณต๊ฐํด ํฌ๋ช
์ฑ ํ๋ณด)\n",
"print(\"[ํต์ ๋นํ ๊ธฐ์ค] ์ ํ์จ 45๋/์ด ์ดํ, 1.5m/s ๋ฏธ๋ง ์ ์ง 20์ด ๋ฏธ๋ง, 5์ด๊ฐ ๊ณ ๋ ํ๊ฐ 30m ์ดํ, ๊ณํ ๊ฒฝ๋ก 250m ์ด๋ด, ๋น์ธ๊ฐ ๊ตฌ์ญ ๋ฐ\")\n",
"\n",
"# 1) ๊ธฐ๋ณธ ๋ฐ์ดํฐ์
: ์ ์(์์ฐฐยท์ธก๋) 50% + ๊ธฐ๋ณธ ์ด์ 5์ข
50%\n",
"basic = make_dataset(100, seed=11, scenarios=SCENARIOS[:5])\n",
"r = run_baseline(basic)\n",
"acc = (r[\"truth\"] == r[\"pred\"]).mean()\n",
"tp, fp = int((r.truth & r.pred).sum()), int((~r.truth & r.pred).sum())\n",
"fn, tn = int((r.truth & ~r.pred).sum()), int((~r.truth & ~r.pred).sum())\n",
"print(f\"\\n๊ธฐ๋ณธ ๋ฐ์ดํฐ์
100๊ฑด: ์ ํ๋ {acc:.1%} (๋ชฉํ 75% ์ด์: {'๋ฌ์ฑ' if acc >= 0.75 else '๋ฏธ๋ฌ'}), TP {tp} / FP {fp} / FN {fn} / TN {tn}\")\n",
"print(f\"๋นํ 1๊ฑด ํ๋จ ์ง์ฐ์๊ฐ ํ๊ท {r['ms'].mean():.1f} ms\")\n",
"\n",
"print(\"\\n[์ ํ๋ณ ํ์ง์จ๊ณผ ์ฃผ๋ก ๊ฑธ๋ฆฐ ๊ท์น]\")\n",
"g = r.groupby(\"kind\").agg(๊ฑด์=(\"id\", \"size\"), ์ด์ํ์ ๋ฅ =(\"pred\", \"mean\"), ๋ํ๊ท์น=(\"rules\", lambda s: s.value_counts().index[0]))\n",
"print(g.round(2).to_string())\n",
"\n",
"# 2) ๊ธ์ ํ ์๊ณ๊ฐ ๋ฏผ๊ฐ๋(ํด๋ฆฌ์คํฑ ๊ฐ์ค์น 1์ฐจ ์กฐ์ )\n",
"print(\"\\n์ ํ์จ ์๊ณ๊ฐ ์ ํ๋ ์ ์ ๋นํ ์คํ\")\n",
"for thr in [30, 45, 60, 90]:\n",
" rr = run_baseline(basic, {**RULES, \"turn_rate\": thr})\n",
" print(f\"{thr:>8}๋/์ด {(rr.truth == rr.pred).mean():.1%} {int((~rr.truth & rr.pred).sum())}๊ฑด\")\n",
"\n",
"# 3) ํ๊ณ: ์๋ฐยท๋ณตํฉ ์ด์ 5์ข
์๋ ๊ท์น์ด ๊ฑฐ์ ๋ฐ์ํ์ง ์๋๋ค(๋ฏธ์
4์ ๋๊ธฐ)\n",
"hard = make_dataset(60, seed=12, scenarios=SCENARIOS[5:])\n",
"h = run_baseline(hard)\n",
"print(\"\\n[๋ณตํฉ ์ด์ 5์ข
ํ์ง์จ]\", h[h.truth].groupby(\"kind\")[\"pred\"].mean().round(2).to_dict())\n",
"\n",
"# 4) ๊ธฐ๋ณธ ์๋๋ฆฌ์ค ์คํ ๋ก๊ทธ ์์์ ๊ทธ๋ฆผ\n",
"ex = next((df, m) for df, m in basic if m[\"kind\"] == \"sharp_turn\")\n",
"d = clean(ex[0], ex[1][\"weather\"]); f = rule_flags(d, ex[1][\"plan\"])\n",
"print(f\"\\n์คํ ๋ก๊ทธ ์์ {ex[1]['id']}: ๊ธ์ ํ ๊ธฐ๋ก {int(f['๊ธ์ ํ'].sum())}๊ฑด (t={list(d['t'][f['๊ธ์ ํ']][:3])}) -> '์ด์ ๋นํ ์์ฌ, ๋ถ์๊ด ํ์ธ ํ์'\")\n",
"fig, ax = plt.subplots(1, 2, figsize=(12, 5))\n",
"for a, (df, m) in zip(ax, [ex, next((df, m) for df, m in basic if m[\"kind\"] == \"deviation\")]):\n",
" d = clean(df, m[\"weather\"]); fl = np.any(list(rule_flags(d, m[\"plan\"]).values()), axis=0)\n",
" P = np.vstack([m[\"plan\"], m[\"plan\"][:1]])\n",
" a.plot(P[:, 0], P[:, 1], \"k--\", lw=1, label=\"planned route\")\n",
" a.plot(d[\"x\"], d[\"y\"], \"-\", color=\"tab:blue\", lw=1, label=\"flight log\")\n",
" a.plot(d[\"x\"][fl], d[\"y\"][fl], \"o\", color=\"tab:red\", ms=4, label=\"rule hit\")\n",
" a.set_title(f\"{m['id']} ({m['kind']})\"); a.axis(\"equal\"); a.legend(fontsize=8); a.set_xlabel(\"East (m)\"); a.set_ylabel(\"North (m)\")\n",
"plt.suptitle(\"Rule-based baseline detections (synthetic logs)\"); plt.tight_layout(); plt.show()\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"๐ก [์ฐธ๊ณ ] ๋ฏธ์
03 ์์ ์คํ ๊ฒฐ๊ณผ (Output)
\n",
"\n",
"```text\n",
"[ํต์ ๋นํ ๊ธฐ์ค] ์ ํ์จ 45๋/์ด ์ดํ, 1.5m/s ๋ฏธ๋ง ์ ์ง 20์ด ๋ฏธ๋ง, 5์ด๊ฐ ๊ณ ๋ ํ๊ฐ 30m ์ดํ, ๊ณํ ๊ฒฝ๋ก 250m ์ด๋ด, ๋น์ธ๊ฐ ๊ตฌ์ญ ๋ฐ\n",
"\n",
"๊ธฐ๋ณธ ๋ฐ์ดํฐ์
100๊ฑด: ์ ํ๋ 91.0% (๋ชฉํ 75% ์ด์: ๋ฌ์ฑ), TP 49 / FP 8 / FN 1 / TN 42\n",
"๋นํ 1๊ฑด ํ๋จ ์ง์ฐ์๊ฐ ํ๊ท 10.0 ms\n",
"\n",
"[์ ํ๋ณ ํ์ง์จ๊ณผ ์ฃผ๋ก ๊ฑธ๋ฆฐ ๊ท์น]\n",
" ๊ฑด์ ์ด์ํ์ ๋ฅ ๋ํ๊ท์น\n",
"kind \n",
"deviation 10 1.00 ๊ฒฝ๋ก์ดํ\n",
"dive 10 1.00 ๊ธ๊ฐํ\n",
"hover 10 1.00 ํธ๋ฒ๋ง\n",
"nfz_entry 10 0.90 ๊ฒฝ๋ก์ดํ,๋น์ธ๊ฐ๊ตฌ์ญ\n",
"normal 40 0.12 -\n",
"sharp_turn 10 1.00 ๊ธ์ ํ\n",
"survey 10 0.30 -\n",
"\n",
"์ ํ์จ ์๊ณ๊ฐ ์ ํ๋ ์ ์ ๋นํ ์คํ\n",
" 30๋/์ด 84.0% 15๊ฑด\n",
" 45๋/์ด 91.0% 8๊ฑด\n",
" 60๋/์ด 93.0% 6๊ฑด\n",
" 90๋/์ด 93.0% 6๊ฑด\n",
"\n",
"[๋ณตํฉ ์ด์ 5์ข
ํ์ง์จ] {'infiltration': 0.0, 'loiter': 0.17, 'low_level': 0.17, 'rc_silent': 0.17, 'speed_burst': 0.0}\n",
"\n",
"์คํ ๋ก๊ทธ ์์ FL0000: ๊ธ์ ํ ๊ธฐ๋ก 3๊ฑด (t=[158, 166, 168]) -> '์ด์ ๋นํ ์์ฌ, ๋ถ์๊ด ํ์ธ ํ์'\n",
"```\n",
" \n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## ๐ฏ ๋ฏธ์
04. ๋นํ ํจํด ํด๋ฌ์คํฐ๋ง ๋ฐ ๋จธ์ ๋ฌ๋ ๊ธฐ๋ฐ ์๋ฐ ์นจํฌ ๋๋ก ์ด์ ๊ถค์ ํ์ง ๋ชจ๋ธ ๊ณ ๋ํ\n",
"**์์์๊ฐ**: ์ฝ 3.0์๊ฐ\n",
"\n",
"### ๐ ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ์ํ ์ง์นจ\n",
"#### ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ๋น์์ฑ\n",
"\n",
"๋นํ ํจํด ํด๋ฌ์คํฐ๋ง ๋ฐ ๋น์ง๋ ๋จธ์ ๋ฌ๋์ ์ ์ฉํ์ฌ ์๋ฐ ์นจํฌ ๋๋ก ์ ํน์ด ๋นํ ๋ก๊ทธ ๋ฐ ๋น์ธ๊ฐ ๋นํ ๊ตฌ์ญ ์ง์
ํจํด์ ์ ๋ฐ ํ์งํ๋ค. ์ค์ ์ ์ ํ์ฅ์์๋ ๋จ์ ํ์ง์ ๊ทธ์น์ง ์๊ณ ์ ์ํ ์ํ ํ๋จ๊ณผ ์ ๋ขฐ๋ ๋์ ์์ฌ๊ฒฐ์ ์ด ํ์์ ์ด๋ฏ๋ก, **ํ์ ๋นํ ํจํด ๊ตฐ์งํ์ ๋น์ง๋ ์ด์ ํ์ง๋ฅผ ์ ์ฉํด ๊ท์น์ผ๋ก ์ก๊ธฐ ์ด๋ ค์ด ์๋ฐ ์นจํฌยท์ ๊ณ ๋ยท์กฐ์ข
์ ํธ ๋ฌด์๋ตยท์ ํ ์ฒด๋ฅ ๋ฑ ๋ณตํฉ ์ด์ ๋นํ์ ํ์งํ๋๋ก ๊ณ ๋ํํ๋ค.** ๊ณผ์
์ ์์ํ์ฌ ์คํ์ ๋ฐฉ์งํ๊ณ ์์ ๋์๋ ฅ์ ๊ทน๋ํํด์ผ ํฉ๋๋ค.\n",
"\n",
"์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ณธ ๋ฏธ์
์์๋ **30์ด ์ฐฝ ๋จ์๋ก ๊ณ ๋ยท์๋ยท์ ํยท์ง์ง์ฑยท์กฐ์ข
์ ํธยท๋น์ธ๊ฐ ๊ตฌ์ญ ์ ๊ทผยท๊ฒฝ๋ก ์ดํ ํน์ง์ ๊ณ์ฐํ๊ณ , ๋ผ๋ฒจ ์๋ ํ์ ๋ก๊ทธ๋ก KMeans ํต์ ํจํด ๊ตฐ์ง๊ณผ IsolationForest๋ฅผ ํ์ต ์ํํ๊ณ ๊ฒ์ฆํ๋** ์ค์ต์ ์ฒด๊ณ์ ์ผ๋ก ์ํํฉ๋๋ค.\n",
"\n",
"ํ์ต์๋ ์ค์ต๊ฐ์ด๋์ ์ ์๋ ์ด 4๊ฐ ์ธ๋ถ ํ๋(๊ธฐ๋ณธ ๊ธฐ๋ฅ ๊ตฌํ โ ๊ฒฐ๊ณผ ์งํ ๊ฒ์ฆ โ ํ๋ผ๋ฏธํฐ ํ๋ ๋ฐ ๋์ ๊ณผ์ )์ ์์ฐจ์ ์ผ๋ก ์งํํ๋ฉฐ ํต์ฌ ํ์ ๊ธฐ์ค์ ๋ฌ์ฑํฉ๋๋ค.\n",
"\n",
"**์ด๋ฒ ๊ฒฐ๊ณผ๋ฌผ:** `src/models/isr03_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": [
"\"\"\"ํ์ ๋นํ๋ก๊ทธ๋ก ํต์ ๋นํ ํจํด์ ๊ตฐ์งํ(KMeans)ํ๊ณ ๋น์ง๋ ์ด์ ํ์ง(IsolationForest)๋ฅผ ๊ฒฐํฉํด, ๊ท์น์ผ๋ก ์ก๊ธฐ ์ด๋ ค์ด ์ ๊ณ ๋ยท์กฐ์ข
์ ํธ ๋ฌด์๋ตยท์ ํ ์ฒด๋ฅยท์๋ฐ ์นจํฌ ๋ฑ 10์ข
๋ณตํฉ ์ด์ ๋นํ์ ํ์งํ๊ณ ์ฌํ์จยท์ค๊ฒฝ๋ณดยท์ฒ๋ฆฌ ์ง์ฐ์ ์ธก์ ํ๋ค.\"\"\"\n",
"import math\n",
"import time\n",
"import numpy as np\n",
"import pandas as pd\n",
"import matplotlib.pyplot as plt\n",
"from sklearn.cluster import KMeans\n",
"from sklearn.ensemble import IsolationForest\n",
"from sklearn.preprocessing import StandardScaler\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# [๊ต์ก์ฉ๊ฐ์์๋ฃ] ํฉ์ฑ ๋๋ก ๋นํ๋ก๊ทธ ์์ฑ๊ธฐ (์ค์ ์ขํยท๊ธฐ์ฒดยท๋ถ๋ ์ ๋ณด ์์)\n",
"# ๊ฐ์ ๊ตฌ์ญ(์์ ๊ธฐ์ค ๋ยท๋ถ ๋ฐฉํฅ m ์ขํ)์์ 1์ด ๊ฐ๊ฒฉ ๋นํ๋ก๊ทธ์ ์ ๋ต(์ด์ ๊ตฌ๊ฐยท์ด์ฉ ์ฃผ์ฒด)์ ๋ง๋ ๋ค.\n",
"# ---------------------------------------------------------------------------\n",
"NFZ = {\"x\": 2500.0, \"y\": 2500.0, \"r\": 700.0} # ๊ฐ์ ๋น์ธ๊ฐ ๋นํ๊ตฌ์ญ\n",
"WATCH = {\"x\": -2000.0, \"y\": 1500.0, \"r\": 600.0} # ๊ฐ์ ๊ด์ฌ ๊ตฌ์ญ\n",
"SCENARIOS = [\"sharp_turn\", \"hover\", \"dive\", \"deviation\", \"nfz_entry\",\n",
" \"low_level\", \"rc_silent\", \"infiltration\", \"loiter\", \"speed_burst\"]\n",
"OPERATORS = [\"friendly\", \"civil\", \"hostile\"]\n",
"# ์๋๋ฆฌ์ค๋ณ ์ด์ฉ ์ฃผ์ฒด ๋ถํฌ: ์ผ๋ถ๋ฌ ํฌ๊ฒ ๊ฒน์น๊ฒ ์ค๊ณํ๋ค(๋นํ ํน์ฑ๋ง์ผ๋ก ์ด์ฉ ์ฃผ์ฒด๋ฅผ ๋จ์ ํ ์ ์๋ค).\n",
"OPERATOR_P = {\"normal\": (.45, .47, .08), \"survey\": (.15, .82, .03), \"sharp_turn\": (.40, .35, .25),\n",
" \"hover\": (.25, .45, .30), \"dive\": (.25, .25, .50), \"deviation\": (.30, .40, .30),\n",
" \"nfz_entry\": (.10, .50, .40), \"low_level\": (.35, .15, .50), \"rc_silent\": (.30, .15, .55),\n",
" \"infiltration\": (.12, .08, .80), \"loiter\": (.30, .20, .50), \"speed_burst\": (.30, .20, .50)}\n",
"PLATFORM_P = {\"friendly\": (.40, .55, .05), \"civil\": (.88, .02, .10), \"hostile\": (.55, .30, .15)} # commercial/military/unknown\n",
"FIRMWARE_P = {\"friendly\": (.30, .65, .05), \"civil\": (.85, .10, .05), \"hostile\": (.20, .50, .30)} # stock/custom/unknown\n",
"\n",
"\n",
"def _wrap(a):\n",
" return (a + 180.0) % 360.0 - 180.0\n",
"\n",
"\n",
"def _bearing(p, q):\n",
" return math.degrees(math.atan2(q[0] - p[0], q[1] - p[1])) % 360.0\n",
"\n",
"\n",
"def _away_from_nfz(rng, min_d):\n",
" while True:\n",
" c = rng.uniform(-3800, 3800, 2)\n",
" if np.hypot(c[0] - NFZ[\"x\"], c[1] - NFZ[\"y\"]) > min_d:\n",
" return c\n",
"\n",
"\n",
"def make_flight(fid, kind, rng, n=300, weather=None):\n",
" \"\"\"๋นํ 1ํ = (๋ก๊ทธ DataFrame, ๋ฉํ dict). kind: normal / survey / SCENARIOS ์ค ํ๋\"\"\"\n",
" weather = weather or str(rng.choice([\"clear\", \"night\", \"windy\", \"rain\"], p=[.4, .25, .2, .15]))\n",
" wind = {\"clear\": 1.0, \"night\": 1.0, \"windy\": 2.5, \"rain\": 1.8}[weather]\n",
" v0, alt0 = rng.uniform(11, 17), rng.uniform(70, 150)\n",
" nfz_c = np.array([NFZ[\"x\"], NFZ[\"y\"]])\n",
" if kind == \"infiltration\": # ์๊ฑฐ๋ฆฌ์์ ๋น์ธ๊ฐ ๊ตฌ์ญ ๋ฐฉํฅ์ผ๋ก ์ ๊ณ ๋ ์ง์ง\n",
" ang = rng.uniform(0, 2 * np.pi)\n",
" start = nfz_c + 4000 * np.array([np.cos(ang), np.sin(ang)])\n",
" plan = np.array([start, nfz_c])\n",
" v0, alt0 = rng.uniform(9, 11), rng.uniform(25, 35)\n",
" else:\n",
" if kind == \"nfz_entry\": # ๋น์ธ๊ฐ ๊ตฌ์ญ 1.6km ๋ฐ์์ ์์ฐฐํ๋ค๊ฐ ์ง์
\n",
" ang = rng.uniform(0, 2 * np.pi)\n",
" c = nfz_c + 1600 * np.array([np.cos(ang), np.sin(ang)])\n",
" else:\n",
" c = _away_from_nfz(rng, 2600)\n",
" if kind == \"survey\": # ๋ฏผ๊ฐ ์ธก๋ํ ์๋ณต(์ง๊ทธ์ฌ๊ทธ) ๊ฒฝ๋ก\n",
" w, h = rng.uniform(500, 700), 120.0\n",
" plan = np.array([c + [(-w if i % 4 in (0, 3) else w), i // 2 * h - 360] for i in range(8)])\n",
" else: # ์ฌ๊ฐ ์์ฐฐ ๊ฒฝ๋ก(์จ์ดํฌ์ธํธ 4๊ฐ)\n",
" a, b, th = rng.uniform(300, 600), rng.uniform(200, 400), rng.uniform(0, np.pi)\n",
" R = np.array([[np.cos(th), -np.sin(th)], [np.sin(th), np.cos(th)]])\n",
" plan = c + np.array([[-a, -b], [a, -b], [a, b], [-a, b]]) @ R.T\n",
" s0 = int(rng.integers(80, 160))\n",
" span = {\"sharp_turn\": 30, \"hover\": 60, \"dive\": 8, \"deviation\": 70, \"nfz_entry\": 120, \"low_level\": 120,\n",
" \"rc_silent\": 120, \"infiltration\": n - 20, \"loiter\": 90, \"speed_burst\": 25}.get(kind, 0)\n",
" if kind == \"infiltration\":\n",
" s0 = 20\n",
" win = (s0, s0 + span)\n",
" turns = set(rng.choice(np.arange(s0, s0 + 30), 3, replace=False).tolist()) if kind == \"sharp_turn\" else set()\n",
" # ๋์๋ฅผ ๋ฏธ๋ฆฌ ํ ๋ฒ์ ๋ฝ์ ๋๊ณ (์๋ ํฅ์) 1์ด ๋จ์๋ก ๊ธฐ์ฒด ์ด๋์ ์ ๋ถํ๋ค.\n",
" e_h, e_v, e_a = rng.normal(0, 0.8 * wind, n), rng.normal(0, 0.3 * wind, n), rng.normal(0, 0.4 * wind, n)\n",
" rc_arr = np.clip(rng.normal(rng.uniform(60, 92), 6, n), 0, 100)\n",
" gust = (rng.random(n) < (0.004 if weather == \"windy\" else 0.0)) * rng.uniform(20, 35, n)\n",
" burst, turn_amt = rng.uniform(23, 28), rng.choice([-1, 1], n) * rng.uniform(100, 160, n)\n",
" side = float(rng.choice([-90, 90]))\n",
" low_alt, rng_rc = rng.uniform(35, 55), rng.uniform(0, 20, n) # ์ ๊ณ ๋ ๋ชฉํ, ์กฐ์ข
์ ํธ ๋์ ์ ์์ฌ ์์ ๊ฐ\n",
" x, y, wi = float(plan[0][0]), float(plan[0][1]), 1\n",
" hdg, alt = _bearing((x, y), plan[1]), alt0\n",
" max_rate = 40.0 if kind == \"survey\" else 12.0\n",
" dev_target, X, Y, A, V, HD = None, np.zeros(n), np.zeros(n), np.zeros(n), np.zeros(n), np.zeros(n)\n",
" for t in range(n):\n",
" inw = win[0] <= t < win[1]\n",
" tx, ty = plan[1] if kind == \"infiltration\" else plan[wi % len(plan)]\n",
" spd_cmd, alt_cmd = v0, alt0\n",
" if inw:\n",
" if kind == \"deviation\":\n",
" if dev_target is None:\n",
" r = math.radians(hdg + side)\n",
" dev_target = (x + 900 * math.sin(r), y + 900 * math.cos(r))\n",
" tx, ty = dev_target\n",
" elif kind == \"nfz_entry\":\n",
" tx, ty = NFZ[\"x\"], NFZ[\"y\"]\n",
" elif kind == \"hover\":\n",
" spd_cmd = 0.0\n",
" elif kind == \"speed_burst\":\n",
" spd_cmd = burst\n",
" elif kind == \"low_level\":\n",
" alt_cmd = low_alt\n",
" if kind in (\"rc_silent\", \"infiltration\"):\n",
" rc_arr[t] = rng_rc[t]\n",
" if inw and kind == \"loiter\":\n",
" hdg = (hdg + 9.0 + e_h[t] / wind) % 360\n",
" else:\n",
" d = _wrap(math.degrees(math.atan2(tx - x, ty - y)) - hdg)\n",
" hdg = (hdg + max(-max_rate, min(max_rate, d)) + e_h[t]) % 360\n",
" if t in turns:\n",
" hdg = (hdg + turn_amt[t]) % 360\n",
" v = abs(spd_cmd + e_v[t]) if spd_cmd > 0 else abs(e_v[t] / wind)\n",
" x += v * math.sin(math.radians(hdg)); y += v * math.cos(math.radians(hdg))\n",
" if kind != \"infiltration\" and math.hypot(x - tx, y - ty) < 40 and not (inw and kind in (\"deviation\", \"nfz_entry\")):\n",
" wi += 1\n",
" if inw and kind == \"dive\":\n",
" alt -= 9.0 + e_v[t]\n",
" else:\n",
" alt += max(-3.0, min(3.0, alt_cmd - alt)) + e_a[t]\n",
" alt -= gust[t] # ๋ํ ํ๊ฐ๊ธฐ๋ฅ(์ ์ ๋นํ์์๋ ๋ฐ์)\n",
" if kind == \"dive\" and t >= win[1]:\n",
" alt0 = min(alt0, alt)\n",
" alt = max(alt, 3.0)\n",
" X[t], Y[t], A[t], V[t], HD[t] = x, y, alt, v, hdg\n",
" gps_sd = {\"clear\": 2.0, \"night\": 2.0, \"windy\": 3.0, \"rain\": 4.5}[weather] # ์
์ฒํ์ผ์๋ก GPS ์ก์(๋ค์ค๊ฒฝ๋ก) ์ฆ๊ฐ\n",
" df = pd.DataFrame({\"t\": np.arange(n), \"x\": X + rng.normal(0, gps_sd, n), \"y\": Y + rng.normal(0, gps_sd, n),\n",
" \"alt\": A + rng.normal(0, 0.5, n), \"speed\": V + rng.normal(0, 0.2, n),\n",
" \"heading\": (HD + rng.normal(0, 1.5, n)) % 360, \"rc\": rc_arr.round(1),\n",
" \"sats\": rng.integers(10, 15, n), \"hdop\": rng.uniform(0.6, 1.2, n).round(2),\n",
" \"x_true\": X, \"y_true\": Y})\n",
" df[\"anomaly\"] = (df[\"t\"] >= win[0]) & (df[\"t\"] < win[1]) & (kind in SCENARIOS)\n",
" op = OPERATORS[int(rng.choice(3, p=OPERATOR_P[kind]))]\n",
" meta = {\"id\": fid, \"kind\": kind, \"weather\": weather, \"operator\": op, \"plan\": plan, \"window\": win if kind in SCENARIOS else None,\n",
" \"platform\": [\"commercial\", \"military\", \"unknown\"][int(rng.choice(3, p=PLATFORM_P[op]))],\n",
" \"firmware\": [\"stock\", \"custom\", \"unknown\"][int(rng.choice(3, p=FIRMWARE_P[op]))]}\n",
" return df, meta\n",
"\n",
"\n",
"def corrupt(df, rng, spoof=True, emi=True, drop=True, dup=True):\n",
" \"\"\"์ ์ฅ ์ก์ ์ฃผ์
: GPS ์คํธํ(์์น ์ ํยทํ๋ฅ), ์ ์๊ธฐ ๊ฐ์ญ(๊ณ ๋ ์คํ์ดํฌ), ์ผ์ ๋จ์ (๊ฒฐ์ธก), ์ค๋ณต ๊ธฐ๋ก.\"\"\"\n",
" d = df.copy()\n",
" d[\"spoof_true\"] = False\n",
" if spoof:\n",
" a = int(rng.integers(40, len(d) - 60)); L = int(rng.integers(25, 45))\n",
" th = rng.uniform(0, 2 * np.pi)\n",
" drift = rng.uniform(22, 28) * np.arange(L)[:, None] * np.array([np.cos(th), np.sin(th)])\n",
" jump = rng.uniform(250, 400) * np.array([np.cos(th + 1), np.sin(th + 1)])\n",
" d.loc[a:a + L - 1, [\"x\", \"y\"]] += drift + jump\n",
" d.loc[a:a + L - 1, \"spoof_true\"] = True\n",
" if emi:\n",
" idx = rng.choice(len(d), 6, replace=False)\n",
" d.loc[idx, \"alt\"] += rng.choice([-1, 1], 6) * rng.uniform(60, 150, 6)\n",
" if drop:\n",
" a = int(rng.integers(10, len(d) - 20))\n",
" d.loc[a:a + int(rng.integers(4, 9)), [\"x\", \"y\", \"alt\", \"speed\", \"heading\"]] = np.nan\n",
" d = d.drop(index=rng.choice(len(d), 5, replace=False))\n",
" if dup:\n",
" d = pd.concat([d, d.sample(5, random_state=int(rng.integers(1e6)))]).sort_values(\"t\", kind=\"stable\")\n",
" return d.reset_index(drop=True)\n",
"\n",
"\n",
"def make_dataset(n=200, seed=7, anomaly_ratio=0.5, scenarios=SCENARIOS):\n",
" rng = np.random.default_rng(seed)\n",
" out = []\n",
" for i in range(n):\n",
" if i % 10 < anomaly_ratio * 10:\n",
" kind = scenarios[(i // 10 * int(anomaly_ratio * 10) + i % 10) % len(scenarios)]\n",
" else:\n",
" kind = \"survey\" if i % 10 == 9 else \"normal\"\n",
" df, meta = make_flight(f\"FL{i:04d}\", kind, rng)\n",
" out.append((df, meta))\n",
" return out\n",
"# ---------------------------------------------------------------------------\n",
"\n",
"# ---- ์ ์ฒ๋ฆฌยท๊ท์น ํ์ง ๊ณตํต ํจ์ ----\n",
"SPOOF_THR = {\"clear\": 12.0, \"night\": 12.0, \"windy\": 15.0, \"rain\": 19.0} # ๊ธฐ์๋ณ GPS-์ถ์ธกํญ๋ฒ ๋ถ์ผ์น ํ์ฉ์น(m)\n",
"\n",
"\n",
"def clean(df, weather=\"clear\"):\n",
" \"\"\"์ ์ฒ๋ฆฌ: ์ค๋ณต ์ ๊ฑฐยท์ ๋ ฌ -> 1์ด ์ฌํ๋ณธํ -> ๊ฒฐ์ธก ๋ณด๊ฐ -> ๊ณ ๋ ์คํ์ดํฌ ์ ๊ฑฐ -> GPS ์คํธํ ๊ตฌ๊ฐ ์ถ์ธกํญ๋ฒ ๋ณด์ .\"\"\"\n",
" d = df.drop_duplicates(\"t\").sort_values(\"t\").set_index(\"t\")\n",
" d = d.reindex(np.arange(int(d.index.min()), int(d.index.max()) + 1))\n",
" n_missing = int(d[\"x\"].isna().sum())\n",
" num = [\"x\", \"y\", \"alt\", \"speed\", \"rc\", \"sats\", \"hdop\"]\n",
" d[num] = d[num].interpolate(limit_direction=\"both\")\n",
" hd = d[\"heading\"].dropna() # ๋ฐฉ์๋ 0/360 ๊ฒฝ๊ณ๋ฅผ ํ์ด์ ๋ณด๊ฐ\n",
" un = pd.Series(np.degrees(np.unwrap(np.radians(hd.to_numpy(float)))), index=hd.index)\n",
" d[\"heading\"] = un.reindex(d.index).interpolate(limit_direction=\"both\").to_numpy() % 360\n",
" med = d[\"alt\"].rolling(5, center=True, min_periods=1).median()\n",
" spike = (d[\"alt\"] - med).abs() > 25\n",
" d.loc[spike, \"alt\"] = med[spike]\n",
" x, y = d[\"x\"].to_numpy(float), d[\"y\"].to_numpy(float)\n",
" r = np.radians(d[\"heading\"].to_numpy(float)); v = d[\"speed\"].to_numpy(float)\n",
" dr = np.c_[v * np.sin(r), v * np.cos(r)] # ๋ณด๊ณ ์๋ยท๋ฐฉ์๋ก ๊ณ์ฐํ 1์ด ์ด๋๋(์ถ์ธกํญ๋ฒ)\n",
" xc, yc, flag = x.copy(), y.copy(), np.zeros(len(x), bool)\n",
" thr = SPOOF_THR.get(weather, 12.0)\n",
" ok_run = 0\n",
" for i in range(1, len(x)):\n",
" px, py = xc[i - 1] + dr[i, 0], yc[i - 1] + dr[i, 1] # ์ถ์ธกํญ๋ฒ ์์ ์์น\n",
" step_ok = np.hypot(x[i] - x[i - 1] - dr[i, 0], y[i] - y[i - 1] - dr[i, 1]) <= thr\n",
" ok_run = ok_run + 1 if step_ok else 0\n",
" if flag[i - 1]:\n",
" # ์คํธํ ๊ตฌ๊ฐ: GPS๊ฐ ์์ ์์น๋ก ๋์์ค๊ฑฐ๋, GPS ์ด๋๋์ด 5์ด ์ฐ์ ๋ฌผ๋ฆฌ์ ์ผ๋ก ๋ง์ผ๋ฉด GPS๋ก ๋ณต๊ท\n",
" if np.hypot(x[i] - px, y[i] - py) < thr * 2 or ok_run >= 5:\n",
" xc[i], yc[i] = x[i], y[i]\n",
" else:\n",
" xc[i], yc[i], flag[i] = px, py, True\n",
" elif not step_ok:\n",
" xc[i], yc[i], flag[i] = px, py, True # GPS ์ด๋๋์ด ๋ฌผ๋ฆฌ์ ์ผ๋ก ๋ง์ง ์์ -> ์คํธํ ์์ฌ\n",
" else:\n",
" xc[i], yc[i] = xc[i - 1] + x[i] - x[i - 1], yc[i - 1] + y[i] - y[i - 1]\n",
" d[\"x\"], d[\"y\"], d[\"spoof_flag\"] = xc, yc, flag\n",
" d = d.reset_index()\n",
" d.attrs.update({\"missing\": n_missing, \"spikes\": int(spike.sum()), \"spoof_pts\": int(flag.sum())})\n",
" return d\n",
"\n",
"\n",
"def xtrack(px, py, plan):\n",
" \"\"\"๊ณํ ๊ฒฝ๋ก(์จ์ดํฌ์ธํธ ํ๊ณก์ ) ๋๋น ์ดํ ๊ฑฐ๋ฆฌ(m).\"\"\"\n",
" P = np.asarray(plan, float)\n",
" segs = [(P[i], P[(i + 1) % len(P)]) for i in range(len(P) if len(P) > 2 else 1)]\n",
" best = np.full(len(px), np.inf)\n",
" for a, b in segs:\n",
" ab = b - a\n",
" u = np.clip(((px - a[0]) * ab[0] + (py - a[1]) * ab[1]) / max(ab @ ab, 1e-9), 0, 1)\n",
" best = np.minimum(best, np.hypot(px - a[0] - u * ab[0], py - a[1] - u * ab[1]))\n",
" return best\n",
"\n",
"\n",
"RULES = {\"turn_rate\": 45.0, \"hover_speed\": 1.5, \"hover_sec\": 20, \"dive_m\": 30.0, \"xtrack_m\": 250.0}\n",
"\n",
"\n",
"def rule_flags(d, plan, R=RULES):\n",
" \"\"\"๊ท์น ๊ธฐ๋ฐ ์ด์ ๋นํ ํ์ (๊ธฐ๋ก ๋จ์). ๊ธ์ ํยทํธ๋ฒ๋งยท๊ธ๊ฐํยท๊ฒฝ๋ก ์ดํยท๋น์ธ๊ฐ ๊ตฌ์ญ ์ง์
.\"\"\"\n",
" hr = np.abs(_wrap(np.diff(d[\"heading\"].to_numpy(float), prepend=d[\"heading\"].iloc[0])))\n",
" slow = (d[\"speed\"] < R[\"hover_speed\"]).to_numpy()\n",
" run = np.zeros(len(slow), int)\n",
" for i in range(len(slow)):\n",
" run[i] = run[i - 1] + 1 if slow[i] and i else int(slow[i])\n",
" hover = np.zeros(len(slow), bool)\n",
" for i in np.where(run >= R[\"hover_sec\"])[0]:\n",
" hover[i - R[\"hover_sec\"] + 1:i + 1] = True\n",
" alt = d[\"alt\"].to_numpy(float)\n",
" drop = alt - np.r_[alt[5:], np.full(5, alt[-1])]\n",
" return {\"๊ธ์ ํ\": hr > R[\"turn_rate\"], \"ํธ๋ฒ๋ง\": hover, \"๊ธ๊ฐํ\": drop > R[\"dive_m\"],\n",
" \"๊ฒฝ๋ก์ดํ\": xtrack(d[\"x\"].to_numpy(float), d[\"y\"].to_numpy(float), plan) > R[\"xtrack_m\"],\n",
" \"๋น์ธ๊ฐ๊ตฌ์ญ\": np.hypot(d[\"x\"] - NFZ[\"x\"], d[\"y\"] - NFZ[\"y\"]).to_numpy() < NFZ[\"r\"]}\n",
"\n",
"# ---- ๊ตฌ๊ฐ(30์ด ์ฐฝ) ํน์ง + ๋น์ง๋ ์ด์ ํ์ง ๊ณตํต ํจ์ ----\n",
"WFEAT = [\"alt_mean\", \"alt_min\", \"alt_std\", \"vz_min\", \"spd_mean\", \"spd_std\", \"spd_max\", \"turn_sum\", \"turn_max\",\n",
" \"straight\", \"rc_mean\", \"rc_zero\", \"nfz_min\", \"nfz_close\", \"xtrack_max\"]\n",
"\n",
"\n",
"def window_feats(d, plan, win=30, step=10):\n",
" \"\"\"30์ด ์ฐฝ์ 10์ด์ฉ ์ฎ๊ธฐ๋ฉฐ ๊ณ ๋ยท์๋ยท์ ํยท์ง์ง์ฑยท์กฐ์ข
์ ํธยท๋น์ธ๊ฐ ๊ตฌ์ญ ์ ๊ทผยท๊ฒฝ๋ก ์ดํ ํน์ง์ ๊ณ์ฐํ๋ค.\"\"\"\n",
" x, y, alt = d[\"x\"].to_numpy(float), d[\"y\"].to_numpy(float), d[\"alt\"].to_numpy(float)\n",
" spd, rc = d[\"speed\"].to_numpy(float), d[\"rc\"].to_numpy(float)\n",
" hr = np.abs(_wrap(np.diff(d[\"heading\"].to_numpy(float), prepend=d[\"heading\"].iloc[0])))\n",
" vz = np.diff(alt, prepend=alt[0])\n",
" nd = np.hypot(x - NFZ[\"x\"], y - NFZ[\"y\"])\n",
" xt = xtrack(x, y, plan)\n",
" step_len = np.hypot(np.diff(x, prepend=x[0]), np.diff(y, prepend=y[0]))\n",
" rows, starts = [], []\n",
" for s in range(0, len(d) - win + 1, step):\n",
" e = s + win\n",
" path = step_len[s + 1:e].sum()\n",
" rows.append([alt[s:e].mean(), alt[s:e].min(), alt[s:e].std(), vz[s:e].min(), spd[s:e].mean(), spd[s:e].std(),\n",
" spd[s:e].max(), hr[s:e].sum(), hr[s:e].max(), np.hypot(x[e - 1] - x[s], y[e - 1] - y[s]) / max(path, 1.0),\n",
" rc[s:e].mean(), (rc[s:e] < 5).mean(), nd[s:e].min(), (nd[s] - nd[e - 1]) / win, xt[s:e].max()])\n",
" starts.append(int(d[\"t\"].iloc[s]))\n",
" return np.array(rows), starts\n",
"\n",
"\n",
"class FlightAnomalyModel:\n",
" \"\"\"ํ์ ๋นํ๋ก๊ทธ(๋ผ๋ฒจ ์์)๋ก ํต์ ํจํด ๊ตฐ์ง(KMeans)๊ณผ ๊ณ ๋ฆฝ ์ฒ(IsolationForest)์ ํ์ตํ๊ณ ,\n",
" ๋ ์ ์๋ฅผ ํ์ต ๊ตฌ๊ฐ ๋ถ์์๋ก ๋๋ 1.0์ ๋์ผ๋ฉด 'ํต์ ํจํด์์ ๋ฒ์ด๋จ'์ผ๋ก ๋ณธ๋ค.\"\"\"\n",
"\n",
" def __init__(self, q=0.999, k=4):\n",
" self.q, self.k = q, k\n",
"\n",
" def fit(self, X):\n",
" self.sc = StandardScaler().fit(X)\n",
" Z = self.sc.transform(X)\n",
" self.iso = IsolationForest(n_estimators=150, random_state=0).fit(Z)\n",
" self.km = KMeans(self.k, n_init=5, random_state=0).fit(Z)\n",
" self.set_q(self.q, X)\n",
" return self\n",
"\n",
" def _raw(self, X):\n",
" Z = self.sc.transform(X)\n",
" return -self.iso.score_samples(Z), self.km.transform(Z).min(axis=1)\n",
"\n",
" def set_q(self, q, X):\n",
" a, b = self._raw(X)\n",
" self.q, self.t_if, self.t_km = q, np.quantile(a, q), np.quantile(b, q)\n",
"\n",
" def score(self, X):\n",
" a, b = self._raw(X)\n",
" return np.maximum(a / self.t_if, b / self.t_km)\n",
"\n",
"\n",
"AFEAT = [\"ml_score\", \"low_alt\", \"rc_weak\", \"nfz_min_km\", \"hover_s\", \"max_speed\", \"night\",\n",
" \"pf_commercial\", \"pf_military\", \"pf_unknown\", \"fw_stock\", \"fw_custom\", \"fw_unknown\"]\n",
"\n",
"\n",
"def attr_features(d, m, ml_score):\n",
" \"\"\"์ด์ฉ ์ฃผ์ฒด ์ถ์ ์ฉ ๋นํ ๋จ์ ํน์ง. ๊ธฐ์ฒดยทํ์จ์ด ์ ๋ณด๋ ์์ฉ ๊ธฐ์ฒด๊ฐ ๋ชจ๋ ์ฃผ์ฒด์ ์ฐ์ด๋ฏ๋ก ์ฝํ ๋จ์์ผ ๋ฟ์ด๋ค.\"\"\"\n",
" return [min(ml_score, 10.0), float((d[\"alt\"] < 60).mean()), float((d[\"rc\"] < 25).mean()),\n",
" float(np.hypot(d[\"x\"] - NFZ[\"x\"], d[\"y\"] - NFZ[\"y\"]).min() / 1000), float((d[\"speed\"] < 1.5).sum()),\n",
" float(d[\"speed\"].max()), float(m[\"weather\"] == \"night\"),\n",
" *[float(m[\"platform\"] == p) for p in (\"commercial\", \"military\", \"unknown\")],\n",
" *[float(m[\"firmware\"] == f) for f in (\"stock\", \"custom\", \"unknown\")]]\n",
"\n",
"\n",
"def prepare(data):\n",
" out = []\n",
" for df, m in data:\n",
" d = clean(df, m[\"weather\"])\n",
" X, st = window_feats(d, m[\"plan\"])\n",
" out.append({\"m\": m, \"d\": d, \"X\": X, \"starts\": st, \"rule\": any(v.any() for v in rule_flags(d, m[\"plan\"]).values())})\n",
" return out\n",
"\n",
"\n",
"# 1) ํ์ต: ๋ผ๋ฒจ ์๋ ํ์ ๋นํ๋ก๊ทธ(์์ฐฐยท์ธก๋) 120๊ฑด์ 30์ด ์ฐฝ\n",
"train = prepare(make_dataset(120, seed=21, anomaly_ratio=0.0))\n",
"Xtr = np.vstack([f[\"X\"] for f in train])\n",
"model = FlightAnomalyModel(q=0.999).fit(Xtr)\n",
"print(f\"ํ์ต ์ฐฝ {len(Xtr)}๊ฐ, ํต์ ํจํด ๊ตฐ์ง {model.k}๊ฐ ํฌ๊ธฐ:\", np.bincount(model.km.labels_).tolist())\n",
"\n",
"# 2) ์ํ: ์ ์ 100๊ฑด + ๋ณตํฉ ์ด์ 10์ข
x 10๊ฑด\n",
"test = prepare(make_dataset(200, seed=11))\n",
"t0 = time.perf_counter()\n",
"for f in test:\n",
" f[\"ml_score\"] = float(model.score(f[\"X\"]).max())\n",
"lat = (time.perf_counter() - t0) / sum(len(f[\"X\"]) for f in test) * 1000\n",
"for f in test:\n",
" f[\"ml\"] = f[\"ml_score\"] > 1.0\n",
" f[\"hybrid\"] = f[\"ml\"] or f[\"rule\"]\n",
"r = pd.DataFrame([{\"kind\": f[\"m\"][\"kind\"], \"truth\": f[\"m\"][\"kind\"] in SCENARIOS, \"๊ท์น\": f[\"rule\"], \"ML\": f[\"ml\"], \"๊ท์น+ML\": f[\"hybrid\"]} for f in test])\n",
"an, nm = r[r.truth], r[~r.truth]\n",
"print(\"\\n๋ฐฉ์ ์ด์ ์ฌํ์จ ์ ์ ๋นํ ์ค๊ฒฝ๋ณด์จ\")\n",
"for c in [\"๊ท์น\", \"ML\", \"๊ท์น+ML\"]:\n",
" print(f\"{c:7s} {an[c].mean():10.1%} {nm[c].mean():14.1%}\")\n",
"rec = an[\"๊ท์น+ML\"].mean()\n",
"print(f\"\\n๋ณตํฉ ์ํ ํ์ง ์ฌํ์จ {rec:.1%} (๋ชฉํ 85% ์ด์: {'๋ฌ์ฑ' if rec >= 0.85 else '๋ฏธ๋ฌ'}), 30์ด ์ฐฝ๋น ์ฒ๋ฆฌ {lat:.2f} ms (๋ชฉํ 200 ms ์ดํ)\")\n",
"\n",
"# 3) ๋ณตํฉ ์ํ ์๋๋ฆฌ์ค 10์ข
ํ์ง ์ํ(์๋๋ฆฌ์ค๋ณ ์ฌํ์จ 80% ์ด์์ด๋ฉด ํต๊ณผ)\n",
"per = an.groupby(\"kind\")[[\"๊ท์น\", \"ML\", \"๊ท์น+ML\"]].mean()\n",
"per[\"ํ์ \"] = np.where(per[\"๊ท์น+ML\"] >= 0.8, \"ํต๊ณผ\", \"๋ฏธํต๊ณผ\")\n",
"print(\"\\n[์๋๋ฆฌ์ค๋ณ ์ฌํ์จ]\")\n",
"print(per.round(2).to_string())\n",
"print(f\"ํต๊ณผ {int((per['ํ์ '] == 'ํต๊ณผ').sum())}/10์ข
\")\n",
"\n",
"# 4) ์ค๊ฒฝ๋ณด ์ ๋ฐ ํจํด: ์ ์์ธ๋ฐ ML์ด ์ด์์ผ๋ก ๋ณธ ๋นํ์ ๊ฐ์ฅ ํ๋ ํน์ง\n",
"fa = [f for f in test if f[\"m\"][\"kind\"] not in SCENARIOS and f[\"ml\"]]\n",
"print(f\"\\n[์ ์ ๋นํ ์ค๊ฒฝ๋ณด {len(fa)}๊ฑด์ ์์ธ ํน์ง]\")\n",
"for f in fa[:5]:\n",
" w = int(np.argmax(model.score(f[\"X\"])))\n",
" z = model.sc.transform(f[\"X\"][w:w + 1])[0]\n",
" top = np.argsort(-np.abs(z))[:2]\n",
" print(f\" {f['m']['id']} ({f['m']['kind']}, {f['m']['weather']}): \" + \", \".join(f\"{WFEAT[i]} z={z[i]:+.1f}\" for i in top))\n",
"print(\"-> ๊ฐํ ๋ํ ํ๊ฐยท์ธก๋ Uํด์ฒ๋ผ ์ ์์ด์ง๋ง ๋๋ฌธ ํจํด์ด ์์ธ์ด๋ฉด ํ์ต ์๋ฃ์ ์ถ๊ฐํ๊ฑฐ๋ ํน์ง์ ์ฌ์ค๊ณํ๋ค.\")\n",
"\n",
"# 5) ๊ทธ๋ฆผ: ์ด์ ์ ์ ๋ถํฌ์ ์๋ฐ ์นจํฌ ์์\n",
"fig, ax = plt.subplots(1, 2, figsize=(12, 4.5))\n",
"ax[0].hist([f[\"ml_score\"] for f in test if f[\"m\"][\"kind\"] not in SCENARIOS], bins=30, range=(0, 6), alpha=.7, label=\"normal\")\n",
"ax[0].hist([f[\"ml_score\"] for f in test if f[\"m\"][\"kind\"] in SCENARIOS], bins=30, range=(0, 6), alpha=.7, label=\"anomalous\")\n",
"ax[0].axvline(1.0, color=\"k\", ls=\"--\"); ax[0].set_xlabel(\"max window anomaly score\"); ax[0].set_ylabel(\"flights\"); ax[0].legend()\n",
"ax[0].set_title(\"Anomaly score by flight type\")\n",
"f = next(f for f in test if f[\"m\"][\"kind\"] == \"infiltration\")\n",
"s = model.score(f[\"X\"])\n",
"ax[1].plot(f[\"starts\"], s, \"-o\", ms=3, label=\"window score\"); ax[1].axhline(1.0, color=\"k\", ls=\"--\")\n",
"ax[1].plot(f[\"d\"][\"t\"], f[\"d\"][\"alt\"] / 30, color=\"tab:gray\", label=\"altitude / 30 m\")\n",
"ax[1].set_xlabel(\"time (s)\"); ax[1].set_title(f\"{f['m']['id']} infiltration: low altitude + RC silent\"); ax[1].legend()\n",
"plt.tight_layout(); plt.show()\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"๐ก [์ฐธ๊ณ ] ๋ฏธ์
04 ์์ ์คํ ๊ฒฐ๊ณผ (Output)
\n",
"\n",
"```text\n",
"ํ์ต ์ฐฝ 3360๊ฐ, ํต์ ํจํด ๊ตฐ์ง 4๊ฐ ํฌ๊ธฐ: [1104, 76, 1316, 864]\n",
"\n",
"๋ฐฉ์ ์ด์ ์ฌํ์จ ์ ์ ๋นํ ์ค๊ฒฝ๋ณด์จ\n",
"๊ท์น 57.0% 13.0%\n",
"ML 100.0% 8.0%\n",
"๊ท์น+ML 100.0% 16.0%\n",
"\n",
"๋ณตํฉ ์ํ ํ์ง ์ฌํ์จ 100.0% (๋ชฉํ 85% ์ด์: ๋ฌ์ฑ), 30์ด ์ฐฝ๋น ์ฒ๋ฆฌ 0.47 ms (๋ชฉํ 200 ms ์ดํ)\n",
"\n",
"[์๋๋ฆฌ์ค๋ณ ์ฌํ์จ]\n",
" ๊ท์น ML ๊ท์น+ML ํ์ \n",
"kind \n",
"deviation 1.0 1.0 1.0 ํต๊ณผ\n",
"dive 1.0 1.0 1.0 ํต๊ณผ\n",
"hover 1.0 1.0 1.0 ํต๊ณผ\n",
"infiltration 0.0 1.0 1.0 ํต๊ณผ\n",
"loiter 0.3 1.0 1.0 ํต๊ณผ\n",
"low_level 0.2 1.0 1.0 ํต๊ณผ\n",
"nfz_entry 1.0 1.0 1.0 ํต๊ณผ\n",
"rc_silent 0.1 1.0 1.0 ํต๊ณผ\n",
"sharp_turn 1.0 1.0 1.0 ํต๊ณผ\n",
"speed_burst 0.1 1.0 1.0 ํต๊ณผ\n",
"ํต๊ณผ 10/10์ข
\n",
"\n",
"[์ ์ ๋นํ ์ค๊ฒฝ๋ณด 8๊ฑด์ ์์ธ ํน์ง]\n",
" FL0019 (survey, windy): alt_std z=+6.7, vz_min z=-6.7\n",
" FL0039 (survey, windy): alt_std z=+8.9, vz_min z=-8.7\n",
" FL0059 (survey, clear): straight z=-5.5, turn_max z=+4.1\n",
" FL0088 (normal, windy): alt_std z=+7.6, alt_min z=-2.6\n",
" FL0089 (survey, rain): straight z=-5.6, turn_max z=+4.1\n",
"-> ๊ฐํ ๋ํ ํ๊ฐยท์ธก๋ Uํด์ฒ๋ผ ์ ์์ด์ง๋ง ๋๋ฌธ ํจํด์ด ์์ธ์ด๋ฉด ํ์ต ์๋ฃ์ ์ถ๊ฐํ๊ฑฐ๋ ํน์ง์ ์ฌ์ค๊ณํ๋ค.\n",
"```\n",
" \n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## ๐ฏ ๋ฏธ์
05. ์ด์ฉ ์ฃผ์ฒด(์ /์๊ตฐ/๋ฏผ๊ฐ) ์ฑ๊ธํ ๋จ์ ๋ฐฉ์ง๋ฅผ ์ํ ๋ถํ์ค์ฑ ์งํ ๋ฐ ์ ๋ขฐ๋ ์์ง ๊ฐ๋ฐ\n",
"**์์์๊ฐ**: ์ฝ 3.0์๊ฐ\n",
"\n",
"### ๐ ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ์ํ ์ง์นจ\n",
"#### ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ๋น์์ฑ\n",
"\n",
"๋นํ ํน์ฑ๋ง์ผ๋ก ํน์ ๊ตญ๊ฐ๋ ๋ถ๋๋ฅผ ์ฑ๊ธํ๊ฒ ๋จ์ ํ์ง ์๋๋ก ๋ถ์ ๋ถํ์ค์ฑ ์งํ์ ๋ค์ฐจ์ ์ ๋ขฐ๋ ์ ์๋ฅผ ์ฐ์ถํ๋ ์์ง์ ๊ฐ๋ฐํ๋ค. ์ค์ ์ ์ ํ์ฅ์์๋ ๋จ์ ํ์ง์ ๊ทธ์น์ง ์๊ณ ์ ์ํ ์ํ ํ๋จ๊ณผ ์ ๋ขฐ๋ ๋์ ์์ฌ๊ฒฐ์ ์ด ํ์์ ์ด๋ฏ๋ก, **๋นํ ํน์ฑ๋ง์ผ๋ก ์ด์ฉ ์ฃผ์ฒด(์๊ตฐยท๋ฏผ๊ฐยท์ ๋)๋ฅผ ๋จ์ ํ์ง ์๋๋ก ์ฃผ์ฒด๋ณ ํ๋ฅ , ๋ถํ์ค์ฑ, ๋ฐ์ดํฐ ํ์ง์ ํจ๊ป ๋ฐ์ํ ์ ๋ขฐ๋ ์์ง์ ๊ฐ๋ฐํ๋ค.** ๊ณผ์
์ ์์ํ์ฌ ์คํ์ ๋ฐฉ์งํ๊ณ ์์ ๋์๋ ฅ์ ๊ทน๋ํํด์ผ ํฉ๋๋ค.\n",
"\n",
"์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ณธ ๋ฏธ์
์์๋ **๊ณผ๊ฑฐ ๊ฒํ ๊ฐ ๋๋ ๊ฐ์ ์ฌ๋ก๋ก ์ฃผ์ฒด๋ณ ํ๋ฅ ๋ชจ๋ธ์ ํ์ตํ๊ณ , ์ ๊ทํ ์ํธ๋กํผ๋ก ๋ถํ์ค์ฑ์, ์คํธํยท๊ฒฐ์ธก ์ ๋๋ก ๋ก๊ทธ ํ์ง ์ฐ์ถํ๊ณ ๋ชจ๋ธ์ ์ ์ฉํ๋** ์ค์ต์ ์ฒด๊ณ์ ์ผ๋ก ์ํํฉ๋๋ค.\n",
"\n",
"ํ์ต์๋ ์ค์ต๊ฐ์ด๋์ ์ ์๋ ์ด 4๊ฐ ์ธ๋ถ ํ๋(๊ธฐ๋ณธ ๊ธฐ๋ฅ ๊ตฌํ โ ๊ฒฐ๊ณผ ์งํ ๊ฒ์ฆ โ ํ๋ผ๋ฏธํฐ ํ๋ ๋ฐ ๋์ ๊ณผ์ )์ ์์ฐจ์ ์ผ๋ก ์งํํ๋ฉฐ ํต์ฌ ํ์ ๊ธฐ์ค์ ๋ฌ์ฑํฉ๋๋ค.\n",
"\n",
"**์ด๋ฒ ๊ฒฐ๊ณผ๋ฌผ:** `src/verification/isr03_confidence_engine.py` ํํ์ ๊ฒฐ๊ณผ๋ฌผ์ ์์ฑํ๋ค.\n",
"\n",
"### ๐ ํ๊ฐ ๋ฐ ๊ฒ์ฆ ๊ธฐ์ค (PASS ์กฐ๊ฑด)\n",
"#### ๋ฏธ์
ํ์ \n",
"\n",
"**ํ๊ฐ ๊ตฌ๋ถ:** ํ์\n",
"\n",
"**ํต์ฌ ํ๊ฐ์์ญ:** ์ ๋ขฐ๋ ๋ฑ๊ธ(High/Med/Low) ๋ถ์ฌ ๊ธฐ์ค์ ๊ตฐ์ฌ์ ํ๋น์ฑ\n",
"\n",
"#### ๋ฏธ์
PASS ์กฐ๊ฑด\n",
"\n",
"- ์ ๋ ๊ธฐ์ค: ์ค๊ฒฝ๋ณด์จ(FAR) 40% ์ด์ ๊ฐ์ถ, ์ ๋ขฐ๋ ์ค์ฝ์ด ์ฐ์ถ ์ผ๊ด์ฑ 90% ์ด์\n",
"\n",
"\n",
"- ์งํ ๊ธฐ์ค: ์ค๊ฒฝ๋ณด ํํฐ๋ง ์ ํ ๋น๊ต ์งํ ์ฐ์ถ ๋ฐ ํตํฉ ํ
์คํธ ์๋ฃ\n",
"\n",
"\n",
"- ๊ฒ์ฆ ๋ฐฉ๋ฒ: ROC-AUC ๊ณก์ ๋ถ์ ๋ฐ ์๊ณ๊ฐ ๋ฏผ๊ฐ๋ ํ
์คํธ\n",
"\n",
"\n",
"#### ์ฑ์ ์ ํ์ ๋ฉ๋ชจ\n",
"\n",
"> ๋จ์ ์ฝ๋ ์คํ ์ฌ๋ถ๋ฟ๋ง ์๋๋ผ ์ถ๋ ฅ๋ ์์น์ ๊ทผ๊ฑฐ ์๋ฃ๊ฐ ๊ตฐ ์ค๋ฌด ์๊ตฌ์ฌํญ์ ๋ถํฉํ๋์ง ๋ฉด๋ฐํ ํ์ธํ๋ค.\n",
"\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"\"\"\"๋นํ ํน์ฑ๋ง์ผ๋ก ์ด์ฉ ์ฃผ์ฒด(์๊ตฐยท๋ฏผ๊ฐยท์ ๋)๋ฅผ ์ฑ๊ธํ๊ฒ ๋จ์ ํ์ง ์๋๋ก, ์ฃผ์ฒด๋ณ ํ๋ฅ ๊ณผ ๋ถํ์ค์ฑ(์ํธ๋กํผ)ยท๋ฐ์ดํฐ ํ์ง์ ํจ๊ป ๊ณ์ฐํด '์ ๋ ๊ฐ๋ฅ์ฑ ๋ฑ๊ธ(High/Medium/Low)'์ ๋งค๊ธฐ๋ ์ ๋ขฐ๋ ์์ง์ ๋ง๋ค๊ณ , ๋จ์ ํ ๊ท์น ๋๋น ์ค๊ฒฝ๋ณด ๊ฐ์ถ๊ณผ ์ ์ ์ผ๊ด์ฑ์ ๊ฒ์ฆํ๋ค.\"\"\"\n",
"import math\n",
"import numpy as np\n",
"import pandas as pd\n",
"import matplotlib.pyplot as plt\n",
"from sklearn.cluster import KMeans\n",
"from sklearn.ensemble import IsolationForest\n",
"from sklearn.linear_model import LogisticRegression\n",
"from sklearn.metrics import roc_auc_score\n",
"from sklearn.pipeline import make_pipeline\n",
"from sklearn.preprocessing import StandardScaler\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# [๊ต์ก์ฉ๊ฐ์์๋ฃ] ํฉ์ฑ ๋๋ก ๋นํ๋ก๊ทธ ์์ฑ๊ธฐ (์ค์ ์ขํยท๊ธฐ์ฒดยท๋ถ๋ ์ ๋ณด ์์)\n",
"# ๊ฐ์ ๊ตฌ์ญ(์์ ๊ธฐ์ค ๋ยท๋ถ ๋ฐฉํฅ m ์ขํ)์์ 1์ด ๊ฐ๊ฒฉ ๋นํ๋ก๊ทธ์ ์ ๋ต(์ด์ ๊ตฌ๊ฐยท์ด์ฉ ์ฃผ์ฒด)์ ๋ง๋ ๋ค.\n",
"# ---------------------------------------------------------------------------\n",
"NFZ = {\"x\": 2500.0, \"y\": 2500.0, \"r\": 700.0} # ๊ฐ์ ๋น์ธ๊ฐ ๋นํ๊ตฌ์ญ\n",
"WATCH = {\"x\": -2000.0, \"y\": 1500.0, \"r\": 600.0} # ๊ฐ์ ๊ด์ฌ ๊ตฌ์ญ\n",
"SCENARIOS = [\"sharp_turn\", \"hover\", \"dive\", \"deviation\", \"nfz_entry\",\n",
" \"low_level\", \"rc_silent\", \"infiltration\", \"loiter\", \"speed_burst\"]\n",
"OPERATORS = [\"friendly\", \"civil\", \"hostile\"]\n",
"# ์๋๋ฆฌ์ค๋ณ ์ด์ฉ ์ฃผ์ฒด ๋ถํฌ: ์ผ๋ถ๋ฌ ํฌ๊ฒ ๊ฒน์น๊ฒ ์ค๊ณํ๋ค(๋นํ ํน์ฑ๋ง์ผ๋ก ์ด์ฉ ์ฃผ์ฒด๋ฅผ ๋จ์ ํ ์ ์๋ค).\n",
"OPERATOR_P = {\"normal\": (.45, .47, .08), \"survey\": (.15, .82, .03), \"sharp_turn\": (.40, .35, .25),\n",
" \"hover\": (.25, .45, .30), \"dive\": (.25, .25, .50), \"deviation\": (.30, .40, .30),\n",
" \"nfz_entry\": (.10, .50, .40), \"low_level\": (.35, .15, .50), \"rc_silent\": (.30, .15, .55),\n",
" \"infiltration\": (.12, .08, .80), \"loiter\": (.30, .20, .50), \"speed_burst\": (.30, .20, .50)}\n",
"PLATFORM_P = {\"friendly\": (.40, .55, .05), \"civil\": (.88, .02, .10), \"hostile\": (.55, .30, .15)} # commercial/military/unknown\n",
"FIRMWARE_P = {\"friendly\": (.30, .65, .05), \"civil\": (.85, .10, .05), \"hostile\": (.20, .50, .30)} # stock/custom/unknown\n",
"\n",
"\n",
"def _wrap(a):\n",
" return (a + 180.0) % 360.0 - 180.0\n",
"\n",
"\n",
"def _bearing(p, q):\n",
" return math.degrees(math.atan2(q[0] - p[0], q[1] - p[1])) % 360.0\n",
"\n",
"\n",
"def _away_from_nfz(rng, min_d):\n",
" while True:\n",
" c = rng.uniform(-3800, 3800, 2)\n",
" if np.hypot(c[0] - NFZ[\"x\"], c[1] - NFZ[\"y\"]) > min_d:\n",
" return c\n",
"\n",
"\n",
"def make_flight(fid, kind, rng, n=300, weather=None):\n",
" \"\"\"๋นํ 1ํ = (๋ก๊ทธ DataFrame, ๋ฉํ dict). kind: normal / survey / SCENARIOS ์ค ํ๋\"\"\"\n",
" weather = weather or str(rng.choice([\"clear\", \"night\", \"windy\", \"rain\"], p=[.4, .25, .2, .15]))\n",
" wind = {\"clear\": 1.0, \"night\": 1.0, \"windy\": 2.5, \"rain\": 1.8}[weather]\n",
" v0, alt0 = rng.uniform(11, 17), rng.uniform(70, 150)\n",
" nfz_c = np.array([NFZ[\"x\"], NFZ[\"y\"]])\n",
" if kind == \"infiltration\": # ์๊ฑฐ๋ฆฌ์์ ๋น์ธ๊ฐ ๊ตฌ์ญ ๋ฐฉํฅ์ผ๋ก ์ ๊ณ ๋ ์ง์ง\n",
" ang = rng.uniform(0, 2 * np.pi)\n",
" start = nfz_c + 4000 * np.array([np.cos(ang), np.sin(ang)])\n",
" plan = np.array([start, nfz_c])\n",
" v0, alt0 = rng.uniform(9, 11), rng.uniform(25, 35)\n",
" else:\n",
" if kind == \"nfz_entry\": # ๋น์ธ๊ฐ ๊ตฌ์ญ 1.6km ๋ฐ์์ ์์ฐฐํ๋ค๊ฐ ์ง์
\n",
" ang = rng.uniform(0, 2 * np.pi)\n",
" c = nfz_c + 1600 * np.array([np.cos(ang), np.sin(ang)])\n",
" else:\n",
" c = _away_from_nfz(rng, 2600)\n",
" if kind == \"survey\": # ๋ฏผ๊ฐ ์ธก๋ํ ์๋ณต(์ง๊ทธ์ฌ๊ทธ) ๊ฒฝ๋ก\n",
" w, h = rng.uniform(500, 700), 120.0\n",
" plan = np.array([c + [(-w if i % 4 in (0, 3) else w), i // 2 * h - 360] for i in range(8)])\n",
" else: # ์ฌ๊ฐ ์์ฐฐ ๊ฒฝ๋ก(์จ์ดํฌ์ธํธ 4๊ฐ)\n",
" a, b, th = rng.uniform(300, 600), rng.uniform(200, 400), rng.uniform(0, np.pi)\n",
" R = np.array([[np.cos(th), -np.sin(th)], [np.sin(th), np.cos(th)]])\n",
" plan = c + np.array([[-a, -b], [a, -b], [a, b], [-a, b]]) @ R.T\n",
" s0 = int(rng.integers(80, 160))\n",
" span = {\"sharp_turn\": 30, \"hover\": 60, \"dive\": 8, \"deviation\": 70, \"nfz_entry\": 120, \"low_level\": 120,\n",
" \"rc_silent\": 120, \"infiltration\": n - 20, \"loiter\": 90, \"speed_burst\": 25}.get(kind, 0)\n",
" if kind == \"infiltration\":\n",
" s0 = 20\n",
" win = (s0, s0 + span)\n",
" turns = set(rng.choice(np.arange(s0, s0 + 30), 3, replace=False).tolist()) if kind == \"sharp_turn\" else set()\n",
" # ๋์๋ฅผ ๋ฏธ๋ฆฌ ํ ๋ฒ์ ๋ฝ์ ๋๊ณ (์๋ ํฅ์) 1์ด ๋จ์๋ก ๊ธฐ์ฒด ์ด๋์ ์ ๋ถํ๋ค.\n",
" e_h, e_v, e_a = rng.normal(0, 0.8 * wind, n), rng.normal(0, 0.3 * wind, n), rng.normal(0, 0.4 * wind, n)\n",
" rc_arr = np.clip(rng.normal(rng.uniform(60, 92), 6, n), 0, 100)\n",
" gust = (rng.random(n) < (0.004 if weather == \"windy\" else 0.0)) * rng.uniform(20, 35, n)\n",
" burst, turn_amt = rng.uniform(23, 28), rng.choice([-1, 1], n) * rng.uniform(100, 160, n)\n",
" side = float(rng.choice([-90, 90]))\n",
" low_alt, rng_rc = rng.uniform(35, 55), rng.uniform(0, 20, n) # ์ ๊ณ ๋ ๋ชฉํ, ์กฐ์ข
์ ํธ ๋์ ์ ์์ฌ ์์ ๊ฐ\n",
" x, y, wi = float(plan[0][0]), float(plan[0][1]), 1\n",
" hdg, alt = _bearing((x, y), plan[1]), alt0\n",
" max_rate = 40.0 if kind == \"survey\" else 12.0\n",
" dev_target, X, Y, A, V, HD = None, np.zeros(n), np.zeros(n), np.zeros(n), np.zeros(n), np.zeros(n)\n",
" for t in range(n):\n",
" inw = win[0] <= t < win[1]\n",
" tx, ty = plan[1] if kind == \"infiltration\" else plan[wi % len(plan)]\n",
" spd_cmd, alt_cmd = v0, alt0\n",
" if inw:\n",
" if kind == \"deviation\":\n",
" if dev_target is None:\n",
" r = math.radians(hdg + side)\n",
" dev_target = (x + 900 * math.sin(r), y + 900 * math.cos(r))\n",
" tx, ty = dev_target\n",
" elif kind == \"nfz_entry\":\n",
" tx, ty = NFZ[\"x\"], NFZ[\"y\"]\n",
" elif kind == \"hover\":\n",
" spd_cmd = 0.0\n",
" elif kind == \"speed_burst\":\n",
" spd_cmd = burst\n",
" elif kind == \"low_level\":\n",
" alt_cmd = low_alt\n",
" if kind in (\"rc_silent\", \"infiltration\"):\n",
" rc_arr[t] = rng_rc[t]\n",
" if inw and kind == \"loiter\":\n",
" hdg = (hdg + 9.0 + e_h[t] / wind) % 360\n",
" else:\n",
" d = _wrap(math.degrees(math.atan2(tx - x, ty - y)) - hdg)\n",
" hdg = (hdg + max(-max_rate, min(max_rate, d)) + e_h[t]) % 360\n",
" if t in turns:\n",
" hdg = (hdg + turn_amt[t]) % 360\n",
" v = abs(spd_cmd + e_v[t]) if spd_cmd > 0 else abs(e_v[t] / wind)\n",
" x += v * math.sin(math.radians(hdg)); y += v * math.cos(math.radians(hdg))\n",
" if kind != \"infiltration\" and math.hypot(x - tx, y - ty) < 40 and not (inw and kind in (\"deviation\", \"nfz_entry\")):\n",
" wi += 1\n",
" if inw and kind == \"dive\":\n",
" alt -= 9.0 + e_v[t]\n",
" else:\n",
" alt += max(-3.0, min(3.0, alt_cmd - alt)) + e_a[t]\n",
" alt -= gust[t] # ๋ํ ํ๊ฐ๊ธฐ๋ฅ(์ ์ ๋นํ์์๋ ๋ฐ์)\n",
" if kind == \"dive\" and t >= win[1]:\n",
" alt0 = min(alt0, alt)\n",
" alt = max(alt, 3.0)\n",
" X[t], Y[t], A[t], V[t], HD[t] = x, y, alt, v, hdg\n",
" gps_sd = {\"clear\": 2.0, \"night\": 2.0, \"windy\": 3.0, \"rain\": 4.5}[weather] # ์
์ฒํ์ผ์๋ก GPS ์ก์(๋ค์ค๊ฒฝ๋ก) ์ฆ๊ฐ\n",
" df = pd.DataFrame({\"t\": np.arange(n), \"x\": X + rng.normal(0, gps_sd, n), \"y\": Y + rng.normal(0, gps_sd, n),\n",
" \"alt\": A + rng.normal(0, 0.5, n), \"speed\": V + rng.normal(0, 0.2, n),\n",
" \"heading\": (HD + rng.normal(0, 1.5, n)) % 360, \"rc\": rc_arr.round(1),\n",
" \"sats\": rng.integers(10, 15, n), \"hdop\": rng.uniform(0.6, 1.2, n).round(2),\n",
" \"x_true\": X, \"y_true\": Y})\n",
" df[\"anomaly\"] = (df[\"t\"] >= win[0]) & (df[\"t\"] < win[1]) & (kind in SCENARIOS)\n",
" op = OPERATORS[int(rng.choice(3, p=OPERATOR_P[kind]))]\n",
" meta = {\"id\": fid, \"kind\": kind, \"weather\": weather, \"operator\": op, \"plan\": plan, \"window\": win if kind in SCENARIOS else None,\n",
" \"platform\": [\"commercial\", \"military\", \"unknown\"][int(rng.choice(3, p=PLATFORM_P[op]))],\n",
" \"firmware\": [\"stock\", \"custom\", \"unknown\"][int(rng.choice(3, p=FIRMWARE_P[op]))]}\n",
" return df, meta\n",
"\n",
"\n",
"def corrupt(df, rng, spoof=True, emi=True, drop=True, dup=True):\n",
" \"\"\"์ ์ฅ ์ก์ ์ฃผ์
: GPS ์คํธํ(์์น ์ ํยทํ๋ฅ), ์ ์๊ธฐ ๊ฐ์ญ(๊ณ ๋ ์คํ์ดํฌ), ์ผ์ ๋จ์ (๊ฒฐ์ธก), ์ค๋ณต ๊ธฐ๋ก.\"\"\"\n",
" d = df.copy()\n",
" d[\"spoof_true\"] = False\n",
" if spoof:\n",
" a = int(rng.integers(40, len(d) - 60)); L = int(rng.integers(25, 45))\n",
" th = rng.uniform(0, 2 * np.pi)\n",
" drift = rng.uniform(22, 28) * np.arange(L)[:, None] * np.array([np.cos(th), np.sin(th)])\n",
" jump = rng.uniform(250, 400) * np.array([np.cos(th + 1), np.sin(th + 1)])\n",
" d.loc[a:a + L - 1, [\"x\", \"y\"]] += drift + jump\n",
" d.loc[a:a + L - 1, \"spoof_true\"] = True\n",
" if emi:\n",
" idx = rng.choice(len(d), 6, replace=False)\n",
" d.loc[idx, \"alt\"] += rng.choice([-1, 1], 6) * rng.uniform(60, 150, 6)\n",
" if drop:\n",
" a = int(rng.integers(10, len(d) - 20))\n",
" d.loc[a:a + int(rng.integers(4, 9)), [\"x\", \"y\", \"alt\", \"speed\", \"heading\"]] = np.nan\n",
" d = d.drop(index=rng.choice(len(d), 5, replace=False))\n",
" if dup:\n",
" d = pd.concat([d, d.sample(5, random_state=int(rng.integers(1e6)))]).sort_values(\"t\", kind=\"stable\")\n",
" return d.reset_index(drop=True)\n",
"\n",
"\n",
"def make_dataset(n=200, seed=7, anomaly_ratio=0.5, scenarios=SCENARIOS):\n",
" rng = np.random.default_rng(seed)\n",
" out = []\n",
" for i in range(n):\n",
" if i % 10 < anomaly_ratio * 10:\n",
" kind = scenarios[(i // 10 * int(anomaly_ratio * 10) + i % 10) % len(scenarios)]\n",
" else:\n",
" kind = \"survey\" if i % 10 == 9 else \"normal\"\n",
" df, meta = make_flight(f\"FL{i:04d}\", kind, rng)\n",
" out.append((df, meta))\n",
" return out\n",
"# ---------------------------------------------------------------------------\n",
"\n",
"# ---- ์ ์ฒ๋ฆฌยท๊ท์น ํ์ง ๊ณตํต ํจ์ ----\n",
"SPOOF_THR = {\"clear\": 12.0, \"night\": 12.0, \"windy\": 15.0, \"rain\": 19.0} # ๊ธฐ์๋ณ GPS-์ถ์ธกํญ๋ฒ ๋ถ์ผ์น ํ์ฉ์น(m)\n",
"\n",
"\n",
"def clean(df, weather=\"clear\"):\n",
" \"\"\"์ ์ฒ๋ฆฌ: ์ค๋ณต ์ ๊ฑฐยท์ ๋ ฌ -> 1์ด ์ฌํ๋ณธํ -> ๊ฒฐ์ธก ๋ณด๊ฐ -> ๊ณ ๋ ์คํ์ดํฌ ์ ๊ฑฐ -> GPS ์คํธํ ๊ตฌ๊ฐ ์ถ์ธกํญ๋ฒ ๋ณด์ .\"\"\"\n",
" d = df.drop_duplicates(\"t\").sort_values(\"t\").set_index(\"t\")\n",
" d = d.reindex(np.arange(int(d.index.min()), int(d.index.max()) + 1))\n",
" n_missing = int(d[\"x\"].isna().sum())\n",
" num = [\"x\", \"y\", \"alt\", \"speed\", \"rc\", \"sats\", \"hdop\"]\n",
" d[num] = d[num].interpolate(limit_direction=\"both\")\n",
" hd = d[\"heading\"].dropna() # ๋ฐฉ์๋ 0/360 ๊ฒฝ๊ณ๋ฅผ ํ์ด์ ๋ณด๊ฐ\n",
" un = pd.Series(np.degrees(np.unwrap(np.radians(hd.to_numpy(float)))), index=hd.index)\n",
" d[\"heading\"] = un.reindex(d.index).interpolate(limit_direction=\"both\").to_numpy() % 360\n",
" med = d[\"alt\"].rolling(5, center=True, min_periods=1).median()\n",
" spike = (d[\"alt\"] - med).abs() > 25\n",
" d.loc[spike, \"alt\"] = med[spike]\n",
" x, y = d[\"x\"].to_numpy(float), d[\"y\"].to_numpy(float)\n",
" r = np.radians(d[\"heading\"].to_numpy(float)); v = d[\"speed\"].to_numpy(float)\n",
" dr = np.c_[v * np.sin(r), v * np.cos(r)] # ๋ณด๊ณ ์๋ยท๋ฐฉ์๋ก ๊ณ์ฐํ 1์ด ์ด๋๋(์ถ์ธกํญ๋ฒ)\n",
" xc, yc, flag = x.copy(), y.copy(), np.zeros(len(x), bool)\n",
" thr = SPOOF_THR.get(weather, 12.0)\n",
" ok_run = 0\n",
" for i in range(1, len(x)):\n",
" px, py = xc[i - 1] + dr[i, 0], yc[i - 1] + dr[i, 1] # ์ถ์ธกํญ๋ฒ ์์ ์์น\n",
" step_ok = np.hypot(x[i] - x[i - 1] - dr[i, 0], y[i] - y[i - 1] - dr[i, 1]) <= thr\n",
" ok_run = ok_run + 1 if step_ok else 0\n",
" if flag[i - 1]:\n",
" # ์คํธํ ๊ตฌ๊ฐ: GPS๊ฐ ์์ ์์น๋ก ๋์์ค๊ฑฐ๋, GPS ์ด๋๋์ด 5์ด ์ฐ์ ๋ฌผ๋ฆฌ์ ์ผ๋ก ๋ง์ผ๋ฉด GPS๋ก ๋ณต๊ท\n",
" if np.hypot(x[i] - px, y[i] - py) < thr * 2 or ok_run >= 5:\n",
" xc[i], yc[i] = x[i], y[i]\n",
" else:\n",
" xc[i], yc[i], flag[i] = px, py, True\n",
" elif not step_ok:\n",
" xc[i], yc[i], flag[i] = px, py, True # GPS ์ด๋๋์ด ๋ฌผ๋ฆฌ์ ์ผ๋ก ๋ง์ง ์์ -> ์คํธํ ์์ฌ\n",
" else:\n",
" xc[i], yc[i] = xc[i - 1] + x[i] - x[i - 1], yc[i - 1] + y[i] - y[i - 1]\n",
" d[\"x\"], d[\"y\"], d[\"spoof_flag\"] = xc, yc, flag\n",
" d = d.reset_index()\n",
" d.attrs.update({\"missing\": n_missing, \"spikes\": int(spike.sum()), \"spoof_pts\": int(flag.sum())})\n",
" return d\n",
"\n",
"\n",
"def xtrack(px, py, plan):\n",
" \"\"\"๊ณํ ๊ฒฝ๋ก(์จ์ดํฌ์ธํธ ํ๊ณก์ ) ๋๋น ์ดํ ๊ฑฐ๋ฆฌ(m).\"\"\"\n",
" P = np.asarray(plan, float)\n",
" segs = [(P[i], P[(i + 1) % len(P)]) for i in range(len(P) if len(P) > 2 else 1)]\n",
" best = np.full(len(px), np.inf)\n",
" for a, b in segs:\n",
" ab = b - a\n",
" u = np.clip(((px - a[0]) * ab[0] + (py - a[1]) * ab[1]) / max(ab @ ab, 1e-9), 0, 1)\n",
" best = np.minimum(best, np.hypot(px - a[0] - u * ab[0], py - a[1] - u * ab[1]))\n",
" return best\n",
"\n",
"\n",
"RULES = {\"turn_rate\": 45.0, \"hover_speed\": 1.5, \"hover_sec\": 20, \"dive_m\": 30.0, \"xtrack_m\": 250.0}\n",
"\n",
"\n",
"def rule_flags(d, plan, R=RULES):\n",
" \"\"\"๊ท์น ๊ธฐ๋ฐ ์ด์ ๋นํ ํ์ (๊ธฐ๋ก ๋จ์). ๊ธ์ ํยทํธ๋ฒ๋งยท๊ธ๊ฐํยท๊ฒฝ๋ก ์ดํยท๋น์ธ๊ฐ ๊ตฌ์ญ ์ง์
.\"\"\"\n",
" hr = np.abs(_wrap(np.diff(d[\"heading\"].to_numpy(float), prepend=d[\"heading\"].iloc[0])))\n",
" slow = (d[\"speed\"] < R[\"hover_speed\"]).to_numpy()\n",
" run = np.zeros(len(slow), int)\n",
" for i in range(len(slow)):\n",
" run[i] = run[i - 1] + 1 if slow[i] and i else int(slow[i])\n",
" hover = np.zeros(len(slow), bool)\n",
" for i in np.where(run >= R[\"hover_sec\"])[0]:\n",
" hover[i - R[\"hover_sec\"] + 1:i + 1] = True\n",
" alt = d[\"alt\"].to_numpy(float)\n",
" drop = alt - np.r_[alt[5:], np.full(5, alt[-1])]\n",
" return {\"๊ธ์ ํ\": hr > R[\"turn_rate\"], \"ํธ๋ฒ๋ง\": hover, \"๊ธ๊ฐํ\": drop > R[\"dive_m\"],\n",
" \"๊ฒฝ๋ก์ดํ\": xtrack(d[\"x\"].to_numpy(float), d[\"y\"].to_numpy(float), plan) > R[\"xtrack_m\"],\n",
" \"๋น์ธ๊ฐ๊ตฌ์ญ\": np.hypot(d[\"x\"] - NFZ[\"x\"], d[\"y\"] - NFZ[\"y\"]).to_numpy() < NFZ[\"r\"]}\n",
"\n",
"# ---- ๊ตฌ๊ฐ(30์ด ์ฐฝ) ํน์ง + ๋น์ง๋ ์ด์ ํ์ง ๊ณตํต ํจ์ ----\n",
"WFEAT = [\"alt_mean\", \"alt_min\", \"alt_std\", \"vz_min\", \"spd_mean\", \"spd_std\", \"spd_max\", \"turn_sum\", \"turn_max\",\n",
" \"straight\", \"rc_mean\", \"rc_zero\", \"nfz_min\", \"nfz_close\", \"xtrack_max\"]\n",
"\n",
"\n",
"def window_feats(d, plan, win=30, step=10):\n",
" \"\"\"30์ด ์ฐฝ์ 10์ด์ฉ ์ฎ๊ธฐ๋ฉฐ ๊ณ ๋ยท์๋ยท์ ํยท์ง์ง์ฑยท์กฐ์ข
์ ํธยท๋น์ธ๊ฐ ๊ตฌ์ญ ์ ๊ทผยท๊ฒฝ๋ก ์ดํ ํน์ง์ ๊ณ์ฐํ๋ค.\"\"\"\n",
" x, y, alt = d[\"x\"].to_numpy(float), d[\"y\"].to_numpy(float), d[\"alt\"].to_numpy(float)\n",
" spd, rc = d[\"speed\"].to_numpy(float), d[\"rc\"].to_numpy(float)\n",
" hr = np.abs(_wrap(np.diff(d[\"heading\"].to_numpy(float), prepend=d[\"heading\"].iloc[0])))\n",
" vz = np.diff(alt, prepend=alt[0])\n",
" nd = np.hypot(x - NFZ[\"x\"], y - NFZ[\"y\"])\n",
" xt = xtrack(x, y, plan)\n",
" step_len = np.hypot(np.diff(x, prepend=x[0]), np.diff(y, prepend=y[0]))\n",
" rows, starts = [], []\n",
" for s in range(0, len(d) - win + 1, step):\n",
" e = s + win\n",
" path = step_len[s + 1:e].sum()\n",
" rows.append([alt[s:e].mean(), alt[s:e].min(), alt[s:e].std(), vz[s:e].min(), spd[s:e].mean(), spd[s:e].std(),\n",
" spd[s:e].max(), hr[s:e].sum(), hr[s:e].max(), np.hypot(x[e - 1] - x[s], y[e - 1] - y[s]) / max(path, 1.0),\n",
" rc[s:e].mean(), (rc[s:e] < 5).mean(), nd[s:e].min(), (nd[s] - nd[e - 1]) / win, xt[s:e].max()])\n",
" starts.append(int(d[\"t\"].iloc[s]))\n",
" return np.array(rows), starts\n",
"\n",
"\n",
"class FlightAnomalyModel:\n",
" \"\"\"ํ์ ๋นํ๋ก๊ทธ(๋ผ๋ฒจ ์์)๋ก ํต์ ํจํด ๊ตฐ์ง(KMeans)๊ณผ ๊ณ ๋ฆฝ ์ฒ(IsolationForest)์ ํ์ตํ๊ณ ,\n",
" ๋ ์ ์๋ฅผ ํ์ต ๊ตฌ๊ฐ ๋ถ์์๋ก ๋๋ 1.0์ ๋์ผ๋ฉด 'ํต์ ํจํด์์ ๋ฒ์ด๋จ'์ผ๋ก ๋ณธ๋ค.\"\"\"\n",
"\n",
" def __init__(self, q=0.999, k=4):\n",
" self.q, self.k = q, k\n",
"\n",
" def fit(self, X):\n",
" self.sc = StandardScaler().fit(X)\n",
" Z = self.sc.transform(X)\n",
" self.iso = IsolationForest(n_estimators=150, random_state=0).fit(Z)\n",
" self.km = KMeans(self.k, n_init=5, random_state=0).fit(Z)\n",
" self.set_q(self.q, X)\n",
" return self\n",
"\n",
" def _raw(self, X):\n",
" Z = self.sc.transform(X)\n",
" return -self.iso.score_samples(Z), self.km.transform(Z).min(axis=1)\n",
"\n",
" def set_q(self, q, X):\n",
" a, b = self._raw(X)\n",
" self.q, self.t_if, self.t_km = q, np.quantile(a, q), np.quantile(b, q)\n",
"\n",
" def score(self, X):\n",
" a, b = self._raw(X)\n",
" return np.maximum(a / self.t_if, b / self.t_km)\n",
"\n",
"\n",
"AFEAT = [\"ml_score\", \"low_alt\", \"rc_weak\", \"nfz_min_km\", \"hover_s\", \"max_speed\", \"night\",\n",
" \"pf_commercial\", \"pf_military\", \"pf_unknown\", \"fw_stock\", \"fw_custom\", \"fw_unknown\"]\n",
"\n",
"\n",
"def attr_features(d, m, ml_score):\n",
" \"\"\"์ด์ฉ ์ฃผ์ฒด ์ถ์ ์ฉ ๋นํ ๋จ์ ํน์ง. ๊ธฐ์ฒดยทํ์จ์ด ์ ๋ณด๋ ์์ฉ ๊ธฐ์ฒด๊ฐ ๋ชจ๋ ์ฃผ์ฒด์ ์ฐ์ด๋ฏ๋ก ์ฝํ ๋จ์์ผ ๋ฟ์ด๋ค.\"\"\"\n",
" return [min(ml_score, 10.0), float((d[\"alt\"] < 60).mean()), float((d[\"rc\"] < 25).mean()),\n",
" float(np.hypot(d[\"x\"] - NFZ[\"x\"], d[\"y\"] - NFZ[\"y\"]).min() / 1000), float((d[\"speed\"] < 1.5).sum()),\n",
" float(d[\"speed\"].max()), float(m[\"weather\"] == \"night\"),\n",
" *[float(m[\"platform\"] == p) for p in (\"commercial\", \"military\", \"unknown\")],\n",
" *[float(m[\"firmware\"] == f) for f in (\"stock\", \"custom\", \"unknown\")]]\n",
"\n",
"P_HIGH, H_MAX, P_MED = 0.60, 0.80, 0.25 # ๋ฑ๊ธ ๊ธฐ์ค(์ ๋ ํ๋ฅ , ์ต๋ ๋ถํ์ค์ฑ, ๋ณด๋ฅ ํํ)\n",
"\n",
"\n",
"def build(data, model, rng, p_corrupt=0.25):\n",
" rows = []\n",
" for df, m in data:\n",
" noisy = rng.random() < p_corrupt\n",
" d = clean(corrupt(df, rng) if noisy else df, m[\"weather\"])\n",
" X, _ = window_feats(d, m[\"plan\"])\n",
" s = float(model.score(X).max())\n",
" flag = s > 1.0 or any(v.any() for v in rule_flags(d, m[\"plan\"]).values())\n",
" rows.append({\"id\": m[\"id\"], \"kind\": m[\"kind\"], \"op\": m[\"operator\"], \"flag\": flag,\n",
" \"quality_ok\": d.attrs[\"spoof_pts\"] < 10 and d.attrs[\"missing\"] < 15, \"f\": attr_features(d, m, s)})\n",
" return rows\n",
"\n",
"\n",
"def grade(p, H, q_ok):\n",
" if p >= P_HIGH and H <= H_MAX and q_ok:\n",
" return \"High\"\n",
" return \"Medium\" if p >= P_MED else \"Low\"\n",
"\n",
"\n",
"def assess(eng, rows, noise=None, rng=None):\n",
" X = np.array([r[\"f\"] for r in rows])\n",
" if noise:\n",
" X = X * (1 + rng.normal(0, noise, X.shape))\n",
" P = eng.predict_proba(X)\n",
" H = -(P * np.log(np.clip(P, 1e-9, 1))).sum(1) / np.log(3)\n",
" ph = P[:, list(eng.classes_).index(\"hostile\")]\n",
" return ph, H, [grade(p, h, r[\"quality_ok\"]) for p, h, r in zip(ph, H, rows)]\n",
"\n",
"\n",
"rng = np.random.default_rng(5)\n",
"normal_X = np.vstack([window_feats(clean(df, m[\"weather\"]), m[\"plan\"])[0] for df, m in make_dataset(120, 21, anomaly_ratio=0.0)])\n",
"det = FlightAnomalyModel().fit(normal_X)\n",
"train = build(make_dataset(240, seed=31), det, rng) # ๊ณผ๊ฑฐ ๊ฒํ ๊ฐ ๋๋ ์ฌ๋ก(๋ผ๋ฒจ ์์)๋ผ๊ณ ๊ฐ์ \n",
"test = build(make_dataset(200, seed=11), det, rng)\n",
"eng = make_pipeline(StandardScaler(), LogisticRegression(max_iter=2000, C=0.5))\n",
"eng.fit([r[\"f\"] for r in train], [r[\"op\"] for r in train])\n",
"ph, H, G = assess(eng, test)\n",
"y = np.array([r[\"op\"] == \"hostile\" for r in test])\n",
"\n",
"# 1) ๋จ์ ํ ๊ท์น(์ด์ ๋นํ์ด๋ฉด '์ ') vs ์ ๋ขฐ๋ ์์ง(High๋ง '์ ๋ ๊ฐ๋ฅ์ฑ ๋์')\n",
"flag = np.array([r[\"flag\"] for r in test]); high = np.array([g == \"High\" for g in G]); med = np.array([g == \"Medium\" for g in G])\n",
"far0 = (~y & flag).sum() / flag.sum(); far1 = (~y & high).sum() / max(high.sum(), 1)\n",
"print(\"๋ฐฉ์ ์ ๋ ์ง๋ชฉ ๊ทธ์ค ๋น์ ๋(์ค๊ฒฝ๋ณด) ์ค๊ฒฝ๋ณด์จ\")\n",
"print(f\"๋จ์ ํ ๊ท์น(์ด์=์ ) {flag.sum():6d} {(~y & flag).sum():14d} {far0:.1%}\")\n",
"print(f\"์ ๋ขฐ๋ ์์ง(High ๋ฑ๊ธ) {high.sum():6d} {(~y & high).sum():14d} {far1:.1%}\")\n",
"cut = 1 - far1 / far0\n",
"print(f\"์ค๊ฒฝ๋ณด์จ ๊ฐ์ถ {cut:.1%} (๋ชฉํ 40% ์ด์: {'๋ฌ์ฑ' if cut >= 0.4 else '๋ฏธ๋ฌ'})\")\n",
"print(f\"์ ๋ ๋นํ {y.sum()}๊ฑด ์ค High+Medium(๊ฒํ ๋์) {(y & (high | med)).sum()}๊ฑด, Low๋ก ๋ฐ๋ฆฐ ์ ๋ {(y & ~high & ~med).sum()}๊ฑด\")\n",
"print(f\"ROC-AUC(์ ๋ ํ๋ฅ ) {roc_auc_score(y, ph):.3f}\")\n",
"\n",
"# 2) ๋ฑ๊ธ ๋ถํฌ์ ์๊ณ๊ฐ ๋ฏผ๊ฐ๋\n",
"print(\"\\n[์ค์ ์ด์ฉ ์ฃผ์ฒด๋ณ ๋ฑ๊ธ ๋ถํฌ]\")\n",
"print(pd.crosstab(pd.Series([r[\"op\"] for r in test], name=\"์ค์ \"), pd.Series(G, name=\"๋ฑ๊ธ\")))\n",
"print(\"\\nHigh ๊ธฐ์ค High ์ ์ค๊ฒฝ๋ณด์จ ์ ๋ ํฌ์ฐฉ๋ฅ \")\n",
"for t in [0.5, 0.6, 0.7, 0.8]:\n",
" hh = (ph >= t) & (H <= H_MAX) & np.array([r[\"quality_ok\"] for r in test])\n",
" print(f\" {t:.1f} {hh.sum():5d} {(~y & hh).sum() / max(hh.sum(), 1):7.1%} {(y & hh).sum() / y.sum():8.1%}\")\n",
"\n",
"# 3) ์ผ๊ด์ฑ: ์
๋ ฅ ํน์ง์ +-2% ์ธก์ ์ก์์ 20๋ฒ ๋ฃ์ด๋ ๊ฐ์ ๋ฑ๊ธ์ด ๋์ค๋ ๋น์จ\n",
"same = np.mean([np.mean([a == b for a, b in zip(G, assess(eng, test, 0.02, rng)[2])]) for _ in range(20)])\n",
"print(f\"\\n์ ๋ขฐ๋ ๋ฑ๊ธ ์ฐ์ถ ์ผ๊ด์ฑ {same:.1%} (๋ชฉํ 90% ์ด์: {'๋ฌ์ฑ' if same >= 0.9 else '๋ฏธ๋ฌ'})\")\n",
"\n",
"# 4) ํ๋จ ๋ฌธ๊ตฌ: 'ํ์ ' ํํ ๊ธ์ง, ํ๋ฅ ยท๋ถํ์ค์ฑยท๊ทผ๊ฑฐยท๊ฒํ ํ์๋ฅผ ํจ๊ป ์ ๋๋ค\n",
"sc, lr = eng[0], eng[-1]\n",
"ci = list(lr.classes_).index(\"hostile\")\n",
"print(\"\\n[๋ถ์ ์๊ฒฌ ๋ฌธ๊ตฌ ์์]\")\n",
"lines = []\n",
"for i in list(np.argsort(-ph)[:2]) + [int(np.argmin(np.abs(ph - 0.5)))]:\n",
" z = (np.array(test[i][\"f\"]) - sc.mean_) / sc.scale_ * lr.coef_[ci]\n",
" why = \", \".join(AFEAT[j] for j in np.argsort(-z)[:2])\n",
" q = \"\" if test[i][\"quality_ok\"] else \", ๋ก๊ทธ ํ์ง ์ ํ(์คํธํยท๊ฒฐ์ธก)๋ก ๋ฑ๊ธ ํํฅ\"\n",
" lines.append(f\"{test[i]['id']}: ์ ๋ ์ด์ฉ ๊ฐ๋ฅ์ฑ {G[i]}(p={ph[i]:.2f}, ๋ถํ์ค์ฑ {H[i]:.2f}{q}), ๊ทผ๊ฑฐ {why} -> ์ ๋ณด๋ถ์๊ด ๊ฒํ ํ์\")\n",
"print(\"\\n\".join(\" \" + s for s in lines))\n",
"print(\"๊ธ์ง ํํ('ํ์ ', '์์ ๋ถ๋') ํฌํจ ๋ฌธ๊ตฌ:\", sum((\"ํ์ \" in s) or (\"์์ ๋ถ๋\" in s) for s in lines), \"๊ฑด\")\n",
"\n",
"fig, ax = plt.subplots(1, 2, figsize=(12, 4.5))\n",
"for op, c in [(\"friendly\", \"tab:blue\"), (\"civil\", \"tab:green\"), (\"hostile\", \"tab:red\")]:\n",
" sel = np.array([r[\"op\"] == op for r in test])\n",
" ax[0].scatter(ph[sel], H[sel], s=18, color=c, label=op, alpha=.7)\n",
"ax[0].axvline(P_HIGH, ls=\"--\", c=\"k\"); ax[0].axhline(H_MAX, ls=\"--\", c=\"k\")\n",
"ax[0].set_xlabel(\"P(hostile)\"); ax[0].set_ylabel(\"uncertainty (normalized entropy)\"); ax[0].legend(); ax[0].set_title(\"Attribution score vs uncertainty\")\n",
"pd.Series(G).value_counts().reindex([\"High\", \"Medium\", \"Low\"]).plot.bar(ax=ax[1], color=[\"tab:red\", \"tab:orange\", \"tab:gray\"])\n",
"ax[1].set_title(\"Grades (High = priority review, not a conclusion)\")\n",
"plt.tight_layout(); plt.show()\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"๐ก [์ฐธ๊ณ ] ๋ฏธ์
05 ์์ ์คํ ๊ฒฐ๊ณผ (Output)
\n",
"\n",
"```text\n",
"๋ฐฉ์ ์ ๋ ์ง๋ชฉ ๊ทธ์ค ๋น์ ๋(์ค๊ฒฝ๋ณด) ์ค๊ฒฝ๋ณด์จ\n",
"๋จ์ ํ ๊ท์น(์ด์=์ ) 116 71 61.2%\n",
"์ ๋ขฐ๋ ์์ง(High ๋ฑ๊ธ) 16 4 25.0%\n",
"์ค๊ฒฝ๋ณด์จ ๊ฐ์ถ 59.2% (๋ชฉํ 40% ์ด์: ๋ฌ์ฑ)\n",
"์ ๋ ๋นํ 48๊ฑด ์ค High+Medium(๊ฒํ ๋์) 34๊ฑด, Low๋ก ๋ฐ๋ฆฐ ์ ๋ 14๊ฑด\n",
"ROC-AUC(์ ๋ ํ๋ฅ ) 0.803\n",
"\n",
"[์ค์ ์ด์ฉ ์ฃผ์ฒด๋ณ ๋ฑ๊ธ ๋ถํฌ]\n",
"๋ฑ๊ธ High Low Medium\n",
"์ค์ \n",
"civil 0 69 18\n",
"friendly 4 43 18\n",
"hostile 12 14 22\n",
"\n",
"High ๊ธฐ์ค High ์ ์ค๊ฒฝ๋ณด์จ ์ ๋ ํฌ์ฐฉ๋ฅ \n",
" 0.5 17 29.4% 25.0%\n",
" 0.6 16 25.0% 25.0%\n",
" 0.7 15 26.7% 22.9%\n",
" 0.8 10 40.0% 12.5%\n",
"\n",
"์ ๋ขฐ๋ ๋ฑ๊ธ ์ฐ์ถ ์ผ๊ด์ฑ 99.1% (๋ชฉํ 90% ์ด์: ๋ฌ์ฑ)\n",
"\n",
"[๋ถ์ ์๊ฒฌ ๋ฌธ๊ตฌ ์์]\n",
" FL0112: ์ ๋ ์ด์ฉ ๊ฐ๋ฅ์ฑ High(p=0.96, ๋ถํ์ค์ฑ 0.16), ๊ทผ๊ฑฐ rc_weak, fw_unknown -> ์ ๋ณด๋ถ์๊ด ๊ฒํ ํ์\n",
" FL0032: ์ ๋ ์ด์ฉ ๊ฐ๋ฅ์ฑ High(p=0.96, ๋ถํ์ค์ฑ 0.16), ๊ทผ๊ฑฐ rc_weak, fw_unknown -> ์ ๋ณด๋ถ์๊ด ๊ฒํ ํ์\n",
" FL0180: ์ ๋ ์ด์ฉ ๊ฐ๋ฅ์ฑ Medium(p=0.48, ๋ถํ์ค์ฑ 0.96), ๊ทผ๊ฑฐ fw_stock, fw_custom -> ์ ๋ณด๋ถ์๊ด ๊ฒํ ํ์\n",
"๊ธ์ง ํํ('ํ์ ', '์์ ๋ถ๋') ํฌํจ ๋ฌธ๊ตฌ: 0 ๊ฑด\n",
"```\n",
" \n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## ๐ฏ ๋ฏธ์
06. ๋๋ก ๋นํ ๊ถค์ ์๊ฐํ ๋ฐ ์ด์ ํจํด ํ์ด๋ผ์ดํธ ๋ถ์ ๋์๋ณด๋ ๊ตฌํ\n",
"**์์์๊ฐ**: ์ฝ 2.5์๊ฐ\n",
"\n",
"### ๐ ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ์ํ ์ง์นจ\n",
"#### ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ๋น์์ฑ\n",
"\n",
"๋๋ก ์ 3D ๋นํ ๊ถค์ , ์กฐ์ข
์ ํธ ์ด์ ๊ตฌ๊ฐ, ์์ฌ ๊ตฌ์ญ ์ฒด๋ฅ ์๊ฐ์ ์ง๋์์ ์๊ฐํํ๋ ๋๋ก ์ ๋ณด ๋ถ์ ๋์๋ณด๋๋ฅผ ๊ตฌ์ถํ๋ค. ์ค์ ์ ์ ํ์ฅ์์๋ ๋จ์ ํ์ง์ ๊ทธ์น์ง ์๊ณ ์ ์ํ ์ํ ํ๋จ๊ณผ ์ ๋ขฐ๋ ๋์ ์์ฌ๊ฒฐ์ ์ด ํ์์ ์ด๋ฏ๋ก, **๋๋ก ์ 3D ๋นํ ๊ถค์ , ์กฐ์ข
์ ํธ ์ด์ ๊ตฌ๊ฐ, ์์ฌ ๊ตฌ์ญ ์ฒด๋ฅ ์๊ฐ, ๋น์ธ๊ฐ ๊ตฌ์ญ ์ ๊ทผ์ ํ ํ๋ฉด์ ๋ณด์ฌ ์ฃผ๋ ๋๋ก ์ ๋ณด ๋ถ์ ๋์๋ณด๋๋ฅผ ๊ตฌ์ถํ๋ค.** ๊ณผ์
์ ์์ํ์ฌ ์คํ์ ๋ฐฉ์งํ๊ณ ์์ ๋์๋ ฅ์ ๊ทน๋ํํด์ผ ํฉ๋๋ค.\n",
"\n",
"์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ณธ ๋ฏธ์
์์๋ **์ฌ๋ฌ ๊ธฐ์ฒด์ ๋นํ๋ก๊ทธ๋ฅผ 1์ด ์ฃผ๊ธฐ๋ก ๋ฐ์ ์ต๊ทผ 10์ด ์ฐฝ์ผ๋ก ๊ฒฝ๋ณด ์ํ๋ฅผ ๊ฐฑ์ ํ๋ ํ๋ฆ์ ๋ชจ์ํ๊ณ ์ฒ๋ฆฌ์๊ฐ์ ์ฐ๋ค ๊ณผ์ ์ ์งํํ๋** ์ค์ต์ ์ฒด๊ณ์ ์ผ๋ก ์ํํฉ๋๋ค.\n",
"\n",
"ํ์ต์๋ ์ค์ต๊ฐ์ด๋์ ์ ์๋ ์ด 4๊ฐ ์ธ๋ถ ํ๋(๊ธฐ๋ณธ ๊ธฐ๋ฅ ๊ตฌํ โ ๊ฒฐ๊ณผ ์งํ ๊ฒ์ฆ โ ํ๋ผ๋ฏธํฐ ํ๋ ๋ฐ ๋์ ๊ณผ์ )์ ์์ฐจ์ ์ผ๋ก ์งํํ๋ฉฐ ํต์ฌ ํ์ ๊ธฐ์ค์ ๋ฌ์ฑํฉ๋๋ค.\n",
"\n",
"**์ด๋ฒ ๊ฒฐ๊ณผ๋ฌผ:** `src/ui/isr03_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": [
"\"\"\"๋๋ก 3D ๋นํ ๊ถค์ , ์กฐ์ข
์ ํธ ์ด์ ๊ตฌ๊ฐ, ์์ฌ ๊ตฌ์ญ ์ฒด๋ฅ ์๊ฐ, ๋น์ธ๊ฐ ๊ตฌ์ญ ์ ๊ทผ์ ํ ํ๋ฉด์ ๋ณด์ฌ ์ฃผ๋ ๋๋ก ์ ๋ณด ๋ถ์ ๋์๋ณด๋๋ฅผ ๊ตฌํํ๊ณ , 1์ด ์ฃผ๊ธฐ ๋ฐ์ดํฐ ๊ฐฑ์ ์ฒ๋ฆฌ์๊ฐ๊ณผ ํ๋ฉด ๋ ๋๋ง ์๊ฐ์ ์ธก์ ํ๋ค.\"\"\"\n",
"import math\n",
"import time\n",
"import numpy as np\n",
"import pandas as pd\n",
"import matplotlib.pyplot as plt\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# [๊ต์ก์ฉ๊ฐ์์๋ฃ] ํฉ์ฑ ๋๋ก ๋นํ๋ก๊ทธ ์์ฑ๊ธฐ (์ค์ ์ขํยท๊ธฐ์ฒดยท๋ถ๋ ์ ๋ณด ์์)\n",
"# ๊ฐ์ ๊ตฌ์ญ(์์ ๊ธฐ์ค ๋ยท๋ถ ๋ฐฉํฅ m ์ขํ)์์ 1์ด ๊ฐ๊ฒฉ ๋นํ๋ก๊ทธ์ ์ ๋ต(์ด์ ๊ตฌ๊ฐยท์ด์ฉ ์ฃผ์ฒด)์ ๋ง๋ ๋ค.\n",
"# ---------------------------------------------------------------------------\n",
"NFZ = {\"x\": 2500.0, \"y\": 2500.0, \"r\": 700.0} # ๊ฐ์ ๋น์ธ๊ฐ ๋นํ๊ตฌ์ญ\n",
"WATCH = {\"x\": -2000.0, \"y\": 1500.0, \"r\": 600.0} # ๊ฐ์ ๊ด์ฌ ๊ตฌ์ญ\n",
"SCENARIOS = [\"sharp_turn\", \"hover\", \"dive\", \"deviation\", \"nfz_entry\",\n",
" \"low_level\", \"rc_silent\", \"infiltration\", \"loiter\", \"speed_burst\"]\n",
"OPERATORS = [\"friendly\", \"civil\", \"hostile\"]\n",
"# ์๋๋ฆฌ์ค๋ณ ์ด์ฉ ์ฃผ์ฒด ๋ถํฌ: ์ผ๋ถ๋ฌ ํฌ๊ฒ ๊ฒน์น๊ฒ ์ค๊ณํ๋ค(๋นํ ํน์ฑ๋ง์ผ๋ก ์ด์ฉ ์ฃผ์ฒด๋ฅผ ๋จ์ ํ ์ ์๋ค).\n",
"OPERATOR_P = {\"normal\": (.45, .47, .08), \"survey\": (.15, .82, .03), \"sharp_turn\": (.40, .35, .25),\n",
" \"hover\": (.25, .45, .30), \"dive\": (.25, .25, .50), \"deviation\": (.30, .40, .30),\n",
" \"nfz_entry\": (.10, .50, .40), \"low_level\": (.35, .15, .50), \"rc_silent\": (.30, .15, .55),\n",
" \"infiltration\": (.12, .08, .80), \"loiter\": (.30, .20, .50), \"speed_burst\": (.30, .20, .50)}\n",
"PLATFORM_P = {\"friendly\": (.40, .55, .05), \"civil\": (.88, .02, .10), \"hostile\": (.55, .30, .15)} # commercial/military/unknown\n",
"FIRMWARE_P = {\"friendly\": (.30, .65, .05), \"civil\": (.85, .10, .05), \"hostile\": (.20, .50, .30)} # stock/custom/unknown\n",
"\n",
"\n",
"def _wrap(a):\n",
" return (a + 180.0) % 360.0 - 180.0\n",
"\n",
"\n",
"def _bearing(p, q):\n",
" return math.degrees(math.atan2(q[0] - p[0], q[1] - p[1])) % 360.0\n",
"\n",
"\n",
"def _away_from_nfz(rng, min_d):\n",
" while True:\n",
" c = rng.uniform(-3800, 3800, 2)\n",
" if np.hypot(c[0] - NFZ[\"x\"], c[1] - NFZ[\"y\"]) > min_d:\n",
" return c\n",
"\n",
"\n",
"def make_flight(fid, kind, rng, n=300, weather=None):\n",
" \"\"\"๋นํ 1ํ = (๋ก๊ทธ DataFrame, ๋ฉํ dict). kind: normal / survey / SCENARIOS ์ค ํ๋\"\"\"\n",
" weather = weather or str(rng.choice([\"clear\", \"night\", \"windy\", \"rain\"], p=[.4, .25, .2, .15]))\n",
" wind = {\"clear\": 1.0, \"night\": 1.0, \"windy\": 2.5, \"rain\": 1.8}[weather]\n",
" v0, alt0 = rng.uniform(11, 17), rng.uniform(70, 150)\n",
" nfz_c = np.array([NFZ[\"x\"], NFZ[\"y\"]])\n",
" if kind == \"infiltration\": # ์๊ฑฐ๋ฆฌ์์ ๋น์ธ๊ฐ ๊ตฌ์ญ ๋ฐฉํฅ์ผ๋ก ์ ๊ณ ๋ ์ง์ง\n",
" ang = rng.uniform(0, 2 * np.pi)\n",
" start = nfz_c + 4000 * np.array([np.cos(ang), np.sin(ang)])\n",
" plan = np.array([start, nfz_c])\n",
" v0, alt0 = rng.uniform(9, 11), rng.uniform(25, 35)\n",
" else:\n",
" if kind == \"nfz_entry\": # ๋น์ธ๊ฐ ๊ตฌ์ญ 1.6km ๋ฐ์์ ์์ฐฐํ๋ค๊ฐ ์ง์
\n",
" ang = rng.uniform(0, 2 * np.pi)\n",
" c = nfz_c + 1600 * np.array([np.cos(ang), np.sin(ang)])\n",
" else:\n",
" c = _away_from_nfz(rng, 2600)\n",
" if kind == \"survey\": # ๋ฏผ๊ฐ ์ธก๋ํ ์๋ณต(์ง๊ทธ์ฌ๊ทธ) ๊ฒฝ๋ก\n",
" w, h = rng.uniform(500, 700), 120.0\n",
" plan = np.array([c + [(-w if i % 4 in (0, 3) else w), i // 2 * h - 360] for i in range(8)])\n",
" else: # ์ฌ๊ฐ ์์ฐฐ ๊ฒฝ๋ก(์จ์ดํฌ์ธํธ 4๊ฐ)\n",
" a, b, th = rng.uniform(300, 600), rng.uniform(200, 400), rng.uniform(0, np.pi)\n",
" R = np.array([[np.cos(th), -np.sin(th)], [np.sin(th), np.cos(th)]])\n",
" plan = c + np.array([[-a, -b], [a, -b], [a, b], [-a, b]]) @ R.T\n",
" s0 = int(rng.integers(80, 160))\n",
" span = {\"sharp_turn\": 30, \"hover\": 60, \"dive\": 8, \"deviation\": 70, \"nfz_entry\": 120, \"low_level\": 120,\n",
" \"rc_silent\": 120, \"infiltration\": n - 20, \"loiter\": 90, \"speed_burst\": 25}.get(kind, 0)\n",
" if kind == \"infiltration\":\n",
" s0 = 20\n",
" win = (s0, s0 + span)\n",
" turns = set(rng.choice(np.arange(s0, s0 + 30), 3, replace=False).tolist()) if kind == \"sharp_turn\" else set()\n",
" # ๋์๋ฅผ ๋ฏธ๋ฆฌ ํ ๋ฒ์ ๋ฝ์ ๋๊ณ (์๋ ํฅ์) 1์ด ๋จ์๋ก ๊ธฐ์ฒด ์ด๋์ ์ ๋ถํ๋ค.\n",
" e_h, e_v, e_a = rng.normal(0, 0.8 * wind, n), rng.normal(0, 0.3 * wind, n), rng.normal(0, 0.4 * wind, n)\n",
" rc_arr = np.clip(rng.normal(rng.uniform(60, 92), 6, n), 0, 100)\n",
" gust = (rng.random(n) < (0.004 if weather == \"windy\" else 0.0)) * rng.uniform(20, 35, n)\n",
" burst, turn_amt = rng.uniform(23, 28), rng.choice([-1, 1], n) * rng.uniform(100, 160, n)\n",
" side = float(rng.choice([-90, 90]))\n",
" low_alt, rng_rc = rng.uniform(35, 55), rng.uniform(0, 20, n) # ์ ๊ณ ๋ ๋ชฉํ, ์กฐ์ข
์ ํธ ๋์ ์ ์์ฌ ์์ ๊ฐ\n",
" x, y, wi = float(plan[0][0]), float(plan[0][1]), 1\n",
" hdg, alt = _bearing((x, y), plan[1]), alt0\n",
" max_rate = 40.0 if kind == \"survey\" else 12.0\n",
" dev_target, X, Y, A, V, HD = None, np.zeros(n), np.zeros(n), np.zeros(n), np.zeros(n), np.zeros(n)\n",
" for t in range(n):\n",
" inw = win[0] <= t < win[1]\n",
" tx, ty = plan[1] if kind == \"infiltration\" else plan[wi % len(plan)]\n",
" spd_cmd, alt_cmd = v0, alt0\n",
" if inw:\n",
" if kind == \"deviation\":\n",
" if dev_target is None:\n",
" r = math.radians(hdg + side)\n",
" dev_target = (x + 900 * math.sin(r), y + 900 * math.cos(r))\n",
" tx, ty = dev_target\n",
" elif kind == \"nfz_entry\":\n",
" tx, ty = NFZ[\"x\"], NFZ[\"y\"]\n",
" elif kind == \"hover\":\n",
" spd_cmd = 0.0\n",
" elif kind == \"speed_burst\":\n",
" spd_cmd = burst\n",
" elif kind == \"low_level\":\n",
" alt_cmd = low_alt\n",
" if kind in (\"rc_silent\", \"infiltration\"):\n",
" rc_arr[t] = rng_rc[t]\n",
" if inw and kind == \"loiter\":\n",
" hdg = (hdg + 9.0 + e_h[t] / wind) % 360\n",
" else:\n",
" d = _wrap(math.degrees(math.atan2(tx - x, ty - y)) - hdg)\n",
" hdg = (hdg + max(-max_rate, min(max_rate, d)) + e_h[t]) % 360\n",
" if t in turns:\n",
" hdg = (hdg + turn_amt[t]) % 360\n",
" v = abs(spd_cmd + e_v[t]) if spd_cmd > 0 else abs(e_v[t] / wind)\n",
" x += v * math.sin(math.radians(hdg)); y += v * math.cos(math.radians(hdg))\n",
" if kind != \"infiltration\" and math.hypot(x - tx, y - ty) < 40 and not (inw and kind in (\"deviation\", \"nfz_entry\")):\n",
" wi += 1\n",
" if inw and kind == \"dive\":\n",
" alt -= 9.0 + e_v[t]\n",
" else:\n",
" alt += max(-3.0, min(3.0, alt_cmd - alt)) + e_a[t]\n",
" alt -= gust[t] # ๋ํ ํ๊ฐ๊ธฐ๋ฅ(์ ์ ๋นํ์์๋ ๋ฐ์)\n",
" if kind == \"dive\" and t >= win[1]:\n",
" alt0 = min(alt0, alt)\n",
" alt = max(alt, 3.0)\n",
" X[t], Y[t], A[t], V[t], HD[t] = x, y, alt, v, hdg\n",
" gps_sd = {\"clear\": 2.0, \"night\": 2.0, \"windy\": 3.0, \"rain\": 4.5}[weather] # ์
์ฒํ์ผ์๋ก GPS ์ก์(๋ค์ค๊ฒฝ๋ก) ์ฆ๊ฐ\n",
" df = pd.DataFrame({\"t\": np.arange(n), \"x\": X + rng.normal(0, gps_sd, n), \"y\": Y + rng.normal(0, gps_sd, n),\n",
" \"alt\": A + rng.normal(0, 0.5, n), \"speed\": V + rng.normal(0, 0.2, n),\n",
" \"heading\": (HD + rng.normal(0, 1.5, n)) % 360, \"rc\": rc_arr.round(1),\n",
" \"sats\": rng.integers(10, 15, n), \"hdop\": rng.uniform(0.6, 1.2, n).round(2),\n",
" \"x_true\": X, \"y_true\": Y})\n",
" df[\"anomaly\"] = (df[\"t\"] >= win[0]) & (df[\"t\"] < win[1]) & (kind in SCENARIOS)\n",
" op = OPERATORS[int(rng.choice(3, p=OPERATOR_P[kind]))]\n",
" meta = {\"id\": fid, \"kind\": kind, \"weather\": weather, \"operator\": op, \"plan\": plan, \"window\": win if kind in SCENARIOS else None,\n",
" \"platform\": [\"commercial\", \"military\", \"unknown\"][int(rng.choice(3, p=PLATFORM_P[op]))],\n",
" \"firmware\": [\"stock\", \"custom\", \"unknown\"][int(rng.choice(3, p=FIRMWARE_P[op]))]}\n",
" return df, meta\n",
"\n",
"\n",
"def corrupt(df, rng, spoof=True, emi=True, drop=True, dup=True):\n",
" \"\"\"์ ์ฅ ์ก์ ์ฃผ์
: GPS ์คํธํ(์์น ์ ํยทํ๋ฅ), ์ ์๊ธฐ ๊ฐ์ญ(๊ณ ๋ ์คํ์ดํฌ), ์ผ์ ๋จ์ (๊ฒฐ์ธก), ์ค๋ณต ๊ธฐ๋ก.\"\"\"\n",
" d = df.copy()\n",
" d[\"spoof_true\"] = False\n",
" if spoof:\n",
" a = int(rng.integers(40, len(d) - 60)); L = int(rng.integers(25, 45))\n",
" th = rng.uniform(0, 2 * np.pi)\n",
" drift = rng.uniform(22, 28) * np.arange(L)[:, None] * np.array([np.cos(th), np.sin(th)])\n",
" jump = rng.uniform(250, 400) * np.array([np.cos(th + 1), np.sin(th + 1)])\n",
" d.loc[a:a + L - 1, [\"x\", \"y\"]] += drift + jump\n",
" d.loc[a:a + L - 1, \"spoof_true\"] = True\n",
" if emi:\n",
" idx = rng.choice(len(d), 6, replace=False)\n",
" d.loc[idx, \"alt\"] += rng.choice([-1, 1], 6) * rng.uniform(60, 150, 6)\n",
" if drop:\n",
" a = int(rng.integers(10, len(d) - 20))\n",
" d.loc[a:a + int(rng.integers(4, 9)), [\"x\", \"y\", \"alt\", \"speed\", \"heading\"]] = np.nan\n",
" d = d.drop(index=rng.choice(len(d), 5, replace=False))\n",
" if dup:\n",
" d = pd.concat([d, d.sample(5, random_state=int(rng.integers(1e6)))]).sort_values(\"t\", kind=\"stable\")\n",
" return d.reset_index(drop=True)\n",
"\n",
"\n",
"def make_dataset(n=200, seed=7, anomaly_ratio=0.5, scenarios=SCENARIOS):\n",
" rng = np.random.default_rng(seed)\n",
" out = []\n",
" for i in range(n):\n",
" if i % 10 < anomaly_ratio * 10:\n",
" kind = scenarios[(i // 10 * int(anomaly_ratio * 10) + i % 10) % len(scenarios)]\n",
" else:\n",
" kind = \"survey\" if i % 10 == 9 else \"normal\"\n",
" df, meta = make_flight(f\"FL{i:04d}\", kind, rng)\n",
" out.append((df, meta))\n",
" return out\n",
"# ---------------------------------------------------------------------------\n",
"\n",
"# ---- ์ ์ฒ๋ฆฌยท๊ท์น ํ์ง ๊ณตํต ํจ์ ----\n",
"SPOOF_THR = {\"clear\": 12.0, \"night\": 12.0, \"windy\": 15.0, \"rain\": 19.0} # ๊ธฐ์๋ณ GPS-์ถ์ธกํญ๋ฒ ๋ถ์ผ์น ํ์ฉ์น(m)\n",
"\n",
"\n",
"def clean(df, weather=\"clear\"):\n",
" \"\"\"์ ์ฒ๋ฆฌ: ์ค๋ณต ์ ๊ฑฐยท์ ๋ ฌ -> 1์ด ์ฌํ๋ณธํ -> ๊ฒฐ์ธก ๋ณด๊ฐ -> ๊ณ ๋ ์คํ์ดํฌ ์ ๊ฑฐ -> GPS ์คํธํ ๊ตฌ๊ฐ ์ถ์ธกํญ๋ฒ ๋ณด์ .\"\"\"\n",
" d = df.drop_duplicates(\"t\").sort_values(\"t\").set_index(\"t\")\n",
" d = d.reindex(np.arange(int(d.index.min()), int(d.index.max()) + 1))\n",
" n_missing = int(d[\"x\"].isna().sum())\n",
" num = [\"x\", \"y\", \"alt\", \"speed\", \"rc\", \"sats\", \"hdop\"]\n",
" d[num] = d[num].interpolate(limit_direction=\"both\")\n",
" hd = d[\"heading\"].dropna() # ๋ฐฉ์๋ 0/360 ๊ฒฝ๊ณ๋ฅผ ํ์ด์ ๋ณด๊ฐ\n",
" un = pd.Series(np.degrees(np.unwrap(np.radians(hd.to_numpy(float)))), index=hd.index)\n",
" d[\"heading\"] = un.reindex(d.index).interpolate(limit_direction=\"both\").to_numpy() % 360\n",
" med = d[\"alt\"].rolling(5, center=True, min_periods=1).median()\n",
" spike = (d[\"alt\"] - med).abs() > 25\n",
" d.loc[spike, \"alt\"] = med[spike]\n",
" x, y = d[\"x\"].to_numpy(float), d[\"y\"].to_numpy(float)\n",
" r = np.radians(d[\"heading\"].to_numpy(float)); v = d[\"speed\"].to_numpy(float)\n",
" dr = np.c_[v * np.sin(r), v * np.cos(r)] # ๋ณด๊ณ ์๋ยท๋ฐฉ์๋ก ๊ณ์ฐํ 1์ด ์ด๋๋(์ถ์ธกํญ๋ฒ)\n",
" xc, yc, flag = x.copy(), y.copy(), np.zeros(len(x), bool)\n",
" thr = SPOOF_THR.get(weather, 12.0)\n",
" ok_run = 0\n",
" for i in range(1, len(x)):\n",
" px, py = xc[i - 1] + dr[i, 0], yc[i - 1] + dr[i, 1] # ์ถ์ธกํญ๋ฒ ์์ ์์น\n",
" step_ok = np.hypot(x[i] - x[i - 1] - dr[i, 0], y[i] - y[i - 1] - dr[i, 1]) <= thr\n",
" ok_run = ok_run + 1 if step_ok else 0\n",
" if flag[i - 1]:\n",
" # ์คํธํ ๊ตฌ๊ฐ: GPS๊ฐ ์์ ์์น๋ก ๋์์ค๊ฑฐ๋, GPS ์ด๋๋์ด 5์ด ์ฐ์ ๋ฌผ๋ฆฌ์ ์ผ๋ก ๋ง์ผ๋ฉด GPS๋ก ๋ณต๊ท\n",
" if np.hypot(x[i] - px, y[i] - py) < thr * 2 or ok_run >= 5:\n",
" xc[i], yc[i] = x[i], y[i]\n",
" else:\n",
" xc[i], yc[i], flag[i] = px, py, True\n",
" elif not step_ok:\n",
" xc[i], yc[i], flag[i] = px, py, True # GPS ์ด๋๋์ด ๋ฌผ๋ฆฌ์ ์ผ๋ก ๋ง์ง ์์ -> ์คํธํ ์์ฌ\n",
" else:\n",
" xc[i], yc[i] = xc[i - 1] + x[i] - x[i - 1], yc[i - 1] + y[i] - y[i - 1]\n",
" d[\"x\"], d[\"y\"], d[\"spoof_flag\"] = xc, yc, flag\n",
" d = d.reset_index()\n",
" d.attrs.update({\"missing\": n_missing, \"spikes\": int(spike.sum()), \"spoof_pts\": int(flag.sum())})\n",
" return d\n",
"\n",
"\n",
"def xtrack(px, py, plan):\n",
" \"\"\"๊ณํ ๊ฒฝ๋ก(์จ์ดํฌ์ธํธ ํ๊ณก์ ) ๋๋น ์ดํ ๊ฑฐ๋ฆฌ(m).\"\"\"\n",
" P = np.asarray(plan, float)\n",
" segs = [(P[i], P[(i + 1) % len(P)]) for i in range(len(P) if len(P) > 2 else 1)]\n",
" best = np.full(len(px), np.inf)\n",
" for a, b in segs:\n",
" ab = b - a\n",
" u = np.clip(((px - a[0]) * ab[0] + (py - a[1]) * ab[1]) / max(ab @ ab, 1e-9), 0, 1)\n",
" best = np.minimum(best, np.hypot(px - a[0] - u * ab[0], py - a[1] - u * ab[1]))\n",
" return best\n",
"\n",
"\n",
"RULES = {\"turn_rate\": 45.0, \"hover_speed\": 1.5, \"hover_sec\": 20, \"dive_m\": 30.0, \"xtrack_m\": 250.0}\n",
"\n",
"\n",
"def rule_flags(d, plan, R=RULES):\n",
" \"\"\"๊ท์น ๊ธฐ๋ฐ ์ด์ ๋นํ ํ์ (๊ธฐ๋ก ๋จ์). ๊ธ์ ํยทํธ๋ฒ๋งยท๊ธ๊ฐํยท๊ฒฝ๋ก ์ดํยท๋น์ธ๊ฐ ๊ตฌ์ญ ์ง์
.\"\"\"\n",
" hr = np.abs(_wrap(np.diff(d[\"heading\"].to_numpy(float), prepend=d[\"heading\"].iloc[0])))\n",
" slow = (d[\"speed\"] < R[\"hover_speed\"]).to_numpy()\n",
" run = np.zeros(len(slow), int)\n",
" for i in range(len(slow)):\n",
" run[i] = run[i - 1] + 1 if slow[i] and i else int(slow[i])\n",
" hover = np.zeros(len(slow), bool)\n",
" for i in np.where(run >= R[\"hover_sec\"])[0]:\n",
" hover[i - R[\"hover_sec\"] + 1:i + 1] = True\n",
" alt = d[\"alt\"].to_numpy(float)\n",
" drop = alt - np.r_[alt[5:], np.full(5, alt[-1])]\n",
" return {\"๊ธ์ ํ\": hr > R[\"turn_rate\"], \"ํธ๋ฒ๋ง\": hover, \"๊ธ๊ฐํ\": drop > R[\"dive_m\"],\n",
" \"๊ฒฝ๋ก์ดํ\": xtrack(d[\"x\"].to_numpy(float), d[\"y\"].to_numpy(float), plan) > R[\"xtrack_m\"],\n",
" \"๋น์ธ๊ฐ๊ตฌ์ญ\": np.hypot(d[\"x\"] - NFZ[\"x\"], d[\"y\"] - NFZ[\"y\"]).to_numpy() < NFZ[\"r\"]}\n",
"\n",
"\n",
"def highlight(d, plan):\n",
" \"\"\"๊ฐ์กฐ ํ์ํ ๊ธฐ๋ก: ๊ท์น ํ์ง + ์ ๊ณ ๋(60m ๋ฏธ๋ง) + ์กฐ์ข
์ ํธ ์ฝํ(25 ๋ฏธ๋ง) + ๊ณ ์(20m/s ์ด๊ณผ).\"\"\"\n",
" f = rule_flags(d, plan)\n",
" f[\"์ ๊ณ ๋\"] = (d[\"alt\"] < 60).to_numpy()\n",
" f[\"์กฐ์ข
์ ํธ์ฝํ\"] = (d[\"rc\"] < 25).to_numpy()\n",
" f[\"๊ณ ์\"] = (d[\"speed\"] > 20).to_numpy()\n",
" return f\n",
"\n",
"\n",
"def dwell_seconds(d, radius=300, span=60):\n",
" \"\"\"60์ด ๋์ 300m ์์ ๋จธ๋ฌธ ์์ ์(์ฒด๋ฅ ์๊ฐ, ์ด).\"\"\"\n",
" x, y = d[\"x\"].to_numpy(), d[\"y\"].to_numpy()\n",
" return int(sum(np.hypot(x[i:i + span] - x[i], y[i:i + span] - y[i]).max() < radius for i in range(0, len(x) - span)))\n",
"\n",
"\n",
"# [๊ต์ก์ฉ๊ฐ์์๋ฃ] ๋ถ์ ๋์ ๋นํ๋ก๊ทธ 6๊ฑด\n",
"pool = make_dataset(40, seed=11)\n",
"want = [\"infiltration\", \"loiter\", \"nfz_entry\", \"rc_silent\", \"hover\", \"normal\"]\n",
"sel = [next((df, m) for df, m in pool if m[\"kind\"] == k) for k in want]\n",
"streams = [(df, m) for df, m in sel]\n",
"\n",
"# 1) 1์ด ์ฃผ๊ธฐ ์คํธ๋ฆผ ๊ฐฑ์ ๋ชจ์: ๋งค ์ด 6๊ฐ ๊ธฐ์ฒด์ ์ ๊ธฐ๋ก 1๊ฑด์ฉ ๋ฐ์ ์ต๊ทผ 10์ด ์ฐฝ์ผ๋ก ๊ฒฝ๋ณด ์ํ ๊ฐฑ์ \n",
"upd, alerts = [], 0\n",
"for t in range(10, 300):\n",
" t0 = time.perf_counter()\n",
" for df, m in streams:\n",
" w = df.iloc[t - 9:t + 1]\n",
" nfz_in = bool((np.hypot(w[\"x\"] - NFZ[\"x\"], w[\"y\"] - NFZ[\"y\"]) < NFZ[\"r\"]).any())\n",
" alerts += int(nfz_in or ((w[\"alt\"] < 60).all() and (w[\"rc\"] < 25).all()) or (w[\"speed\"] > 20).any())\n",
" upd.append((time.perf_counter() - t0) * 1000)\n",
"print(f\"๋ฐ์ดํฐ ๊ฐฑ์ {len(upd)}ํ, ์ฒ๋ฆฌ์๊ฐ ํ๊ท {np.mean(upd):.2f} ms, ์ต๋ {np.max(upd):.2f} ms (๋ชฉํ: 1์ด ์ฃผ๊ธฐ ์์์ 100 ms ์ด๋ด), ๋์ ๊ฒฝ๋ณด ํ์ {alerts}ํ\")\n",
"\n",
"# 2) ์ํ ์์ฝํ\n",
"rows = []\n",
"for df, m in streams:\n",
" d = clean(df, m[\"weather\"]); f = highlight(d, m[\"plan\"])\n",
" dw = dwell_seconds(d)\n",
" hit = [k for k, v in f.items() if v.any()] + ([\"์ฅ์๊ฐ์ฒด๋ฅ\"] if dw > 30 else [])\n",
" rows.append({\"ID\": m[\"id\"], \"์ต์ ๊ณ ๋(m)\": int(d[\"alt\"].min()), \"์ต๋์๋(m/s)\": round(float(d[\"speed\"].max()), 1),\n",
" \"์ ํธ์ฝํ(์ด)\": int(f[\"์กฐ์ข
์ ํธ์ฝํ\"].sum()), \"์ฒด๋ฅ(์ด)\": dw,\n",
" \"๋น์ธ๊ฐ๊ตฌ์ญ ์ต๊ทผ์ (m)\": int(np.hypot(d[\"x\"] - NFZ[\"x\"], d[\"y\"] - NFZ[\"y\"]).min()),\n",
" \"๊ฐ์กฐ ํญ๋ชฉ\": \",\".join(hit) or \"-\", \"๊ธด๊ธ\": bool(f[\"๋น์ธ๊ฐ๊ตฌ์ญ\"].any() or (f[\"์ ๊ณ ๋\"].any() and f[\"์กฐ์ข
์ ํธ์ฝํ\"].any()))})\n",
" m[\"d\"], m[\"f\"] = d, f\n",
"table = pd.DataFrame(rows).sort_values(\"๊ธด๊ธ\", ascending=False)\n",
"print(\"\\n[๋๋ก ๋นํ ์ด์ ํํฉ: ๊ธด๊ธ ์ฐ์ ]\")\n",
"print(table.to_string(index=False))\n",
"\n",
"# 3) ๋์๋ณด๋(์ผ๊ฐ ์ด์ฉ์ฉ ์ด๋์ด ํ๋ฉด)\n",
"plt.style.use(\"dark_background\")\n",
"fig = plt.figure(figsize=(14, 9))\n",
"ax3 = fig.add_subplot(2, 2, 1, projection=\"3d\")\n",
"axm, axr, axd = fig.add_subplot(2, 2, 2), fig.add_subplot(2, 2, 3), fig.add_subplot(2, 2, 4)\n",
"colors = plt.cm.tab10(np.arange(len(streams)))\n",
"for c, (df, m) in zip(colors, streams):\n",
" d, f = m[\"d\"], m[\"f\"]\n",
" hl = np.any(list(f.values()), axis=0)\n",
" ax3.plot(d[\"x\"], d[\"y\"], d[\"alt\"], color=c, lw=1)\n",
" ax3.scatter(d[\"x\"][hl], d[\"y\"][hl], d[\"alt\"][hl], color=\"red\", s=4)\n",
" axm.plot(d[\"x\"], d[\"y\"], color=c, lw=1, label=m[\"id\"])\n",
" axm.plot(d[\"x\"][hl], d[\"y\"][hl], \".\", color=\"red\", ms=2)\n",
" axr.plot(d[\"t\"], d[\"rc\"], color=c, lw=1)\n",
"ax3.set_title(\"3D flight paths (red = highlighted)\"); ax3.set_xlabel(\"E (m)\"); ax3.set_ylabel(\"N (m)\"); ax3.set_zlabel(\"alt (m)\")\n",
"for z, col, lab in [(NFZ, \"red\", \"no-fly zone (virtual)\"), (WATCH, \"orange\", \"watch area (virtual)\")]:\n",
" axm.add_patch(plt.Circle((z[\"x\"], z[\"y\"]), z[\"r\"], color=col, alpha=0.2, label=lab))\n",
"axm.set_aspect(\"equal\"); axm.legend(fontsize=7, loc=\"lower left\"); axm.set_title(\"Map view\"); axm.grid(alpha=.2)\n",
"axr.axhspan(0, 25, color=\"red\", alpha=0.2, label=\"weak / lost RC link\")\n",
"axr.set_title(\"RC link quality timeline\"); axr.set_xlabel(\"time (s)\"); axr.set_ylabel(\"RC link (0-100)\"); axr.legend(fontsize=8)\n",
"axd.barh(table[\"ID\"], table[\"์ฒด๋ฅ(์ด)\"], color=[\"red\" if u else \"deepskyblue\" for u in table[\"๊ธด๊ธ\"]])\n",
"axd.set_title(\"Dwell time within 300 m (red = urgent)\"); axd.set_xlabel(\"seconds\")\n",
"fig.suptitle(\"ISR03 drone log analysis dashboard - training data only\")\n",
"t0 = time.perf_counter(); fig.canvas.draw(); draw_ms = (time.perf_counter() - t0) * 1000\n",
"plt.tight_layout(); plt.show()\n",
"print(f\"\\nํ๋ฉด ๋ ๋๋ง {draw_ms:.0f} ms (๊ฐฑ์ ์ฃผ๊ธฐ 1์ด ์ด๋ด: {'๋ฌ์ฑ' if draw_ms < 1000 else '๋ฏธ๋ฌ'}), ์๊ฐํ ๊ตฌ์ฑ์์ 4์ข
(3D ๊ถค์ ยท์ง๋ยท์กฐ์ข
์ ํธ ์๊ณ์ดยท์ฒด๋ฅ ์๊ฐ)\")\n",
"print(\"๋์๋ณด๋์ '๊ธด๊ธ'์ ๋ถ์ ์ฐ์ ์์ ํ์์ด๋ฉฐ ์ด์ฉ ์ฃผ์ฒด ํ๋จ์ด ์๋๋ค. ํ๋จ์ ๋ฏธ์
7์ ์ ๋ณด๋ถ์๊ด ๊ฒํ ๋ฅผ ๊ฑฐ์น๋ค.\")\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"๐ก [์ฐธ๊ณ ] ๋ฏธ์
06 ์์ ์คํ ๊ฒฐ๊ณผ (Output)
\n",
"\n",
"```text\n",
"๋ฐ์ดํฐ ๊ฐฑ์ 290ํ, ์ฒ๋ฆฌ์๊ฐ ํ๊ท 3.92 ms, ์ต๋ 55.84 ms (๋ชฉํ: 1์ด ์ฃผ๊ธฐ ์์์ 100 ms ์ด๋ด), ๋์ ๊ฒฝ๋ณด ํ์ 428ํ\n",
"\n",
"[๋๋ก ๋นํ ์ด์ ํํฉ: ๊ธด๊ธ ์ฐ์ ]\n",
" ID ์ต์ ๊ณ ๋(m) ์ต๋์๋(m/s) ์ ํธ์ฝํ(์ด) ์ฒด๋ฅ(์ด) ๋น์ธ๊ฐ๊ตฌ์ญ ์ต๊ทผ์ (m) ๊ฐ์กฐ ํญ๋ชฉ ๊ธด๊ธ\n",
"FL0012 26 10.7 280 0 1068 ์ ๊ณ ๋,์กฐ์ข
์ ํธ์ฝํ True\n",
"FL0004 71 13.3 0 18 2 ๊ฒฝ๋ก์ดํ,๋น์ธ๊ฐ๊ตฌ์ญ True\n",
"FL0013 103 16.9 0 72 3331 ์ฅ์๊ฐ์ฒด๋ฅ False\n",
"FL0011 117 15.8 120 0 5822 ์กฐ์ข
์ ํธ์ฝํ False\n",
"FL0001 101 17.9 0 41 3575 ํธ๋ฒ๋ง,์ฅ์๊ฐ์ฒด๋ฅ False\n",
"FL0005 118 16.0 0 0 4709 - False\n",
"\n",
"ํ๋ฉด ๋ ๋๋ง 335 ms (๊ฐฑ์ ์ฃผ๊ธฐ 1์ด ์ด๋ด: ๋ฌ์ฑ), ์๊ฐํ ๊ตฌ์ฑ์์ 4์ข
(3D ๊ถค์ ยท์ง๋ยท์กฐ์ข
์ ํธ ์๊ณ์ดยท์ฒด๋ฅ ์๊ฐ)\n",
"๋์๋ณด๋์ '๊ธด๊ธ'์ ๋ถ์ ์ฐ์ ์์ ํ์์ด๋ฉฐ ์ด์ฉ ์ฃผ์ฒด ํ๋จ์ด ์๋๋ค. ํ๋จ์ ๋ฏธ์
7์ ์ ๋ณด๋ถ์๊ด ๊ฒํ ๋ฅผ ๊ฑฐ์น๋ค.\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",
"ํ์ต์๋ ์ค์ต๊ฐ์ด๋์ ์ ์๋ ์ด 5๊ฐ ์ธ๋ถ ํ๋(๊ธฐ๋ณธ ๊ธฐ๋ฅ ๊ตฌํ โ ๊ฒฐ๊ณผ ์งํ ๊ฒ์ฆ โ ํ๋ผ๋ฏธํฐ ํ๋ ๋ฐ ๋์ ๊ณผ์ )์ ์์ฐจ์ ์ผ๋ก ์งํํ๋ฉฐ ํต์ฌ ํ์ ๊ธฐ์ค์ ๋ฌ์ฑํฉ๋๋ค.\n",
"\n",
"**์ด๋ฒ ๊ฒฐ๊ณผ๋ฌผ:** `src/hitl/isr03_approval_workflow.py ๋ฐ docs/isr03_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": [
"\"\"\"๋๋ก ์ด์ ๋นํ ๋ถ์ ๊ฒฐ๊ณผ๋ฅผ ์ ๋ณด๋ถ์๊ด์ด ์ถ์ฒ ํ์ธ๊ณผ ๋ก๊ทธ ๋ฌด๊ฒฐ์ฑ(ํด์) ๊ฒ์ฆ์ ๊ฑฐ์ณ ์ฑํยท๋ณด๋ฅยท๋ฐ๋ คํ๋ ์ธ๊ฐ ๊ฐ์
(HITL) ์ฌ์ธต ๊ฒํ ์ํฌํ๋ก์ฐ๋ฅผ ๊ตฌํํ๋ค. ์ด์ฉ ์ฃผ์ฒด ๊ด๋ จ ์๊ฒฌ์ 2์ธ ๊ต์ฐจ ๊ฒํ ๋ฅผ ๊ฑฐ์ณ์ผ ์ฑํ๋๋ฉฐ, ๋ชจ๋ ๊ฒฐ์ ์ ์ยท๋ณ์กฐ ํ์ธ์ด ๊ฐ๋ฅํ ๊ฐ์ฌ ๋ก๊ทธ์ ๋จ๋๋ค.\"\"\"\n",
"import hashlib\n",
"import json\n",
"import time\n",
"import pandas as pd\n",
"\n",
"ACTIONS = {\"ADOPT\": \"์ฑํ(๋ถ์ ์๊ฒฌ์ผ๋ก ๋ณด๊ณ )\", \"HOLD\": \"๋ณด๋ฅ(์ถ๊ฐ ์ถ์ฒ ํ์)\", \"REJECT\": \"๋ฐ๋ ค(๊ทผ๊ฑฐ ๋ถ์กฑยท์คํ)\"}\n",
"PRIORITY = {\"High\": 0, \"Medium\": 1, \"Low\": 2}\n",
"HOLD_LIMIT_S = 1800\n",
"\n",
"\n",
"class WorkflowError(ValueError):\n",
" pass\n",
"\n",
"\n",
"class ReviewQueue:\n",
" def __init__(self, clock=time.time):\n",
" self.items, self.log, self.clock = {}, [], clock\n",
"\n",
" def _record(self, body):\n",
" prev = self.log[-1][\"hash\"] if self.log else \"GENESIS\"\n",
" body = {**body, \"ts\": round(self.clock(), 3)}\n",
" h = hashlib.sha256((prev + json.dumps(body, ensure_ascii=False, sort_keys=True)).encode()).hexdigest()\n",
" self.log.append({**body, \"prev\": prev, \"hash\": h})\n",
"\n",
" def verify_log(self):\n",
" prev = \"GENESIS\"\n",
" for n, rec in enumerate(self.log, start=1):\n",
" body = {k: v for k, v in rec.items() if k not in (\"prev\", \"hash\")}\n",
" if rec[\"prev\"] != prev or rec[\"hash\"] != hashlib.sha256((prev + json.dumps(body, ensure_ascii=False, sort_keys=True)).encode()).hexdigest():\n",
" return False, n\n",
" prev = rec[\"hash\"]\n",
" return True, -1\n",
"\n",
" def submit(self, fid, grade, p, claim, log_bytes):\n",
" \"\"\"๋ถ์ ์์ง ๊ฒฐ๊ณผ ๋ฑ๋ก. ํญ์ PENDING์ผ๋ก ์์ํ๋ฉฐ ์๋ณธ ๋ก๊ทธ ํด์๋ฅผ ํจ๊ป ๋ณด๊ดํ๋ค.\"\"\"\n",
" self.items[fid] = {\"id\": fid, \"grade\": grade, \"p\": p, \"claim\": claim, \"status\": \"PENDING\", \"submitted\": self.clock(),\n",
" \"hash\": hashlib.sha256(log_bytes).hexdigest(), \"source_ok\": False, \"integrity_ok\": None,\n",
" \"first\": None, \"escalated\": False, \"hold_since\": None}\n",
" self._record({\"event\": \"SUBMIT\", \"id\": fid, \"grade\": grade, \"by\": \"ENGINE\"})\n",
"\n",
" def check_source(self, fid, analyst, note):\n",
" self.items[fid][\"source_ok\"] = True\n",
" self._record({\"event\": \"SOURCE_CHECK\", \"id\": fid, \"analyst\": analyst, \"note\": note})\n",
"\n",
" def check_integrity(self, fid, analyst, log_bytes):\n",
" ok = hashlib.sha256(log_bytes).hexdigest() == self.items[fid][\"hash\"]\n",
" self.items[fid][\"integrity_ok\"] = ok\n",
" self._record({\"event\": \"INTEGRITY_CHECK\", \"id\": fid, \"analyst\": analyst, \"result\": ok})\n",
" return ok\n",
"\n",
" def pending(self):\n",
" todo = [i for i in self.items.values() if i[\"status\"] in (\"PENDING\", \"HOLD\", \"ADOPT_1ST\")]\n",
" return sorted(todo, key=lambda i: (not i[\"escalated\"], PRIORITY[i[\"grade\"]], -i[\"p\"]))\n",
"\n",
" def decide(self, fid, action, analyst, reason):\n",
" it = self.items.get(fid)\n",
" if action not in ACTIONS:\n",
" raise WorkflowError(f\"ํ์ฉ๋์ง ์์ ์กฐ์น: {action}\")\n",
" if not analyst or analyst.upper() in (\"AI\", \"ENGINE\", \"SYSTEM\"):\n",
" raise WorkflowError(\"์ฌ๋ ๋ถ์๊ด ID ํ์(์๋ ๊ฒฐ์ ๊ธ์ง)\")\n",
" if it is None or it[\"status\"] in (\"ADOPTED\", \"REJECTED\"):\n",
" raise WorkflowError(\"๋๊ธฐ์ด์ ์๊ฑฐ๋ ์ด๋ฏธ ์ข
๊ฒฐ๋ ๊ฑด\")\n",
" attrib = \"์ด์ฉ ์ฃผ์ฒด\" in it[\"claim\"]\n",
" if len(reason.strip()) < (20 if attrib and action == \"ADOPT\" else 8):\n",
" raise WorkflowError(\"ํ๋จ ๊ทผ๊ฑฐ ๋ถ์กฑ(์ด์ฉ ์ฃผ์ฒด ์๊ฒฌ ์ฑํ์ 20์ ์ด์)\")\n",
" if action == \"ADOPT\":\n",
" if not it[\"source_ok\"] or it[\"integrity_ok\"] is not True:\n",
" raise WorkflowError(\"์ถ์ฒ ํ์ธยท๋ก๊ทธ ๋ฌด๊ฒฐ์ฑ ๊ฒ์ฆ ์ ์๋ ์ฑํ ๋ถ๊ฐ\")\n",
" if attrib and it[\"first\"] is None:\n",
" it[\"first\"], it[\"status\"] = analyst, \"ADOPT_1ST\"\n",
" self._record({\"event\": \"ADOPT_1ST\", \"id\": fid, \"analyst\": analyst, \"reason\": reason})\n",
" return\n",
" if attrib and it[\"first\"] == analyst:\n",
" raise WorkflowError(\"2์ธ ๊ต์ฐจ ๊ฒํ : 1์ฐจ ๊ฒํ ์์ ๋ค๋ฅธ ๋ถ์๊ด์ด ํ์\")\n",
" it[\"status\"] = {\"ADOPT\": \"ADOPTED\", \"HOLD\": \"HOLD\", \"REJECT\": \"REJECTED\"}[action]\n",
" it[\"hold_since\"] = self.clock() if action == \"HOLD\" else None\n",
" it[\"decided\"] = self.clock()\n",
" self._record({\"event\": \"DECIDE\", \"id\": fid, \"action\": action, \"analyst\": analyst, \"reason\": reason})\n",
"\n",
" def check_holds(self):\n",
" \"\"\"๋ณด๋ฅ ๊ธฐํ ์ด๊ณผ ์ ์๋ ์ฑํํ์ง ์๊ณ ์ ์ ๋ถ์๊ด์๊ฒ ์์ ํ๋ค.\"\"\"\n",
" out = []\n",
" for i in self.items.values():\n",
" if i[\"status\"] == \"HOLD\" and not i[\"escalated\"] and self.clock() - i[\"hold_since\"] > HOLD_LIMIT_S:\n",
" i[\"escalated\"] = True; out.append(i[\"id\"])\n",
" self._record({\"event\": \"ESCALATE\", \"id\": i[\"id\"], \"by\": \"SYSTEM\", \"to\": \"์ ์ ๋ถ์๊ด\"})\n",
" return out\n",
"\n",
" def feedback(self, fid, commander, text):\n",
" \"\"\"์งํ๊ด ํผ๋๋ฐฑ ์
๋ ฅ(๊ฒฐ์ ์ ๋ฐ๊พธ์ง ์๊ณ ๊ธฐ๋ก๋ง ๋จ๊ธด๋ค).\"\"\"\n",
" self._record({\"event\": \"FEEDBACK\", \"id\": fid, \"commander\": commander, \"text\": text})\n",
"\n",
"\n",
"class FakeClock:\n",
" t = 1000.0\n",
"\n",
" def __call__(self):\n",
" return self.t\n",
"\n",
"\n",
"clock = FakeClock()\n",
"q = ReviewQueue(clock)\n",
"# [๊ต์ก์ฉ๊ฐ์์๋ฃ] ๋ฏธ์
5 ์์ง์์ ๋์ด์จ ๋ถ์ ๊ฒฐ๊ณผ 5๊ฑด๊ณผ ์๋ณธ ๋ก๊ทธ(๋ฐ์ดํธ)\n",
"logs = {f\"FL{i:04d}\": f\"virtual-log-{i}\".encode() for i in (12, 32, 64, 112, 180)}\n",
"for fid, g, p, claim in [(\"FL0032\", \"High\", 0.96, \"์ด์ฉ ์ฃผ์ฒด: ์ ๋ ์ด์ฉ ๊ฐ๋ฅ์ฑ ๋์\"), (\"FL0112\", \"High\", 0.91, \"์ด์ฉ ์ฃผ์ฒด: ์ ๋ ์ด์ฉ ๊ฐ๋ฅ์ฑ ๋์\"),\n",
" (\"FL0064\", \"Medium\", 0.51, \"์ด์ฉ ์ฃผ์ฒด: ํ๋จ ๋ณด๋ฅ\"), (\"FL0012\", \"Medium\", 0.44, \"์ด์ ๋นํ: ์ ๊ณ ๋ยท์กฐ์ข
์ ํธ ๋์ \"),\n",
" (\"FL0180\", \"Low\", 0.21, \"์ด์ ๋นํ: ์ฒด๋ฅ\")]:\n",
" q.submit(fid, g, p, claim, logs[fid])\n",
"print(\"๊ฒํ ์์:\", [i[\"id\"] for i in q.pending()])\n",
"print(\"์๋ ์ฑํ๋ ๊ฑด์:\", sum(i[\"status\"] == \"ADOPTED\" for i in q.items.values()))\n",
"\n",
"# ๊ท์น ์๋ฐ ์๋ ์ฐจ๋จ ํ์ธ\n",
"tries = [(\"FL0032\", \"ADOPT\", \"AI\", \"์์ง ์ ์๊ฐ ๋์ ์๋ ์ฑํ\"), (\"FL0032\", \"ADOPT\", \"an01\", \"์กฐ์ข
์ ํธ ๋์ ๊ณผ ๋น์ธ๊ฐ ๊ตฌ์ญ ์ ๊ทผ์ด ๊ฒน์นจ\"),\n",
" (\"FL0012\", \"PUBLISH\", \"an01\", \"ํ์ฉ๋์ง ์์ ์กฐ์น ์๋\")]\n",
"for args in tries:\n",
" try:\n",
" q.decide(*args)\n",
" except WorkflowError as e:\n",
" print(\"์ฐจ๋จ:\", e)\n",
"\n",
"clock.t += 60; q.check_source(\"FL0032\", \"an01\", \"๋
ธํ ๊ธฐ์ฒด ์ธ๊ณ ๊ธฐ๋ก๊ณผ ์์ง ๊ฒฝ์ ์ผ์น\")\n",
"tampered = logs[\"FL0112\"] + b\"edited\"\n",
"print(\"FL0112 ๋ฌด๊ฒฐ์ฑ ๊ฒ์ฆ(์์ง ํ ๋ณ๊ฒฝ๋ ๋ก๊ทธ):\", q.check_integrity(\"FL0112\", \"an01\", tampered))\n",
"q.check_integrity(\"FL0032\", \"an01\", logs[\"FL0032\"])\n",
"clock.t += 120; q.decide(\"FL0032\", \"ADOPT\", \"an01\", \"์ ๊ณ ๋ ์กฐ์ข
์ ํธ ๋์ ๋นํ์ด ๋น์ธ๊ฐ ๊ตฌ์ญ ๋ฐฉํฅ์ผ๋ก ์ง์, ์ถ๊ฐ ์ถ์ฒ 1๊ฑด ์ผ์น\")\n",
"try:\n",
" q.decide(\"FL0032\", \"ADOPT\", \"an01\", \"๊ฐ์ ๋ถ์๊ด์ด 2์ฐจ ์ฑํ์ ์๋ํ๋ ๊ฒฝ์ฐ\")\n",
"except WorkflowError as e:\n",
" print(\"์ฐจ๋จ:\", e)\n",
"clock.t += 300; q.decide(\"FL0032\", \"ADOPT\", \"an02\", \"1์ฐจ ์๊ฒฌ ์ฌ๊ฒํ ๊ฒฐ๊ณผ ๋์, ๋จ ํน์ ๋ถ๋ ๊ท์์ ๊ทผ๊ฑฐ ์์์ผ๋ก ์ ์ธ\")\n",
"q.check_source(\"FL0112\", \"an02\", \"์๋ณธ ์ฌ์์ฒญ ํ์\"); q.decide(\"FL0112\", \"HOLD\", \"an02\", \"๋ก๊ทธ ํด์ ๋ถ์ผ์น, ์๋ณธ ์ฌ์์ง ์์ฒญ\")\n",
"clock.t += 200; q.decide(\"FL0064\", \"HOLD\", \"an01\", \"๋ถํ์ค์ฑ ๋์, ์ถ๊ฐ ์ถ์ฒ ํ์\")\n",
"clock.t += 100; q.decide(\"FL0180\", \"REJECT\", \"an02\", \"์ ์ ์ธก๋ ๋นํ๊ณผ ์ผ์น, ์คํ\")\n",
"clock.t += 2000\n",
"print(\"๋ณด๋ฅ ๊ธฐํ ์ด๊ณผ ์์ :\", q.check_holds())\n",
"q.feedback(\"FL0032\", \"cmd01\", \"๋ธ๋ฆฌํ์ ๋ถํ์ค์ฑ ์์น๋ฅผ ํจ๊ป ํ๊ธฐํ ๊ฒ\")\n",
"\n",
"df = pd.DataFrame(q.items.values())\n",
"df[\"๋๊ธฐ์๊ฐ(s)\"] = (df[\"decided\"] - df[\"submitted\"]).round(0)\n",
"print(\"\\n\" + df[[\"id\", \"grade\", \"claim\", \"status\", \"first\", \"escalated\", \"๋๊ธฐ์๊ฐ(s)\"]].fillna(\"-\").to_string(index=False))\n",
"ok, pos = q.verify_log()\n",
"print(f\"\\n๊ฐ์ฌ ๋ก๊ทธ {len(q.log)}๊ฑด ๊ธฐ๋ก(์ ์ถยท์ถ์ฒํ์ธยท๋ฌด๊ฒฐ์ฑยท๊ฒฐ์ ยท์์ ยทํผ๋๋ฐฑ), ๋ก๊น
์ฑ๊ณต๋ฅ {len(q.log) / len(q.log):.0%}, ํด์ ์ฒด์ธ ๊ฒ์ฆ: {ok}\")\n",
"q.log[6][\"analyst\"] = \"an99\"\n",
"print(\"7๋ฒ์งธ ๊ธฐ๋ก ๋ณ์กฐ ํ ๊ฒ์ฆ(False, ๊นจ์ง ์์น):\", q.verify_log())\n",
"print(\"\\n์ฑํ์ '๋ถ์ ์๊ฒฌ'์ผ๋ก ๋ณด๊ณ ํ๋ ๋จ๊ณ์ด๋ฉฐ, ํน์ ๊ตญ๊ฐยท๋ถ๋๋ก์ ๊ท์ ํ๋จ์ ์งํ๊ณํต์ ์ถ๊ฐ ๊ฒํ ์ฌํญ์ด๋ค.\")\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"๐ก [์ฐธ๊ณ ] ๋ฏธ์
07 ์์ ์คํ ๊ฒฐ๊ณผ (Output)
\n",
"\n",
"```text\n",
"๊ฒํ ์์: ['FL0032', 'FL0112', 'FL0064', 'FL0012', 'FL0180']\n",
"์๋ ์ฑํ๋ ๊ฑด์: 0\n",
"์ฐจ๋จ: ์ฌ๋ ๋ถ์๊ด ID ํ์(์๋ ๊ฒฐ์ ๊ธ์ง)\n",
"์ฐจ๋จ: ์ถ์ฒ ํ์ธยท๋ก๊ทธ ๋ฌด๊ฒฐ์ฑ ๊ฒ์ฆ ์ ์๋ ์ฑํ ๋ถ๊ฐ\n",
"์ฐจ๋จ: ํ์ฉ๋์ง ์์ ์กฐ์น: PUBLISH\n",
"FL0112 ๋ฌด๊ฒฐ์ฑ ๊ฒ์ฆ(์์ง ํ ๋ณ๊ฒฝ๋ ๋ก๊ทธ): False\n",
"์ฐจ๋จ: 2์ธ ๊ต์ฐจ ๊ฒํ : 1์ฐจ ๊ฒํ ์์ ๋ค๋ฅธ ๋ถ์๊ด์ด ํ์\n",
"๋ณด๋ฅ ๊ธฐํ ์ด๊ณผ ์์ : ['FL0112', 'FL0064']\n",
"\n",
" id grade claim status first escalated ๋๊ธฐ์๊ฐ(s)\n",
"FL0032 High ์ด์ฉ ์ฃผ์ฒด: ์ ๋ ์ด์ฉ ๊ฐ๋ฅ์ฑ ๋์ ADOPTED an01 False 480.0\n",
"FL0112 High ์ด์ฉ ์ฃผ์ฒด: ์ ๋ ์ด์ฉ ๊ฐ๋ฅ์ฑ ๋์ HOLD - True 480.0\n",
"FL0064 Medium ์ด์ฉ ์ฃผ์ฒด: ํ๋จ ๋ณด๋ฅ HOLD - True 680.0\n",
"FL0012 Medium ์ด์ ๋นํ: ์ ๊ณ ๋ยท์กฐ์ข
์ ํธ ๋์ PENDING - False -\n",
"FL0180 Low ์ด์ ๋นํ: ์ฒด๋ฅ REJECTED - False 780.0\n",
"\n",
"๊ฐ์ฌ ๋ก๊ทธ 17๊ฑด ๊ธฐ๋ก(์ ์ถยท์ถ์ฒํ์ธยท๋ฌด๊ฒฐ์ฑยท๊ฒฐ์ ยท์์ ยทํผ๋๋ฐฑ), ๋ก๊น
์ฑ๊ณต๋ฅ 100%, ํด์ ์ฒด์ธ ๊ฒ์ฆ: True\n",
"7๋ฒ์งธ ๊ธฐ๋ก ๋ณ์กฐ ํ ๊ฒ์ฆ(False, ๊นจ์ง ์์น): (False, 7)\n",
"\n",
"์ฑํ์ '๋ถ์ ์๊ฒฌ'์ผ๋ก ๋ณด๊ณ ํ๋ ๋จ๊ณ์ด๋ฉฐ, ํน์ ๊ตญ๊ฐยท๋ถ๋๋ก์ ๊ท์ ํ๋จ์ ์งํ๊ณํต์ ์ถ๊ฐ ๊ฒํ ์ฌํญ์ด๋ค.\n",
"```\n",
" \n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## ๐ฏ ๋ฏธ์
08. ์ฅ๊ฑฐ๋ฆฌ ์ ์ ๋นํ ๋๋ ๋ก๊ทธ ์ฃผ์
์๋ฎฌ๋ ์ด์
๋ฐ ์ด์ ํ์ง ์ฒ๋ฆฌ ์ฑ๋ฅ ํ
์คํธ\n",
"**์์์๊ฐ**: ์ฝ 3.5์๊ฐ\n",
"\n",
"### ๐ ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ์ํ ์ง์นจ\n",
"#### ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ๋น์์ฑ\n",
"\n",
"์๋ง ์๊ฐ ๋ถ๋์ ๋๊ท๋ชจ ๋๋ก ๋นํ๋ก๊ทธ ๋ฐ์ดํฐ๋ฅผ ์ผ๊ด ๋ฐฐ์น ์ฒ๋ฆฌํ์ฌ ์ด์ ํ์ง ์ฒ๋ฆฌ๋ ๋ฐ ๋ฉ๋ชจ๋ฆฌ ์์ ์ฑ์ ๊ฒ์ฆํ๋ ๋ถํ ํ
์คํธ๋ฅผ ์ํํ๋ค. ์ค์ ์ ์ ํ์ฅ์์๋ ๋จ์ ํ์ง์ ๊ทธ์น์ง ์๊ณ ์ ์ํ ์ํ ํ๋จ๊ณผ ์ ๋ขฐ๋ ๋์ ์์ฌ๊ฒฐ์ ์ด ํ์์ ์ด๋ฏ๋ก, **๋๋ ๋นํ๋ก๊ทธ๋ฅผ ๋ฐฐ์น๋ก ์ฃผ์
ํด ์ด์ ํ์ง ์ฒ๋ฆฌ๋, ๋ฉ๋ชจ๋ฆฌ ์์ ์ฑ, ๋ณ๋ชฉ ๊ตฌ๊ฐ, ๊ธฐ๋ฅ ์ถ์ ์๋์ ๊ฒ์ฆํ๋ค.** ๊ณผ์
์ ์์ํ์ฌ ์คํ์ ๋ฐฉ์งํ๊ณ ์์ ๋์๋ ฅ์ ๊ทน๋ํํด์ผ ํฉ๋๋ค.\n",
"\n",
"์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ณธ ๋ฏธ์
์์๋ **ํ์ ๋ถํ ๋๋น 500% ์ฆ๊ฐํ ๋นํ๋ก๊ทธ, ์์ยท์ก์ ๋ก๊ทธ ํผ์
, ์ฒ๋ฆฌ ์๊ฐ ์์ฐ ๋ถ์กฑ ์ํฉ์ ๊ฐ์์ผ๋ก ์ฃผ์
ํ์ฌ ์ฒ๋ฆฌ ์ง์ฐ ๋ฐ ๋ณต์๋ ฅ์ ์ํํ๋** ์ค์ต์ ์ฒด๊ณ์ ์ผ๋ก ์ํํฉ๋๋ค.\n",
"\n",
"ํ์ต์๋ ์ค์ต๊ฐ์ด๋์ ์ ์๋ ์ด 5๊ฐ ์ธ๋ถ ํ๋(๊ธฐ๋ณธ ๊ธฐ๋ฅ ๊ตฌํ โ ๊ฒฐ๊ณผ ์งํ ๊ฒ์ฆ โ ํ๋ผ๋ฏธํฐ ํ๋ ๋ฐ ๋์ ๊ณผ์ )์ ์์ฐจ์ ์ผ๋ก ์งํํ๋ฉฐ ํต์ฌ ํ์ ๊ธฐ์ค์ ๋ฌ์ฑํฉ๋๋ค.\n",
"\n",
"**์ด๋ฒ ๊ฒฐ๊ณผ๋ฌผ:** `tests/isr03_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": [
"\"\"\"๋๋ ๋๋ก ๋นํ๋ก๊ทธ๋ฅผ ๋ฐฐ์น๋ก ์ฃผ์
ํด ํ์ ๋๋น 500% ์ฆ๊ฐ ๋ถํ, ์์ ๋ก๊ทธ ํผ์
, ์ฒ๋ฆฌ ์๊ฐ ์์ฐ ๋ถ์กฑ ์ํฉ์์ ์ด์ ํ์ง ํ์ดํ๋ผ์ธ์ ๋ค์ด ์ฌ๋ถยท๋ฐ์ดํฐ ์ ์ค๋ฅ ยท์ฒ๋ฆฌ๋ยท๋ฉ๋ชจ๋ฆฌ ์์ ์ฑยท๋ณ๋ชฉ ๊ตฌ๊ฐยท๊ธฐ๋ฅ ์ถ์(์ฑ๋ฅ์ ํ) ๋ชจ๋๋ฅผ ๊ฒ์ฆํ๋ค.\"\"\"\n",
"import math\n",
"import time\n",
"import tracemalloc\n",
"from collections import deque\n",
"import numpy as np\n",
"import pandas as pd\n",
"import matplotlib.pyplot as plt\n",
"from sklearn.cluster import KMeans\n",
"from sklearn.ensemble import IsolationForest\n",
"from sklearn.preprocessing import StandardScaler\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# [๊ต์ก์ฉ๊ฐ์์๋ฃ] ํฉ์ฑ ๋๋ก ๋นํ๋ก๊ทธ ์์ฑ๊ธฐ (์ค์ ์ขํยท๊ธฐ์ฒดยท๋ถ๋ ์ ๋ณด ์์)\n",
"# ๊ฐ์ ๊ตฌ์ญ(์์ ๊ธฐ์ค ๋ยท๋ถ ๋ฐฉํฅ m ์ขํ)์์ 1์ด ๊ฐ๊ฒฉ ๋นํ๋ก๊ทธ์ ์ ๋ต(์ด์ ๊ตฌ๊ฐยท์ด์ฉ ์ฃผ์ฒด)์ ๋ง๋ ๋ค.\n",
"# ---------------------------------------------------------------------------\n",
"NFZ = {\"x\": 2500.0, \"y\": 2500.0, \"r\": 700.0} # ๊ฐ์ ๋น์ธ๊ฐ ๋นํ๊ตฌ์ญ\n",
"WATCH = {\"x\": -2000.0, \"y\": 1500.0, \"r\": 600.0} # ๊ฐ์ ๊ด์ฌ ๊ตฌ์ญ\n",
"SCENARIOS = [\"sharp_turn\", \"hover\", \"dive\", \"deviation\", \"nfz_entry\",\n",
" \"low_level\", \"rc_silent\", \"infiltration\", \"loiter\", \"speed_burst\"]\n",
"OPERATORS = [\"friendly\", \"civil\", \"hostile\"]\n",
"# ์๋๋ฆฌ์ค๋ณ ์ด์ฉ ์ฃผ์ฒด ๋ถํฌ: ์ผ๋ถ๋ฌ ํฌ๊ฒ ๊ฒน์น๊ฒ ์ค๊ณํ๋ค(๋นํ ํน์ฑ๋ง์ผ๋ก ์ด์ฉ ์ฃผ์ฒด๋ฅผ ๋จ์ ํ ์ ์๋ค).\n",
"OPERATOR_P = {\"normal\": (.45, .47, .08), \"survey\": (.15, .82, .03), \"sharp_turn\": (.40, .35, .25),\n",
" \"hover\": (.25, .45, .30), \"dive\": (.25, .25, .50), \"deviation\": (.30, .40, .30),\n",
" \"nfz_entry\": (.10, .50, .40), \"low_level\": (.35, .15, .50), \"rc_silent\": (.30, .15, .55),\n",
" \"infiltration\": (.12, .08, .80), \"loiter\": (.30, .20, .50), \"speed_burst\": (.30, .20, .50)}\n",
"PLATFORM_P = {\"friendly\": (.40, .55, .05), \"civil\": (.88, .02, .10), \"hostile\": (.55, .30, .15)} # commercial/military/unknown\n",
"FIRMWARE_P = {\"friendly\": (.30, .65, .05), \"civil\": (.85, .10, .05), \"hostile\": (.20, .50, .30)} # stock/custom/unknown\n",
"\n",
"\n",
"def _wrap(a):\n",
" return (a + 180.0) % 360.0 - 180.0\n",
"\n",
"\n",
"def _bearing(p, q):\n",
" return math.degrees(math.atan2(q[0] - p[0], q[1] - p[1])) % 360.0\n",
"\n",
"\n",
"def _away_from_nfz(rng, min_d):\n",
" while True:\n",
" c = rng.uniform(-3800, 3800, 2)\n",
" if np.hypot(c[0] - NFZ[\"x\"], c[1] - NFZ[\"y\"]) > min_d:\n",
" return c\n",
"\n",
"\n",
"def make_flight(fid, kind, rng, n=300, weather=None):\n",
" \"\"\"๋นํ 1ํ = (๋ก๊ทธ DataFrame, ๋ฉํ dict). kind: normal / survey / SCENARIOS ์ค ํ๋\"\"\"\n",
" weather = weather or str(rng.choice([\"clear\", \"night\", \"windy\", \"rain\"], p=[.4, .25, .2, .15]))\n",
" wind = {\"clear\": 1.0, \"night\": 1.0, \"windy\": 2.5, \"rain\": 1.8}[weather]\n",
" v0, alt0 = rng.uniform(11, 17), rng.uniform(70, 150)\n",
" nfz_c = np.array([NFZ[\"x\"], NFZ[\"y\"]])\n",
" if kind == \"infiltration\": # ์๊ฑฐ๋ฆฌ์์ ๋น์ธ๊ฐ ๊ตฌ์ญ ๋ฐฉํฅ์ผ๋ก ์ ๊ณ ๋ ์ง์ง\n",
" ang = rng.uniform(0, 2 * np.pi)\n",
" start = nfz_c + 4000 * np.array([np.cos(ang), np.sin(ang)])\n",
" plan = np.array([start, nfz_c])\n",
" v0, alt0 = rng.uniform(9, 11), rng.uniform(25, 35)\n",
" else:\n",
" if kind == \"nfz_entry\": # ๋น์ธ๊ฐ ๊ตฌ์ญ 1.6km ๋ฐ์์ ์์ฐฐํ๋ค๊ฐ ์ง์
\n",
" ang = rng.uniform(0, 2 * np.pi)\n",
" c = nfz_c + 1600 * np.array([np.cos(ang), np.sin(ang)])\n",
" else:\n",
" c = _away_from_nfz(rng, 2600)\n",
" if kind == \"survey\": # ๋ฏผ๊ฐ ์ธก๋ํ ์๋ณต(์ง๊ทธ์ฌ๊ทธ) ๊ฒฝ๋ก\n",
" w, h = rng.uniform(500, 700), 120.0\n",
" plan = np.array([c + [(-w if i % 4 in (0, 3) else w), i // 2 * h - 360] for i in range(8)])\n",
" else: # ์ฌ๊ฐ ์์ฐฐ ๊ฒฝ๋ก(์จ์ดํฌ์ธํธ 4๊ฐ)\n",
" a, b, th = rng.uniform(300, 600), rng.uniform(200, 400), rng.uniform(0, np.pi)\n",
" R = np.array([[np.cos(th), -np.sin(th)], [np.sin(th), np.cos(th)]])\n",
" plan = c + np.array([[-a, -b], [a, -b], [a, b], [-a, b]]) @ R.T\n",
" s0 = int(rng.integers(80, 160))\n",
" span = {\"sharp_turn\": 30, \"hover\": 60, \"dive\": 8, \"deviation\": 70, \"nfz_entry\": 120, \"low_level\": 120,\n",
" \"rc_silent\": 120, \"infiltration\": n - 20, \"loiter\": 90, \"speed_burst\": 25}.get(kind, 0)\n",
" if kind == \"infiltration\":\n",
" s0 = 20\n",
" win = (s0, s0 + span)\n",
" turns = set(rng.choice(np.arange(s0, s0 + 30), 3, replace=False).tolist()) if kind == \"sharp_turn\" else set()\n",
" # ๋์๋ฅผ ๋ฏธ๋ฆฌ ํ ๋ฒ์ ๋ฝ์ ๋๊ณ (์๋ ํฅ์) 1์ด ๋จ์๋ก ๊ธฐ์ฒด ์ด๋์ ์ ๋ถํ๋ค.\n",
" e_h, e_v, e_a = rng.normal(0, 0.8 * wind, n), rng.normal(0, 0.3 * wind, n), rng.normal(0, 0.4 * wind, n)\n",
" rc_arr = np.clip(rng.normal(rng.uniform(60, 92), 6, n), 0, 100)\n",
" gust = (rng.random(n) < (0.004 if weather == \"windy\" else 0.0)) * rng.uniform(20, 35, n)\n",
" burst, turn_amt = rng.uniform(23, 28), rng.choice([-1, 1], n) * rng.uniform(100, 160, n)\n",
" side = float(rng.choice([-90, 90]))\n",
" low_alt, rng_rc = rng.uniform(35, 55), rng.uniform(0, 20, n) # ์ ๊ณ ๋ ๋ชฉํ, ์กฐ์ข
์ ํธ ๋์ ์ ์์ฌ ์์ ๊ฐ\n",
" x, y, wi = float(plan[0][0]), float(plan[0][1]), 1\n",
" hdg, alt = _bearing((x, y), plan[1]), alt0\n",
" max_rate = 40.0 if kind == \"survey\" else 12.0\n",
" dev_target, X, Y, A, V, HD = None, np.zeros(n), np.zeros(n), np.zeros(n), np.zeros(n), np.zeros(n)\n",
" for t in range(n):\n",
" inw = win[0] <= t < win[1]\n",
" tx, ty = plan[1] if kind == \"infiltration\" else plan[wi % len(plan)]\n",
" spd_cmd, alt_cmd = v0, alt0\n",
" if inw:\n",
" if kind == \"deviation\":\n",
" if dev_target is None:\n",
" r = math.radians(hdg + side)\n",
" dev_target = (x + 900 * math.sin(r), y + 900 * math.cos(r))\n",
" tx, ty = dev_target\n",
" elif kind == \"nfz_entry\":\n",
" tx, ty = NFZ[\"x\"], NFZ[\"y\"]\n",
" elif kind == \"hover\":\n",
" spd_cmd = 0.0\n",
" elif kind == \"speed_burst\":\n",
" spd_cmd = burst\n",
" elif kind == \"low_level\":\n",
" alt_cmd = low_alt\n",
" if kind in (\"rc_silent\", \"infiltration\"):\n",
" rc_arr[t] = rng_rc[t]\n",
" if inw and kind == \"loiter\":\n",
" hdg = (hdg + 9.0 + e_h[t] / wind) % 360\n",
" else:\n",
" d = _wrap(math.degrees(math.atan2(tx - x, ty - y)) - hdg)\n",
" hdg = (hdg + max(-max_rate, min(max_rate, d)) + e_h[t]) % 360\n",
" if t in turns:\n",
" hdg = (hdg + turn_amt[t]) % 360\n",
" v = abs(spd_cmd + e_v[t]) if spd_cmd > 0 else abs(e_v[t] / wind)\n",
" x += v * math.sin(math.radians(hdg)); y += v * math.cos(math.radians(hdg))\n",
" if kind != \"infiltration\" and math.hypot(x - tx, y - ty) < 40 and not (inw and kind in (\"deviation\", \"nfz_entry\")):\n",
" wi += 1\n",
" if inw and kind == \"dive\":\n",
" alt -= 9.0 + e_v[t]\n",
" else:\n",
" alt += max(-3.0, min(3.0, alt_cmd - alt)) + e_a[t]\n",
" alt -= gust[t] # ๋ํ ํ๊ฐ๊ธฐ๋ฅ(์ ์ ๋นํ์์๋ ๋ฐ์)\n",
" if kind == \"dive\" and t >= win[1]:\n",
" alt0 = min(alt0, alt)\n",
" alt = max(alt, 3.0)\n",
" X[t], Y[t], A[t], V[t], HD[t] = x, y, alt, v, hdg\n",
" gps_sd = {\"clear\": 2.0, \"night\": 2.0, \"windy\": 3.0, \"rain\": 4.5}[weather] # ์
์ฒํ์ผ์๋ก GPS ์ก์(๋ค์ค๊ฒฝ๋ก) ์ฆ๊ฐ\n",
" df = pd.DataFrame({\"t\": np.arange(n), \"x\": X + rng.normal(0, gps_sd, n), \"y\": Y + rng.normal(0, gps_sd, n),\n",
" \"alt\": A + rng.normal(0, 0.5, n), \"speed\": V + rng.normal(0, 0.2, n),\n",
" \"heading\": (HD + rng.normal(0, 1.5, n)) % 360, \"rc\": rc_arr.round(1),\n",
" \"sats\": rng.integers(10, 15, n), \"hdop\": rng.uniform(0.6, 1.2, n).round(2),\n",
" \"x_true\": X, \"y_true\": Y})\n",
" df[\"anomaly\"] = (df[\"t\"] >= win[0]) & (df[\"t\"] < win[1]) & (kind in SCENARIOS)\n",
" op = OPERATORS[int(rng.choice(3, p=OPERATOR_P[kind]))]\n",
" meta = {\"id\": fid, \"kind\": kind, \"weather\": weather, \"operator\": op, \"plan\": plan, \"window\": win if kind in SCENARIOS else None,\n",
" \"platform\": [\"commercial\", \"military\", \"unknown\"][int(rng.choice(3, p=PLATFORM_P[op]))],\n",
" \"firmware\": [\"stock\", \"custom\", \"unknown\"][int(rng.choice(3, p=FIRMWARE_P[op]))]}\n",
" return df, meta\n",
"\n",
"\n",
"def corrupt(df, rng, spoof=True, emi=True, drop=True, dup=True):\n",
" \"\"\"์ ์ฅ ์ก์ ์ฃผ์
: GPS ์คํธํ(์์น ์ ํยทํ๋ฅ), ์ ์๊ธฐ ๊ฐ์ญ(๊ณ ๋ ์คํ์ดํฌ), ์ผ์ ๋จ์ (๊ฒฐ์ธก), ์ค๋ณต ๊ธฐ๋ก.\"\"\"\n",
" d = df.copy()\n",
" d[\"spoof_true\"] = False\n",
" if spoof:\n",
" a = int(rng.integers(40, len(d) - 60)); L = int(rng.integers(25, 45))\n",
" th = rng.uniform(0, 2 * np.pi)\n",
" drift = rng.uniform(22, 28) * np.arange(L)[:, None] * np.array([np.cos(th), np.sin(th)])\n",
" jump = rng.uniform(250, 400) * np.array([np.cos(th + 1), np.sin(th + 1)])\n",
" d.loc[a:a + L - 1, [\"x\", \"y\"]] += drift + jump\n",
" d.loc[a:a + L - 1, \"spoof_true\"] = True\n",
" if emi:\n",
" idx = rng.choice(len(d), 6, replace=False)\n",
" d.loc[idx, \"alt\"] += rng.choice([-1, 1], 6) * rng.uniform(60, 150, 6)\n",
" if drop:\n",
" a = int(rng.integers(10, len(d) - 20))\n",
" d.loc[a:a + int(rng.integers(4, 9)), [\"x\", \"y\", \"alt\", \"speed\", \"heading\"]] = np.nan\n",
" d = d.drop(index=rng.choice(len(d), 5, replace=False))\n",
" if dup:\n",
" d = pd.concat([d, d.sample(5, random_state=int(rng.integers(1e6)))]).sort_values(\"t\", kind=\"stable\")\n",
" return d.reset_index(drop=True)\n",
"\n",
"\n",
"def make_dataset(n=200, seed=7, anomaly_ratio=0.5, scenarios=SCENARIOS):\n",
" rng = np.random.default_rng(seed)\n",
" out = []\n",
" for i in range(n):\n",
" if i % 10 < anomaly_ratio * 10:\n",
" kind = scenarios[(i // 10 * int(anomaly_ratio * 10) + i % 10) % len(scenarios)]\n",
" else:\n",
" kind = \"survey\" if i % 10 == 9 else \"normal\"\n",
" df, meta = make_flight(f\"FL{i:04d}\", kind, rng)\n",
" out.append((df, meta))\n",
" return out\n",
"# ---------------------------------------------------------------------------\n",
"\n",
"# ---- ์ ์ฒ๋ฆฌยท๊ท์น ํ์ง ๊ณตํต ํจ์ ----\n",
"SPOOF_THR = {\"clear\": 12.0, \"night\": 12.0, \"windy\": 15.0, \"rain\": 19.0} # ๊ธฐ์๋ณ GPS-์ถ์ธกํญ๋ฒ ๋ถ์ผ์น ํ์ฉ์น(m)\n",
"\n",
"\n",
"def clean(df, weather=\"clear\"):\n",
" \"\"\"์ ์ฒ๋ฆฌ: ์ค๋ณต ์ ๊ฑฐยท์ ๋ ฌ -> 1์ด ์ฌํ๋ณธํ -> ๊ฒฐ์ธก ๋ณด๊ฐ -> ๊ณ ๋ ์คํ์ดํฌ ์ ๊ฑฐ -> GPS ์คํธํ ๊ตฌ๊ฐ ์ถ์ธกํญ๋ฒ ๋ณด์ .\"\"\"\n",
" d = df.drop_duplicates(\"t\").sort_values(\"t\").set_index(\"t\")\n",
" d = d.reindex(np.arange(int(d.index.min()), int(d.index.max()) + 1))\n",
" n_missing = int(d[\"x\"].isna().sum())\n",
" num = [\"x\", \"y\", \"alt\", \"speed\", \"rc\", \"sats\", \"hdop\"]\n",
" d[num] = d[num].interpolate(limit_direction=\"both\")\n",
" hd = d[\"heading\"].dropna() # ๋ฐฉ์๋ 0/360 ๊ฒฝ๊ณ๋ฅผ ํ์ด์ ๋ณด๊ฐ\n",
" un = pd.Series(np.degrees(np.unwrap(np.radians(hd.to_numpy(float)))), index=hd.index)\n",
" d[\"heading\"] = un.reindex(d.index).interpolate(limit_direction=\"both\").to_numpy() % 360\n",
" med = d[\"alt\"].rolling(5, center=True, min_periods=1).median()\n",
" spike = (d[\"alt\"] - med).abs() > 25\n",
" d.loc[spike, \"alt\"] = med[spike]\n",
" x, y = d[\"x\"].to_numpy(float), d[\"y\"].to_numpy(float)\n",
" r = np.radians(d[\"heading\"].to_numpy(float)); v = d[\"speed\"].to_numpy(float)\n",
" dr = np.c_[v * np.sin(r), v * np.cos(r)] # ๋ณด๊ณ ์๋ยท๋ฐฉ์๋ก ๊ณ์ฐํ 1์ด ์ด๋๋(์ถ์ธกํญ๋ฒ)\n",
" xc, yc, flag = x.copy(), y.copy(), np.zeros(len(x), bool)\n",
" thr = SPOOF_THR.get(weather, 12.0)\n",
" ok_run = 0\n",
" for i in range(1, len(x)):\n",
" px, py = xc[i - 1] + dr[i, 0], yc[i - 1] + dr[i, 1] # ์ถ์ธกํญ๋ฒ ์์ ์์น\n",
" step_ok = np.hypot(x[i] - x[i - 1] - dr[i, 0], y[i] - y[i - 1] - dr[i, 1]) <= thr\n",
" ok_run = ok_run + 1 if step_ok else 0\n",
" if flag[i - 1]:\n",
" # ์คํธํ ๊ตฌ๊ฐ: GPS๊ฐ ์์ ์์น๋ก ๋์์ค๊ฑฐ๋, GPS ์ด๋๋์ด 5์ด ์ฐ์ ๋ฌผ๋ฆฌ์ ์ผ๋ก ๋ง์ผ๋ฉด GPS๋ก ๋ณต๊ท\n",
" if np.hypot(x[i] - px, y[i] - py) < thr * 2 or ok_run >= 5:\n",
" xc[i], yc[i] = x[i], y[i]\n",
" else:\n",
" xc[i], yc[i], flag[i] = px, py, True\n",
" elif not step_ok:\n",
" xc[i], yc[i], flag[i] = px, py, True # GPS ์ด๋๋์ด ๋ฌผ๋ฆฌ์ ์ผ๋ก ๋ง์ง ์์ -> ์คํธํ ์์ฌ\n",
" else:\n",
" xc[i], yc[i] = xc[i - 1] + x[i] - x[i - 1], yc[i - 1] + y[i] - y[i - 1]\n",
" d[\"x\"], d[\"y\"], d[\"spoof_flag\"] = xc, yc, flag\n",
" d = d.reset_index()\n",
" d.attrs.update({\"missing\": n_missing, \"spikes\": int(spike.sum()), \"spoof_pts\": int(flag.sum())})\n",
" return d\n",
"\n",
"\n",
"def xtrack(px, py, plan):\n",
" \"\"\"๊ณํ ๊ฒฝ๋ก(์จ์ดํฌ์ธํธ ํ๊ณก์ ) ๋๋น ์ดํ ๊ฑฐ๋ฆฌ(m).\"\"\"\n",
" P = np.asarray(plan, float)\n",
" segs = [(P[i], P[(i + 1) % len(P)]) for i in range(len(P) if len(P) > 2 else 1)]\n",
" best = np.full(len(px), np.inf)\n",
" for a, b in segs:\n",
" ab = b - a\n",
" u = np.clip(((px - a[0]) * ab[0] + (py - a[1]) * ab[1]) / max(ab @ ab, 1e-9), 0, 1)\n",
" best = np.minimum(best, np.hypot(px - a[0] - u * ab[0], py - a[1] - u * ab[1]))\n",
" return best\n",
"\n",
"\n",
"RULES = {\"turn_rate\": 45.0, \"hover_speed\": 1.5, \"hover_sec\": 20, \"dive_m\": 30.0, \"xtrack_m\": 250.0}\n",
"\n",
"\n",
"def rule_flags(d, plan, R=RULES):\n",
" \"\"\"๊ท์น ๊ธฐ๋ฐ ์ด์ ๋นํ ํ์ (๊ธฐ๋ก ๋จ์). ๊ธ์ ํยทํธ๋ฒ๋งยท๊ธ๊ฐํยท๊ฒฝ๋ก ์ดํยท๋น์ธ๊ฐ ๊ตฌ์ญ ์ง์
.\"\"\"\n",
" hr = np.abs(_wrap(np.diff(d[\"heading\"].to_numpy(float), prepend=d[\"heading\"].iloc[0])))\n",
" slow = (d[\"speed\"] < R[\"hover_speed\"]).to_numpy()\n",
" run = np.zeros(len(slow), int)\n",
" for i in range(len(slow)):\n",
" run[i] = run[i - 1] + 1 if slow[i] and i else int(slow[i])\n",
" hover = np.zeros(len(slow), bool)\n",
" for i in np.where(run >= R[\"hover_sec\"])[0]:\n",
" hover[i - R[\"hover_sec\"] + 1:i + 1] = True\n",
" alt = d[\"alt\"].to_numpy(float)\n",
" drop = alt - np.r_[alt[5:], np.full(5, alt[-1])]\n",
" return {\"๊ธ์ ํ\": hr > R[\"turn_rate\"], \"ํธ๋ฒ๋ง\": hover, \"๊ธ๊ฐํ\": drop > R[\"dive_m\"],\n",
" \"๊ฒฝ๋ก์ดํ\": xtrack(d[\"x\"].to_numpy(float), d[\"y\"].to_numpy(float), plan) > R[\"xtrack_m\"],\n",
" \"๋น์ธ๊ฐ๊ตฌ์ญ\": np.hypot(d[\"x\"] - NFZ[\"x\"], d[\"y\"] - NFZ[\"y\"]).to_numpy() < NFZ[\"r\"]}\n",
"\n",
"# ---- ๊ตฌ๊ฐ(30์ด ์ฐฝ) ํน์ง + ๋น์ง๋ ์ด์ ํ์ง ๊ณตํต ํจ์ ----\n",
"WFEAT = [\"alt_mean\", \"alt_min\", \"alt_std\", \"vz_min\", \"spd_mean\", \"spd_std\", \"spd_max\", \"turn_sum\", \"turn_max\",\n",
" \"straight\", \"rc_mean\", \"rc_zero\", \"nfz_min\", \"nfz_close\", \"xtrack_max\"]\n",
"\n",
"\n",
"def window_feats(d, plan, win=30, step=10):\n",
" \"\"\"30์ด ์ฐฝ์ 10์ด์ฉ ์ฎ๊ธฐ๋ฉฐ ๊ณ ๋ยท์๋ยท์ ํยท์ง์ง์ฑยท์กฐ์ข
์ ํธยท๋น์ธ๊ฐ ๊ตฌ์ญ ์ ๊ทผยท๊ฒฝ๋ก ์ดํ ํน์ง์ ๊ณ์ฐํ๋ค.\"\"\"\n",
" x, y, alt = d[\"x\"].to_numpy(float), d[\"y\"].to_numpy(float), d[\"alt\"].to_numpy(float)\n",
" spd, rc = d[\"speed\"].to_numpy(float), d[\"rc\"].to_numpy(float)\n",
" hr = np.abs(_wrap(np.diff(d[\"heading\"].to_numpy(float), prepend=d[\"heading\"].iloc[0])))\n",
" vz = np.diff(alt, prepend=alt[0])\n",
" nd = np.hypot(x - NFZ[\"x\"], y - NFZ[\"y\"])\n",
" xt = xtrack(x, y, plan)\n",
" step_len = np.hypot(np.diff(x, prepend=x[0]), np.diff(y, prepend=y[0]))\n",
" rows, starts = [], []\n",
" for s in range(0, len(d) - win + 1, step):\n",
" e = s + win\n",
" path = step_len[s + 1:e].sum()\n",
" rows.append([alt[s:e].mean(), alt[s:e].min(), alt[s:e].std(), vz[s:e].min(), spd[s:e].mean(), spd[s:e].std(),\n",
" spd[s:e].max(), hr[s:e].sum(), hr[s:e].max(), np.hypot(x[e - 1] - x[s], y[e - 1] - y[s]) / max(path, 1.0),\n",
" rc[s:e].mean(), (rc[s:e] < 5).mean(), nd[s:e].min(), (nd[s] - nd[e - 1]) / win, xt[s:e].max()])\n",
" starts.append(int(d[\"t\"].iloc[s]))\n",
" return np.array(rows), starts\n",
"\n",
"\n",
"class FlightAnomalyModel:\n",
" \"\"\"ํ์ ๋นํ๋ก๊ทธ(๋ผ๋ฒจ ์์)๋ก ํต์ ํจํด ๊ตฐ์ง(KMeans)๊ณผ ๊ณ ๋ฆฝ ์ฒ(IsolationForest)์ ํ์ตํ๊ณ ,\n",
" ๋ ์ ์๋ฅผ ํ์ต ๊ตฌ๊ฐ ๋ถ์์๋ก ๋๋ 1.0์ ๋์ผ๋ฉด 'ํต์ ํจํด์์ ๋ฒ์ด๋จ'์ผ๋ก ๋ณธ๋ค.\"\"\"\n",
"\n",
" def __init__(self, q=0.999, k=4):\n",
" self.q, self.k = q, k\n",
"\n",
" def fit(self, X):\n",
" self.sc = StandardScaler().fit(X)\n",
" Z = self.sc.transform(X)\n",
" self.iso = IsolationForest(n_estimators=150, random_state=0).fit(Z)\n",
" self.km = KMeans(self.k, n_init=5, random_state=0).fit(Z)\n",
" self.set_q(self.q, X)\n",
" return self\n",
"\n",
" def _raw(self, X):\n",
" Z = self.sc.transform(X)\n",
" return -self.iso.score_samples(Z), self.km.transform(Z).min(axis=1)\n",
"\n",
" def set_q(self, q, X):\n",
" a, b = self._raw(X)\n",
" self.q, self.t_if, self.t_km = q, np.quantile(a, q), np.quantile(b, q)\n",
"\n",
" def score(self, X):\n",
" a, b = self._raw(X)\n",
" return np.maximum(a / self.t_if, b / self.t_km)\n",
"\n",
"\n",
"AFEAT = [\"ml_score\", \"low_alt\", \"rc_weak\", \"nfz_min_km\", \"hover_s\", \"max_speed\", \"night\",\n",
" \"pf_commercial\", \"pf_military\", \"pf_unknown\", \"fw_stock\", \"fw_custom\", \"fw_unknown\"]\n",
"\n",
"\n",
"def attr_features(d, m, ml_score):\n",
" \"\"\"์ด์ฉ ์ฃผ์ฒด ์ถ์ ์ฉ ๋นํ ๋จ์ ํน์ง. ๊ธฐ์ฒดยทํ์จ์ด ์ ๋ณด๋ ์์ฉ ๊ธฐ์ฒด๊ฐ ๋ชจ๋ ์ฃผ์ฒด์ ์ฐ์ด๋ฏ๋ก ์ฝํ ๋จ์์ผ ๋ฟ์ด๋ค.\"\"\"\n",
" return [min(ml_score, 10.0), float((d[\"alt\"] < 60).mean()), float((d[\"rc\"] < 25).mean()),\n",
" float(np.hypot(d[\"x\"] - NFZ[\"x\"], d[\"y\"] - NFZ[\"y\"]).min() / 1000), float((d[\"speed\"] < 1.5).sum()),\n",
" float(d[\"speed\"].max()), float(m[\"weather\"] == \"night\"),\n",
" *[float(m[\"platform\"] == p) for p in (\"commercial\", \"military\", \"unknown\")],\n",
" *[float(m[\"firmware\"] == f) for f in (\"stock\", \"custom\", \"unknown\")]]\n",
"\n",
"REQUIRED = [\"t\", \"x\", \"y\", \"alt\", \"speed\", \"heading\", \"rc\"]\n",
"\n",
"\n",
"def process(batch, model, budget_ms, prof):\n",
" \"\"\"๋ฐฐ์น ์ฒ๋ฆฌ. ์ฐฝ ํน์ง์ ๋ชจ์ ML ์ ์๋ฅผ ํ ๋ฒ์ ๊ณ์ฐ(๋ณ๋ชฉ ์ต์ ํ)ํ๊ณ ,\n",
" ์์ฐ ์ด๊ณผ๊ฐ ์์๋๋ฉด ML ๋จ๊ณ๋ฅผ ๊ฑด๋๋ฐ๋ ๊ธฐ๋ฅ ์ถ์ ๋ชจ๋(๊ท์น๋ง)๋ก ์ ํํ๋ค.\"\"\"\n",
" done, quarantine, feats = 0, [], []\n",
" t_start = time.perf_counter()\n",
" for df, m in batch:\n",
" try:\n",
" if df.empty or any(c not in df for c in REQUIRED) or not np.issubdtype(df[\"alt\"].dtype, np.number):\n",
" raise ValueError(\"ํ์ ์ค๋ฅ\")\n",
" t0 = time.perf_counter(); d = clean(df, m[\"weather\"]); prof[\"์ ์ฒ๋ฆฌ\"] += time.perf_counter() - t0\n",
" t0 = time.perf_counter(); rule_flags(d, m[\"plan\"]); prof[\"๊ท์น\"] += time.perf_counter() - t0\n",
" t0 = time.perf_counter(); feats.append((d, m)); prof[\"ํน์ง\"] += time.perf_counter() - t0\n",
" done += 1\n",
" except Exception as e:\n",
" quarantine.append((m[\"id\"], str(e)[:20])) # ๊ฒฉ๋ฆฌ ๋ณด๊ด(์ ์ค ์๋): ์๋ณธ์ ๋ฐ๋ก ๋ณด๊ดํ๊ณ ์ฌ์ ๊ธฐ๋ก\n",
" degraded = (time.perf_counter() - t_start) * 1000 > budget_ms * 0.6\n",
" if not degraded: # ๋ฐ๋ฆผ์ด ์์ ๋๋ง ML ์ํ(๋ฐ๋ฆฌ๋ฉด ๊ท์น ๊ฒฐ๊ณผ๋ง ๋จผ์ ๋ด๊ณ ML์ ์ฌ์ฒ๋ฆฌ ๋๊ธฐ)\n",
" t0 = time.perf_counter(); X = np.vstack([window_feats(d, m[\"plan\"])[0] for d, m in feats]) if feats else None\n",
" prof[\"ํน์ง\"] += time.perf_counter() - t0\n",
" t0 = time.perf_counter()\n",
" if X is not None:\n",
" model.score(X)\n",
" prof[\"ML\"] += time.perf_counter() - t0\n",
" return done, quarantine, degraded\n",
"\n",
"\n",
"rng = np.random.default_rng(8)\n",
"train = make_dataset(60, seed=21, anomaly_ratio=0.0)\n",
"model = FlightAnomalyModel().fit(np.vstack([window_feats(clean(df, m[\"weather\"]), m[\"plan\"])[0] for df, m in train]))\n",
"pool = make_dataset(150, seed=11) # ๋ก๊ทธ ์๋ณธ ํ(๋ณต์ ํด ๋๋ ์ฃผ์
)\n",
"\n",
"\n",
"def workload(n, broken=0.0, noisy=0.0):\n",
" out = []\n",
" for i in range(n):\n",
" df, m = pool[i % len(pool)]\n",
" m = {**m, \"id\": f\"B{i:05d}\"}\n",
" if rng.random() < noisy:\n",
" df = corrupt(df, rng)\n",
" if rng.random() < broken:\n",
" df = df.drop(columns=[\"alt\"]) if i % 2 else df.iloc[0:0]\n",
" out.append((df, m))\n",
" return out\n",
"\n",
"\n",
"cases = [(\"ํ์ 40๊ฑด(๊ธฐ์ค)\", 40, 0, 0, 1e9), (\"500% ์ฆ๊ฐ (240๊ฑด)\", 240, 0, 0, 1e9),\n",
" (\"240๊ฑด + ์์ยท์ก์ ๋ก๊ทธ\", 240, 0.05, 0.3, 1e9), (\"240๊ฑด + ์ฒ๋ฆฌ ์์ฐ ๋ถ์กฑ\", 240, 0, 0, 1200)]\n",
"rows, prof = [], {\"์ ์ฒ๋ฆฌ\": 0.0, \"๊ท์น\": 0.0, \"ํน์ง\": 0.0, \"ML\": 0.0}\n",
"mem_trace = []\n",
"for name, n, br, nz, budget in cases:\n",
" work = workload(n, br, nz)\n",
" q = deque(work) # ์์ ๋๊ธฐ์ด(์ ์ค ์์ด ๋ชจ๋ ์ ์ฌ)\n",
" received, done, quar, degraded, down = len(q), 0, [], False, 0\n",
" t0 = time.perf_counter()\n",
" try:\n",
" while q:\n",
" chunk = [q.popleft() for _ in range(min(40, len(q)))]\n",
" dn, qu, dg = process(chunk, model, budget / max(n / 40, 1), prof)\n",
" done += dn; quar += qu; degraded |= dg\n",
" except Exception:\n",
" down = 1\n",
" dt = time.perf_counter() - t0\n",
" recs = sum(len(df) for df, _ in work)\n",
" rows.append({\"์ํฉ\": name, \"์์ \": received, \"์ฒ๋ฆฌ\": done, \"๊ฒฉ๋ฆฌ\": len(quar), \"์ ์ค\": received - done - len(quar), \"๋ค์ด\": down,\n",
" \"์ฒ๋ฆฌ๋(๊ฑด/์ด)\": int(recs / dt), \"์ฑ๋ฅ์ ํ ๋ชจ๋\": \"ML ์ง์ฐ์ฒ๋ฆฌ\" if degraded else \"-\"})\n",
"df = pd.DataFrame(rows)\n",
"print(df.to_string(index=False))\n",
"lost = df[\"์ ์ค\"].sum() / df[\"์์ \"].sum()\n",
"print(f\"\\n์์คํ
๋ค์ด {df['๋ค์ด'].sum()}๊ฑด, ๋ฐ์ดํฐ ์ ์ค๋ฅ {lost:.1%} (๋ชฉํ 0%), ๊ฒฉ๋ฆฌ ๋ก๊ทธ๋ ์๋ณธ ๋ณด๊ด ํ ์ฌ์ ์ ํจ๊ป ์ฌ๊ฒํ \")\n",
"# ๋ฉ๋ชจ๋ฆฌ ์์ ์ฑ: ๊ฐ์ ๋ฐฐ์น(20๊ฑด)๋ฅผ 5ํ ๋ฐ๋ณต ์ฒ๋ฆฌํ๋ฉฐ ์ถ์ ๋ฉ๋ชจ๋ฆฌ ๋ณํ ํ์ธ\n",
"tracemalloc.start()\n",
"for _ in range(5):\n",
" process(workload(20), model, 1e9, {\"์ ์ฒ๋ฆฌ\": 0, \"๊ท์น\": 0, \"ํน์ง\": 0, \"ML\": 0})\n",
" mem_trace.append(tracemalloc.get_traced_memory()[0] / 1e6)\n",
"tracemalloc.stop()\n",
"print(\"๋ฉ๋ชจ๋ฆฌ(๋์ผ ๋ฐฐ์น 5ํ ๋ฐ๋ณต, MB):\", [round(m, 1) for m in mem_trace], \"-> ์ฆ๊ฐ ํญ\", round(mem_trace[-1] - mem_trace[0], 1), \"MB (๋์ ์์ ํ๋จ ๊ธฐ์ค 5MB ์ด๋ด)\")\n",
"tot = sum(prof.values())\n",
"print(\"๋ณ๋ชฉ ๊ตฌ๊ฐ(๋์ ์ฒ๋ฆฌ์๊ฐ ๋น์จ):\", {k: f\"{v / tot:.0%}\" for k, v in sorted(prof.items(), key=lambda x: -x[1])})\n",
"thr = df.loc[1, \"์ฒ๋ฆฌ๋(๊ฑด/์ด)\"]\n",
"print(f\"ํ์ฐ: 1๋ง ๋นํ์๊ฐ(3,600๋ง ๊ฑด) ์ฒ๋ฆฌ ์์ {36e6 / thr / 3600:.1f}์๊ฐ(๋จ์ผ ์ฝ์ด) -> ์ ์ฒ๋ฆฌ ๋ณ๋ ฌํยท๋น๋๊ธฐ ๋๊ธฐ์ด๋ก ๋จ์ถ ํ์\")\n",
"\n",
"plt.figure(figsize=(8, 4))\n",
"plt.bar(range(len(df)), df[\"์ฒ๋ฆฌ๋(๊ฑด/์ด)\"], color=[\"tab:red\" if m != \"-\" else \"tab:blue\" for m in df[\"์ฑ๋ฅ์ ํ ๋ชจ๋\"]])\n",
"plt.xticks(range(len(df)), [f\"case{i + 1}\" for i in range(len(df))]); plt.ylabel(\"records / s\")\n",
"plt.title(\"Batch stress test throughput (red = degraded mode)\"); plt.show()\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"๐ก [์ฐธ๊ณ ] ๋ฏธ์
08 ์์ ์คํ ๊ฒฐ๊ณผ (Output)
\n",
"\n",
"```text\n",
" ์ํฉ ์์ ์ฒ๋ฆฌ ๊ฒฉ๋ฆฌ ์ ์ค ๋ค์ด ์ฒ๋ฆฌ๋(๊ฑด/์ด) ์ฑ๋ฅ์ ํ ๋ชจ๋\n",
" ํ์ 40๊ฑด(๊ธฐ์ค) 40 40 0 0 0 23161 -\n",
" 500% ์ฆ๊ฐ (240๊ฑด) 240 240 0 0 0 21576 -\n",
"240๊ฑด + ์์ยท์ก์ ๋ก๊ทธ 240 229 11 0 0 22027 -\n",
"240๊ฑด + ์ฒ๋ฆฌ ์์ฐ ๋ถ์กฑ 240 240 0 0 0 36834 ML ์ง์ฐ์ฒ๋ฆฌ\n",
"\n",
"์์คํ
๋ค์ด 0๊ฑด, ๋ฐ์ดํฐ ์ ์ค๋ฅ 0.0% (๋ชฉํ 0%), ๊ฒฉ๋ฆฌ ๋ก๊ทธ๋ ์๋ณธ ๋ณด๊ด ํ ์ฌ์ ์ ํจ๊ป ์ฌ๊ฒํ \n",
"๋ฉ๋ชจ๋ฆฌ(๋์ผ ๋ฐฐ์น 5ํ ๋ฐ๋ณต, MB): [0.1, 0.2, 0.2, 0.3, 0.4] -> ์ฆ๊ฐ ํญ 0.3 MB (๋์ ์์ ํ๋จ ๊ธฐ์ค 5MB ์ด๋ด)\n",
"๋ณ๋ชฉ ๊ตฌ๊ฐ(๋์ ์ฒ๋ฆฌ์๊ฐ ๋น์จ): {'์ ์ฒ๋ฆฌ': '58%', 'ํน์ง': '26%', '๊ท์น': '12%', 'ML': '4%'}\n",
"ํ์ฐ: 1๋ง ๋นํ์๊ฐ(3,600๋ง ๊ฑด) ์ฒ๋ฆฌ ์์ 0.5์๊ฐ(๋จ์ผ ์ฝ์ด) -> ์ ์ฒ๋ฆฌ ๋ณ๋ ฌํยท๋น๋๊ธฐ ๋๊ธฐ์ด๋ก ๋จ์ถ ํ์\n",
"```\n",
" \n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## ๐ฏ ๋ฏธ์
09. AAR ๊ธฐ๋ฐ ๋นํ ์ด์ ์คํยท๋ฏธํ ์ฌ๋ก ๋ถ์ ๋ฐ ์ด์ ํ์ง ์๊ณ์น ์ต์ ํ\n",
"**์์์๊ฐ**: ์ฝ 3.0์๊ฐ\n",
"\n",
"### ๐ ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ์ํ ์ง์นจ\n",
"#### ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ๋น์์ฑ\n",
"\n",
"AAR ์ฌํ๊ฒํ ๋ฅผ ํตํด ๋นํ๋ก๊ทธ ์ด์์น ์คํ ์ฌ๋ก๋ฅผ ๋ถ์ํ๊ณ ์ ์ ๋นํ๊ณผ ์ ๋ ๋นํ์ ๊ฒฝ๊ณ ํ๋ผ๋ฏธํฐ๋ฅผ ์ฌ๋ณด์ ํ๋ค. ์ค์ ์ ์ ํ์ฅ์์๋ ๋จ์ ํ์ง์ ๊ทธ์น์ง ์๊ณ ์ ์ํ ์ํ ํ๋จ๊ณผ ์ ๋ขฐ๋ ๋์ ์์ฌ๊ฒฐ์ ์ด ํ์์ ์ด๋ฏ๋ก, **AAR ์ฌํ๊ฒํ ๋ก ๋นํ ์ด์ ์คํยท๋ฏธํ ์ฌ๋ก์ ์์ธ์ ๋ถ์ํ๊ณ ์ ์ ๋นํ๊ณผ ์์ฌ ๋นํ์ ๊ฒฝ๊ณ ํ๋ผ๋ฏธํฐ๋ฅผ ์ฌ๋ณด์ ํ๋ค.** ๊ณผ์
์ ์์ํ์ฌ ์คํ์ ๋ฐฉ์งํ๊ณ ์์ ๋์๋ ฅ์ ๊ทน๋ํํด์ผ ํฉ๋๋ค.\n",
"\n",
"์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ณธ ๋ฏธ์
์์๋ **'๋์น์ง ์๊ธฐ'๋ฅผ ์ฐ์ ํ ์ด๊ธฐ ์ค์ ์ผ๋ก ๋ชจ์ ์ด์ฉํ ๋ค ์คํ์ ์์ธ(์ธก๋ Uํด, ๊ฐํ ๋ํ ํ๊ฐ, ๋๋ฌธ ์ ์ ํจํด)๋ณ๋ก, ๋ฏธํ์ ์๋๋ฆฌ์ค๋ณ๋ก ๋ถ๋ฅํ AAR ์ผ์ง ์์ฑํ์ฌ ์์ธ์ ๋ถ์ํ๋** ์ค์ต์ ์ฒด๊ณ์ ์ผ๋ก ์ํํฉ๋๋ค.\n",
"\n",
"ํ์ต์๋ ์ค์ต๊ฐ์ด๋์ ์ ์๋ ์ด 4๊ฐ ์ธ๋ถ ํ๋(๊ธฐ๋ณธ ๊ธฐ๋ฅ ๊ตฌํ โ ๊ฒฐ๊ณผ ์งํ ๊ฒ์ฆ โ ํ๋ผ๋ฏธํฐ ํ๋ ๋ฐ ๋์ ๊ณผ์ )์ ์์ฐจ์ ์ผ๋ก ์งํํ๋ฉฐ ํต์ฌ ํ์ ๊ธฐ์ค์ ๋ฌ์ฑํฉ๋๋ค.\n",
"\n",
"**์ด๋ฒ ๊ฒฐ๊ณผ๋ฌผ:** `docs/isr03_aar_report.md ๋ฐ configs/isr03_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(์ฌํ๊ฒํ ) ์ผ์ง ํ์์ผ๋ก ๋ชจ์ ์ด์ฉ ์ค ๋ฐ์ํ ๋นํ ์ด์ ์คํยท๋ฏธํ ์ฌ๋ก๋ฅผ ์์ธ๋ณ๋ก ๋ถ๋ฅํ๊ณ , ๊ธ์ ํยท๊ธ๊ฐํ ๊ท์น ์๊ณ๊ฐ๊ณผ ML ์ด์ ์ ์ ์๊ณ๊ฐ์ ๊ฒฉ์ ํ์์ผ๋ก ์ฌ๋ณด์ ํด ์ ์ ๋นํ๊ณผ ์ ๋ ์์ฌ ๋นํ์ ๊ฒฝ๊ณ๋ฅผ ์กฐ์ ํ ๋ค ํ๊ท ์ํ์ผ๋ก ๊ฐ์ ํจ๊ณผ์ ์ฌ๋ฐ ์ฌ๋ถ๋ฅผ ํ์ธํ๋ค.\"\"\"\n",
"import math\n",
"import numpy as np\n",
"import pandas as pd\n",
"import matplotlib.pyplot as plt\n",
"from sklearn.cluster import KMeans\n",
"from sklearn.ensemble import IsolationForest\n",
"from sklearn.preprocessing import StandardScaler\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# [๊ต์ก์ฉ๊ฐ์์๋ฃ] ํฉ์ฑ ๋๋ก ๋นํ๋ก๊ทธ ์์ฑ๊ธฐ (์ค์ ์ขํยท๊ธฐ์ฒดยท๋ถ๋ ์ ๋ณด ์์)\n",
"# ๊ฐ์ ๊ตฌ์ญ(์์ ๊ธฐ์ค ๋ยท๋ถ ๋ฐฉํฅ m ์ขํ)์์ 1์ด ๊ฐ๊ฒฉ ๋นํ๋ก๊ทธ์ ์ ๋ต(์ด์ ๊ตฌ๊ฐยท์ด์ฉ ์ฃผ์ฒด)์ ๋ง๋ ๋ค.\n",
"# ---------------------------------------------------------------------------\n",
"NFZ = {\"x\": 2500.0, \"y\": 2500.0, \"r\": 700.0} # ๊ฐ์ ๋น์ธ๊ฐ ๋นํ๊ตฌ์ญ\n",
"WATCH = {\"x\": -2000.0, \"y\": 1500.0, \"r\": 600.0} # ๊ฐ์ ๊ด์ฌ ๊ตฌ์ญ\n",
"SCENARIOS = [\"sharp_turn\", \"hover\", \"dive\", \"deviation\", \"nfz_entry\",\n",
" \"low_level\", \"rc_silent\", \"infiltration\", \"loiter\", \"speed_burst\"]\n",
"OPERATORS = [\"friendly\", \"civil\", \"hostile\"]\n",
"# ์๋๋ฆฌ์ค๋ณ ์ด์ฉ ์ฃผ์ฒด ๋ถํฌ: ์ผ๋ถ๋ฌ ํฌ๊ฒ ๊ฒน์น๊ฒ ์ค๊ณํ๋ค(๋นํ ํน์ฑ๋ง์ผ๋ก ์ด์ฉ ์ฃผ์ฒด๋ฅผ ๋จ์ ํ ์ ์๋ค).\n",
"OPERATOR_P = {\"normal\": (.45, .47, .08), \"survey\": (.15, .82, .03), \"sharp_turn\": (.40, .35, .25),\n",
" \"hover\": (.25, .45, .30), \"dive\": (.25, .25, .50), \"deviation\": (.30, .40, .30),\n",
" \"nfz_entry\": (.10, .50, .40), \"low_level\": (.35, .15, .50), \"rc_silent\": (.30, .15, .55),\n",
" \"infiltration\": (.12, .08, .80), \"loiter\": (.30, .20, .50), \"speed_burst\": (.30, .20, .50)}\n",
"PLATFORM_P = {\"friendly\": (.40, .55, .05), \"civil\": (.88, .02, .10), \"hostile\": (.55, .30, .15)} # commercial/military/unknown\n",
"FIRMWARE_P = {\"friendly\": (.30, .65, .05), \"civil\": (.85, .10, .05), \"hostile\": (.20, .50, .30)} # stock/custom/unknown\n",
"\n",
"\n",
"def _wrap(a):\n",
" return (a + 180.0) % 360.0 - 180.0\n",
"\n",
"\n",
"def _bearing(p, q):\n",
" return math.degrees(math.atan2(q[0] - p[0], q[1] - p[1])) % 360.0\n",
"\n",
"\n",
"def _away_from_nfz(rng, min_d):\n",
" while True:\n",
" c = rng.uniform(-3800, 3800, 2)\n",
" if np.hypot(c[0] - NFZ[\"x\"], c[1] - NFZ[\"y\"]) > min_d:\n",
" return c\n",
"\n",
"\n",
"def make_flight(fid, kind, rng, n=300, weather=None):\n",
" \"\"\"๋นํ 1ํ = (๋ก๊ทธ DataFrame, ๋ฉํ dict). kind: normal / survey / SCENARIOS ์ค ํ๋\"\"\"\n",
" weather = weather or str(rng.choice([\"clear\", \"night\", \"windy\", \"rain\"], p=[.4, .25, .2, .15]))\n",
" wind = {\"clear\": 1.0, \"night\": 1.0, \"windy\": 2.5, \"rain\": 1.8}[weather]\n",
" v0, alt0 = rng.uniform(11, 17), rng.uniform(70, 150)\n",
" nfz_c = np.array([NFZ[\"x\"], NFZ[\"y\"]])\n",
" if kind == \"infiltration\": # ์๊ฑฐ๋ฆฌ์์ ๋น์ธ๊ฐ ๊ตฌ์ญ ๋ฐฉํฅ์ผ๋ก ์ ๊ณ ๋ ์ง์ง\n",
" ang = rng.uniform(0, 2 * np.pi)\n",
" start = nfz_c + 4000 * np.array([np.cos(ang), np.sin(ang)])\n",
" plan = np.array([start, nfz_c])\n",
" v0, alt0 = rng.uniform(9, 11), rng.uniform(25, 35)\n",
" else:\n",
" if kind == \"nfz_entry\": # ๋น์ธ๊ฐ ๊ตฌ์ญ 1.6km ๋ฐ์์ ์์ฐฐํ๋ค๊ฐ ์ง์
\n",
" ang = rng.uniform(0, 2 * np.pi)\n",
" c = nfz_c + 1600 * np.array([np.cos(ang), np.sin(ang)])\n",
" else:\n",
" c = _away_from_nfz(rng, 2600)\n",
" if kind == \"survey\": # ๋ฏผ๊ฐ ์ธก๋ํ ์๋ณต(์ง๊ทธ์ฌ๊ทธ) ๊ฒฝ๋ก\n",
" w, h = rng.uniform(500, 700), 120.0\n",
" plan = np.array([c + [(-w if i % 4 in (0, 3) else w), i // 2 * h - 360] for i in range(8)])\n",
" else: # ์ฌ๊ฐ ์์ฐฐ ๊ฒฝ๋ก(์จ์ดํฌ์ธํธ 4๊ฐ)\n",
" a, b, th = rng.uniform(300, 600), rng.uniform(200, 400), rng.uniform(0, np.pi)\n",
" R = np.array([[np.cos(th), -np.sin(th)], [np.sin(th), np.cos(th)]])\n",
" plan = c + np.array([[-a, -b], [a, -b], [a, b], [-a, b]]) @ R.T\n",
" s0 = int(rng.integers(80, 160))\n",
" span = {\"sharp_turn\": 30, \"hover\": 60, \"dive\": 8, \"deviation\": 70, \"nfz_entry\": 120, \"low_level\": 120,\n",
" \"rc_silent\": 120, \"infiltration\": n - 20, \"loiter\": 90, \"speed_burst\": 25}.get(kind, 0)\n",
" if kind == \"infiltration\":\n",
" s0 = 20\n",
" win = (s0, s0 + span)\n",
" turns = set(rng.choice(np.arange(s0, s0 + 30), 3, replace=False).tolist()) if kind == \"sharp_turn\" else set()\n",
" # ๋์๋ฅผ ๋ฏธ๋ฆฌ ํ ๋ฒ์ ๋ฝ์ ๋๊ณ (์๋ ํฅ์) 1์ด ๋จ์๋ก ๊ธฐ์ฒด ์ด๋์ ์ ๋ถํ๋ค.\n",
" e_h, e_v, e_a = rng.normal(0, 0.8 * wind, n), rng.normal(0, 0.3 * wind, n), rng.normal(0, 0.4 * wind, n)\n",
" rc_arr = np.clip(rng.normal(rng.uniform(60, 92), 6, n), 0, 100)\n",
" gust = (rng.random(n) < (0.004 if weather == \"windy\" else 0.0)) * rng.uniform(20, 35, n)\n",
" burst, turn_amt = rng.uniform(23, 28), rng.choice([-1, 1], n) * rng.uniform(100, 160, n)\n",
" side = float(rng.choice([-90, 90]))\n",
" low_alt, rng_rc = rng.uniform(35, 55), rng.uniform(0, 20, n) # ์ ๊ณ ๋ ๋ชฉํ, ์กฐ์ข
์ ํธ ๋์ ์ ์์ฌ ์์ ๊ฐ\n",
" x, y, wi = float(plan[0][0]), float(plan[0][1]), 1\n",
" hdg, alt = _bearing((x, y), plan[1]), alt0\n",
" max_rate = 40.0 if kind == \"survey\" else 12.0\n",
" dev_target, X, Y, A, V, HD = None, np.zeros(n), np.zeros(n), np.zeros(n), np.zeros(n), np.zeros(n)\n",
" for t in range(n):\n",
" inw = win[0] <= t < win[1]\n",
" tx, ty = plan[1] if kind == \"infiltration\" else plan[wi % len(plan)]\n",
" spd_cmd, alt_cmd = v0, alt0\n",
" if inw:\n",
" if kind == \"deviation\":\n",
" if dev_target is None:\n",
" r = math.radians(hdg + side)\n",
" dev_target = (x + 900 * math.sin(r), y + 900 * math.cos(r))\n",
" tx, ty = dev_target\n",
" elif kind == \"nfz_entry\":\n",
" tx, ty = NFZ[\"x\"], NFZ[\"y\"]\n",
" elif kind == \"hover\":\n",
" spd_cmd = 0.0\n",
" elif kind == \"speed_burst\":\n",
" spd_cmd = burst\n",
" elif kind == \"low_level\":\n",
" alt_cmd = low_alt\n",
" if kind in (\"rc_silent\", \"infiltration\"):\n",
" rc_arr[t] = rng_rc[t]\n",
" if inw and kind == \"loiter\":\n",
" hdg = (hdg + 9.0 + e_h[t] / wind) % 360\n",
" else:\n",
" d = _wrap(math.degrees(math.atan2(tx - x, ty - y)) - hdg)\n",
" hdg = (hdg + max(-max_rate, min(max_rate, d)) + e_h[t]) % 360\n",
" if t in turns:\n",
" hdg = (hdg + turn_amt[t]) % 360\n",
" v = abs(spd_cmd + e_v[t]) if spd_cmd > 0 else abs(e_v[t] / wind)\n",
" x += v * math.sin(math.radians(hdg)); y += v * math.cos(math.radians(hdg))\n",
" if kind != \"infiltration\" and math.hypot(x - tx, y - ty) < 40 and not (inw and kind in (\"deviation\", \"nfz_entry\")):\n",
" wi += 1\n",
" if inw and kind == \"dive\":\n",
" alt -= 9.0 + e_v[t]\n",
" else:\n",
" alt += max(-3.0, min(3.0, alt_cmd - alt)) + e_a[t]\n",
" alt -= gust[t] # ๋ํ ํ๊ฐ๊ธฐ๋ฅ(์ ์ ๋นํ์์๋ ๋ฐ์)\n",
" if kind == \"dive\" and t >= win[1]:\n",
" alt0 = min(alt0, alt)\n",
" alt = max(alt, 3.0)\n",
" X[t], Y[t], A[t], V[t], HD[t] = x, y, alt, v, hdg\n",
" gps_sd = {\"clear\": 2.0, \"night\": 2.0, \"windy\": 3.0, \"rain\": 4.5}[weather] # ์
์ฒํ์ผ์๋ก GPS ์ก์(๋ค์ค๊ฒฝ๋ก) ์ฆ๊ฐ\n",
" df = pd.DataFrame({\"t\": np.arange(n), \"x\": X + rng.normal(0, gps_sd, n), \"y\": Y + rng.normal(0, gps_sd, n),\n",
" \"alt\": A + rng.normal(0, 0.5, n), \"speed\": V + rng.normal(0, 0.2, n),\n",
" \"heading\": (HD + rng.normal(0, 1.5, n)) % 360, \"rc\": rc_arr.round(1),\n",
" \"sats\": rng.integers(10, 15, n), \"hdop\": rng.uniform(0.6, 1.2, n).round(2),\n",
" \"x_true\": X, \"y_true\": Y})\n",
" df[\"anomaly\"] = (df[\"t\"] >= win[0]) & (df[\"t\"] < win[1]) & (kind in SCENARIOS)\n",
" op = OPERATORS[int(rng.choice(3, p=OPERATOR_P[kind]))]\n",
" meta = {\"id\": fid, \"kind\": kind, \"weather\": weather, \"operator\": op, \"plan\": plan, \"window\": win if kind in SCENARIOS else None,\n",
" \"platform\": [\"commercial\", \"military\", \"unknown\"][int(rng.choice(3, p=PLATFORM_P[op]))],\n",
" \"firmware\": [\"stock\", \"custom\", \"unknown\"][int(rng.choice(3, p=FIRMWARE_P[op]))]}\n",
" return df, meta\n",
"\n",
"\n",
"def corrupt(df, rng, spoof=True, emi=True, drop=True, dup=True):\n",
" \"\"\"์ ์ฅ ์ก์ ์ฃผ์
: GPS ์คํธํ(์์น ์ ํยทํ๋ฅ), ์ ์๊ธฐ ๊ฐ์ญ(๊ณ ๋ ์คํ์ดํฌ), ์ผ์ ๋จ์ (๊ฒฐ์ธก), ์ค๋ณต ๊ธฐ๋ก.\"\"\"\n",
" d = df.copy()\n",
" d[\"spoof_true\"] = False\n",
" if spoof:\n",
" a = int(rng.integers(40, len(d) - 60)); L = int(rng.integers(25, 45))\n",
" th = rng.uniform(0, 2 * np.pi)\n",
" drift = rng.uniform(22, 28) * np.arange(L)[:, None] * np.array([np.cos(th), np.sin(th)])\n",
" jump = rng.uniform(250, 400) * np.array([np.cos(th + 1), np.sin(th + 1)])\n",
" d.loc[a:a + L - 1, [\"x\", \"y\"]] += drift + jump\n",
" d.loc[a:a + L - 1, \"spoof_true\"] = True\n",
" if emi:\n",
" idx = rng.choice(len(d), 6, replace=False)\n",
" d.loc[idx, \"alt\"] += rng.choice([-1, 1], 6) * rng.uniform(60, 150, 6)\n",
" if drop:\n",
" a = int(rng.integers(10, len(d) - 20))\n",
" d.loc[a:a + int(rng.integers(4, 9)), [\"x\", \"y\", \"alt\", \"speed\", \"heading\"]] = np.nan\n",
" d = d.drop(index=rng.choice(len(d), 5, replace=False))\n",
" if dup:\n",
" d = pd.concat([d, d.sample(5, random_state=int(rng.integers(1e6)))]).sort_values(\"t\", kind=\"stable\")\n",
" return d.reset_index(drop=True)\n",
"\n",
"\n",
"def make_dataset(n=200, seed=7, anomaly_ratio=0.5, scenarios=SCENARIOS):\n",
" rng = np.random.default_rng(seed)\n",
" out = []\n",
" for i in range(n):\n",
" if i % 10 < anomaly_ratio * 10:\n",
" kind = scenarios[(i // 10 * int(anomaly_ratio * 10) + i % 10) % len(scenarios)]\n",
" else:\n",
" kind = \"survey\" if i % 10 == 9 else \"normal\"\n",
" df, meta = make_flight(f\"FL{i:04d}\", kind, rng)\n",
" out.append((df, meta))\n",
" return out\n",
"# ---------------------------------------------------------------------------\n",
"\n",
"# ---- ์ ์ฒ๋ฆฌยท๊ท์น ํ์ง ๊ณตํต ํจ์ ----\n",
"SPOOF_THR = {\"clear\": 12.0, \"night\": 12.0, \"windy\": 15.0, \"rain\": 19.0} # ๊ธฐ์๋ณ GPS-์ถ์ธกํญ๋ฒ ๋ถ์ผ์น ํ์ฉ์น(m)\n",
"\n",
"\n",
"def clean(df, weather=\"clear\"):\n",
" \"\"\"์ ์ฒ๋ฆฌ: ์ค๋ณต ์ ๊ฑฐยท์ ๋ ฌ -> 1์ด ์ฌํ๋ณธํ -> ๊ฒฐ์ธก ๋ณด๊ฐ -> ๊ณ ๋ ์คํ์ดํฌ ์ ๊ฑฐ -> GPS ์คํธํ ๊ตฌ๊ฐ ์ถ์ธกํญ๋ฒ ๋ณด์ .\"\"\"\n",
" d = df.drop_duplicates(\"t\").sort_values(\"t\").set_index(\"t\")\n",
" d = d.reindex(np.arange(int(d.index.min()), int(d.index.max()) + 1))\n",
" n_missing = int(d[\"x\"].isna().sum())\n",
" num = [\"x\", \"y\", \"alt\", \"speed\", \"rc\", \"sats\", \"hdop\"]\n",
" d[num] = d[num].interpolate(limit_direction=\"both\")\n",
" hd = d[\"heading\"].dropna() # ๋ฐฉ์๋ 0/360 ๊ฒฝ๊ณ๋ฅผ ํ์ด์ ๋ณด๊ฐ\n",
" un = pd.Series(np.degrees(np.unwrap(np.radians(hd.to_numpy(float)))), index=hd.index)\n",
" d[\"heading\"] = un.reindex(d.index).interpolate(limit_direction=\"both\").to_numpy() % 360\n",
" med = d[\"alt\"].rolling(5, center=True, min_periods=1).median()\n",
" spike = (d[\"alt\"] - med).abs() > 25\n",
" d.loc[spike, \"alt\"] = med[spike]\n",
" x, y = d[\"x\"].to_numpy(float), d[\"y\"].to_numpy(float)\n",
" r = np.radians(d[\"heading\"].to_numpy(float)); v = d[\"speed\"].to_numpy(float)\n",
" dr = np.c_[v * np.sin(r), v * np.cos(r)] # ๋ณด๊ณ ์๋ยท๋ฐฉ์๋ก ๊ณ์ฐํ 1์ด ์ด๋๋(์ถ์ธกํญ๋ฒ)\n",
" xc, yc, flag = x.copy(), y.copy(), np.zeros(len(x), bool)\n",
" thr = SPOOF_THR.get(weather, 12.0)\n",
" ok_run = 0\n",
" for i in range(1, len(x)):\n",
" px, py = xc[i - 1] + dr[i, 0], yc[i - 1] + dr[i, 1] # ์ถ์ธกํญ๋ฒ ์์ ์์น\n",
" step_ok = np.hypot(x[i] - x[i - 1] - dr[i, 0], y[i] - y[i - 1] - dr[i, 1]) <= thr\n",
" ok_run = ok_run + 1 if step_ok else 0\n",
" if flag[i - 1]:\n",
" # ์คํธํ ๊ตฌ๊ฐ: GPS๊ฐ ์์ ์์น๋ก ๋์์ค๊ฑฐ๋, GPS ์ด๋๋์ด 5์ด ์ฐ์ ๋ฌผ๋ฆฌ์ ์ผ๋ก ๋ง์ผ๋ฉด GPS๋ก ๋ณต๊ท\n",
" if np.hypot(x[i] - px, y[i] - py) < thr * 2 or ok_run >= 5:\n",
" xc[i], yc[i] = x[i], y[i]\n",
" else:\n",
" xc[i], yc[i], flag[i] = px, py, True\n",
" elif not step_ok:\n",
" xc[i], yc[i], flag[i] = px, py, True # GPS ์ด๋๋์ด ๋ฌผ๋ฆฌ์ ์ผ๋ก ๋ง์ง ์์ -> ์คํธํ ์์ฌ\n",
" else:\n",
" xc[i], yc[i] = xc[i - 1] + x[i] - x[i - 1], yc[i - 1] + y[i] - y[i - 1]\n",
" d[\"x\"], d[\"y\"], d[\"spoof_flag\"] = xc, yc, flag\n",
" d = d.reset_index()\n",
" d.attrs.update({\"missing\": n_missing, \"spikes\": int(spike.sum()), \"spoof_pts\": int(flag.sum())})\n",
" return d\n",
"\n",
"\n",
"def xtrack(px, py, plan):\n",
" \"\"\"๊ณํ ๊ฒฝ๋ก(์จ์ดํฌ์ธํธ ํ๊ณก์ ) ๋๋น ์ดํ ๊ฑฐ๋ฆฌ(m).\"\"\"\n",
" P = np.asarray(plan, float)\n",
" segs = [(P[i], P[(i + 1) % len(P)]) for i in range(len(P) if len(P) > 2 else 1)]\n",
" best = np.full(len(px), np.inf)\n",
" for a, b in segs:\n",
" ab = b - a\n",
" u = np.clip(((px - a[0]) * ab[0] + (py - a[1]) * ab[1]) / max(ab @ ab, 1e-9), 0, 1)\n",
" best = np.minimum(best, np.hypot(px - a[0] - u * ab[0], py - a[1] - u * ab[1]))\n",
" return best\n",
"\n",
"\n",
"RULES = {\"turn_rate\": 45.0, \"hover_speed\": 1.5, \"hover_sec\": 20, \"dive_m\": 30.0, \"xtrack_m\": 250.0}\n",
"\n",
"\n",
"def rule_flags(d, plan, R=RULES):\n",
" \"\"\"๊ท์น ๊ธฐ๋ฐ ์ด์ ๋นํ ํ์ (๊ธฐ๋ก ๋จ์). ๊ธ์ ํยทํธ๋ฒ๋งยท๊ธ๊ฐํยท๊ฒฝ๋ก ์ดํยท๋น์ธ๊ฐ ๊ตฌ์ญ ์ง์
.\"\"\"\n",
" hr = np.abs(_wrap(np.diff(d[\"heading\"].to_numpy(float), prepend=d[\"heading\"].iloc[0])))\n",
" slow = (d[\"speed\"] < R[\"hover_speed\"]).to_numpy()\n",
" run = np.zeros(len(slow), int)\n",
" for i in range(len(slow)):\n",
" run[i] = run[i - 1] + 1 if slow[i] and i else int(slow[i])\n",
" hover = np.zeros(len(slow), bool)\n",
" for i in np.where(run >= R[\"hover_sec\"])[0]:\n",
" hover[i - R[\"hover_sec\"] + 1:i + 1] = True\n",
" alt = d[\"alt\"].to_numpy(float)\n",
" drop = alt - np.r_[alt[5:], np.full(5, alt[-1])]\n",
" return {\"๊ธ์ ํ\": hr > R[\"turn_rate\"], \"ํธ๋ฒ๋ง\": hover, \"๊ธ๊ฐํ\": drop > R[\"dive_m\"],\n",
" \"๊ฒฝ๋ก์ดํ\": xtrack(d[\"x\"].to_numpy(float), d[\"y\"].to_numpy(float), plan) > R[\"xtrack_m\"],\n",
" \"๋น์ธ๊ฐ๊ตฌ์ญ\": np.hypot(d[\"x\"] - NFZ[\"x\"], d[\"y\"] - NFZ[\"y\"]).to_numpy() < NFZ[\"r\"]}\n",
"\n",
"# ---- ๊ตฌ๊ฐ(30์ด ์ฐฝ) ํน์ง + ๋น์ง๋ ์ด์ ํ์ง ๊ณตํต ํจ์ ----\n",
"WFEAT = [\"alt_mean\", \"alt_min\", \"alt_std\", \"vz_min\", \"spd_mean\", \"spd_std\", \"spd_max\", \"turn_sum\", \"turn_max\",\n",
" \"straight\", \"rc_mean\", \"rc_zero\", \"nfz_min\", \"nfz_close\", \"xtrack_max\"]\n",
"\n",
"\n",
"def window_feats(d, plan, win=30, step=10):\n",
" \"\"\"30์ด ์ฐฝ์ 10์ด์ฉ ์ฎ๊ธฐ๋ฉฐ ๊ณ ๋ยท์๋ยท์ ํยท์ง์ง์ฑยท์กฐ์ข
์ ํธยท๋น์ธ๊ฐ ๊ตฌ์ญ ์ ๊ทผยท๊ฒฝ๋ก ์ดํ ํน์ง์ ๊ณ์ฐํ๋ค.\"\"\"\n",
" x, y, alt = d[\"x\"].to_numpy(float), d[\"y\"].to_numpy(float), d[\"alt\"].to_numpy(float)\n",
" spd, rc = d[\"speed\"].to_numpy(float), d[\"rc\"].to_numpy(float)\n",
" hr = np.abs(_wrap(np.diff(d[\"heading\"].to_numpy(float), prepend=d[\"heading\"].iloc[0])))\n",
" vz = np.diff(alt, prepend=alt[0])\n",
" nd = np.hypot(x - NFZ[\"x\"], y - NFZ[\"y\"])\n",
" xt = xtrack(x, y, plan)\n",
" step_len = np.hypot(np.diff(x, prepend=x[0]), np.diff(y, prepend=y[0]))\n",
" rows, starts = [], []\n",
" for s in range(0, len(d) - win + 1, step):\n",
" e = s + win\n",
" path = step_len[s + 1:e].sum()\n",
" rows.append([alt[s:e].mean(), alt[s:e].min(), alt[s:e].std(), vz[s:e].min(), spd[s:e].mean(), spd[s:e].std(),\n",
" spd[s:e].max(), hr[s:e].sum(), hr[s:e].max(), np.hypot(x[e - 1] - x[s], y[e - 1] - y[s]) / max(path, 1.0),\n",
" rc[s:e].mean(), (rc[s:e] < 5).mean(), nd[s:e].min(), (nd[s] - nd[e - 1]) / win, xt[s:e].max()])\n",
" starts.append(int(d[\"t\"].iloc[s]))\n",
" return np.array(rows), starts\n",
"\n",
"\n",
"class FlightAnomalyModel:\n",
" \"\"\"ํ์ ๋นํ๋ก๊ทธ(๋ผ๋ฒจ ์์)๋ก ํต์ ํจํด ๊ตฐ์ง(KMeans)๊ณผ ๊ณ ๋ฆฝ ์ฒ(IsolationForest)์ ํ์ตํ๊ณ ,\n",
" ๋ ์ ์๋ฅผ ํ์ต ๊ตฌ๊ฐ ๋ถ์์๋ก ๋๋ 1.0์ ๋์ผ๋ฉด 'ํต์ ํจํด์์ ๋ฒ์ด๋จ'์ผ๋ก ๋ณธ๋ค.\"\"\"\n",
"\n",
" def __init__(self, q=0.999, k=4):\n",
" self.q, self.k = q, k\n",
"\n",
" def fit(self, X):\n",
" self.sc = StandardScaler().fit(X)\n",
" Z = self.sc.transform(X)\n",
" self.iso = IsolationForest(n_estimators=150, random_state=0).fit(Z)\n",
" self.km = KMeans(self.k, n_init=5, random_state=0).fit(Z)\n",
" self.set_q(self.q, X)\n",
" return self\n",
"\n",
" def _raw(self, X):\n",
" Z = self.sc.transform(X)\n",
" return -self.iso.score_samples(Z), self.km.transform(Z).min(axis=1)\n",
"\n",
" def set_q(self, q, X):\n",
" a, b = self._raw(X)\n",
" self.q, self.t_if, self.t_km = q, np.quantile(a, q), np.quantile(b, q)\n",
"\n",
" def score(self, X):\n",
" a, b = self._raw(X)\n",
" return np.maximum(a / self.t_if, b / self.t_km)\n",
"\n",
"\n",
"AFEAT = [\"ml_score\", \"low_alt\", \"rc_weak\", \"nfz_min_km\", \"hover_s\", \"max_speed\", \"night\",\n",
" \"pf_commercial\", \"pf_military\", \"pf_unknown\", \"fw_stock\", \"fw_custom\", \"fw_unknown\"]\n",
"\n",
"\n",
"def attr_features(d, m, ml_score):\n",
" \"\"\"์ด์ฉ ์ฃผ์ฒด ์ถ์ ์ฉ ๋นํ ๋จ์ ํน์ง. ๊ธฐ์ฒดยทํ์จ์ด ์ ๋ณด๋ ์์ฉ ๊ธฐ์ฒด๊ฐ ๋ชจ๋ ์ฃผ์ฒด์ ์ฐ์ด๋ฏ๋ก ์ฝํ ๋จ์์ผ ๋ฟ์ด๋ค.\"\"\"\n",
" return [min(ml_score, 10.0), float((d[\"alt\"] < 60).mean()), float((d[\"rc\"] < 25).mean()),\n",
" float(np.hypot(d[\"x\"] - NFZ[\"x\"], d[\"y\"] - NFZ[\"y\"]).min() / 1000), float((d[\"speed\"] < 1.5).sum()),\n",
" float(d[\"speed\"].max()), float(m[\"weather\"] == \"night\"),\n",
" *[float(m[\"platform\"] == p) for p in (\"commercial\", \"military\", \"unknown\")],\n",
" *[float(m[\"firmware\"] == f) for f in (\"stock\", \"custom\", \"unknown\")]]\n",
"\n",
"\n",
"def flight_stats(data, model):\n",
" \"\"\"๋นํ๋ง๋ค ํ๋จ์ ํ์ํ ๊ฐ(์ต๋ ์ ํ์จ, 5์ด ์ต๋ ํ๊ฐ, ๊ธฐํ ๊ท์น, ML ์ต๋ ์ ์)์ ํ ๋ฒ ๊ณ์ฐํด ๋๋ค.\"\"\"\n",
" rows = []\n",
" for df, m in data:\n",
" d = clean(df, m[\"weather\"])\n",
" f = rule_flags(d, m[\"plan\"], {**RULES, \"turn_rate\": 1e9, \"dive_m\": 1e9})\n",
" alt = d[\"alt\"].to_numpy()\n",
" rows.append({\"id\": m[\"id\"], \"kind\": m[\"kind\"], \"weather\": m[\"weather\"], \"truth\": m[\"kind\"] in SCENARIOS,\n",
" \"turn\": float(np.abs(_wrap(np.diff(d[\"heading\"].to_numpy()))).max()),\n",
" \"drop\": float((alt[:-5] - alt[5:]).max()),\n",
" \"other\": bool(f[\"ํธ๋ฒ๋ง\"].any() or f[\"๊ฒฝ๋ก์ดํ\"].any() or f[\"๋น์ธ๊ฐ๊ตฌ์ญ\"].any()),\n",
" \"ml\": float(model.score(window_feats(d, m[\"plan\"])[0]).max())})\n",
" return pd.DataFrame(rows)\n",
"\n",
"\n",
"def predict(S, c):\n",
" S = S.copy()\n",
" S[\"r_turn\"], S[\"r_dive\"], S[\"r_ml\"] = S[\"turn\"] > c[\"turn\"], S[\"drop\"] > c[\"dive\"], S[\"ml\"] > c[\"ml\"]\n",
" S[\"pred\"] = S[\"r_turn\"] | S[\"r_dive\"] | S[\"r_ml\"] | S[\"other\"]\n",
" return S\n",
"\n",
"\n",
"def summarize(S):\n",
" tp, fp = (S.truth & S.pred).sum(), (~S.truth & S.pred).sum()\n",
" return {\"์ ํ๋\": round(float((S.truth == S.pred).mean()), 3), \"์ฌํ์จ\": round(float(tp / S.truth.sum()), 3),\n",
" \"์ค๊ฒฝ๋ณด์จ\": round(float(fp / max(S.pred.sum(), 1)), 3), \"์ ์ ์คํ\": int(fp), \"๋ฏธํ\": int((S.truth & ~S.pred).sum())}\n",
"\n",
"\n",
"def aar_log(S):\n",
" rows = []\n",
" for _, r in S[~S.truth & S.pred].iterrows():\n",
" cause = (\"์ธก๋ Uํด ๊ธ์ ํ\" if r.kind == \"survey\" else \"์ ํ\") if r.r_turn else (\"๋ํ ํ๊ฐ(\" + r.weather + \")\" if r.r_dive else \"๋๋ฌธ ์ ์ ํจํด(ML)\")\n",
" rows.append({\"๊ตฌ๋ถ\": \"์คํ\", \"์์ธ\": cause, \"๋นํ\": r.id})\n",
" for _, r in S[S.truth & ~S.pred].iterrows():\n",
" rows.append({\"๊ตฌ๋ถ\": \"๋ฏธํ\", \"์์ธ\": f\"{r.kind} ์ ์ ๋ฏธ๋ฌ\", \"๋นํ\": r.id})\n",
" return pd.DataFrame(rows, columns=[\"๊ตฌ๋ถ\", \"์์ธ\", \"๋นํ\"])\n",
"\n",
"\n",
"def windows(data):\n",
" return np.vstack([window_feats(clean(df, m[\"weather\"]), m[\"plan\"])[0] for df, m in data])\n",
"\n",
"\n",
"base_X = windows(make_dataset(80, 21, anomaly_ratio=0.0))\n",
"model = FlightAnomalyModel(q=0.995).fit(base_X)\n",
"aar_data, hold_data = make_dataset(200, seed=11), make_dataset(200, seed=13)\n",
"aar = flight_stats(aar_data, model) # ๋ชจ์ ์ด์ฉ(AAR ๋์)\n",
"hold = flight_stats(hold_data, model) # ํ๊ท ์ํ์ฉ(์ฌ๋ณด์ ์ ์ฐ์ง ์์)\n",
"\n",
"# 1) ์ด๊ธฐ ์ค์ ('๋์น์ง ์๊ธฐ' ์ฐ์ ): ์ ํ 30๋/์ด, ํ๊ฐ 30m, ML ์ ์ 1.0\n",
"init = {\"turn\": 30, \"dive\": 30, \"ml\": 1.0}\n",
"S0 = predict(aar, init)\n",
"before = summarize(S0)\n",
"log = aar_log(S0)\n",
"print(\"[1. ๋ฌด์์ด ์ผ์ด๋ฌ๋] ์ด๊ธฐ ์ค์ :\", before)\n",
"print(log.groupby([\"๊ตฌ๋ถ\", \"์์ธ\"]).size().rename(\"๊ฑด์\").to_string())\n",
"\n",
"# 2) ์ญํ๋ฅ 1: ์คํ ์์ธ(์ธก๋ Uํดยท๊ฐํ)์ด ํ์ ํ์ต ์๋ฃ์ ์ ์์ -> ์ธก๋ยท๊ฐํ ํ์ ๋ก๊ทธ 60๊ฑด์ ์ถ๊ฐ ์์งํด ์ฌํ์ต\n",
"extra = [(df, m) for df, m in make_dataset(200, 22, anomaly_ratio=0.0) if m[\"kind\"] == \"survey\" or m[\"weather\"] == \"windy\"][:60]\n",
"model = FlightAnomalyModel(q=0.995).fit(np.vstack([base_X, windows(extra)]))\n",
"aar, hold = flight_stats(aar_data, model), flight_stats(hold_data, model)\n",
"print(f\"\\n[์ญํ๋ฅ] ํ์ ํ์ต ๋ก๊ทธ 80๊ฑด + ์ธก๋ยท๊ฐํ ๋ก๊ทธ {len(extra)}๊ฑด์ผ๋ก ML ์ฌํ์ต\")\n",
"\n",
"# 3) ์ญํ๋ฅ 2: ์ฌํ์จ 95% ์ด์ ์ ์ฝ ์์์ ์ ํ๋ ์ต๋ ์๊ณ๊ฐ ์กฐํฉ ํ์\n",
"grid = []\n",
"for t in [30, 45, 60, 90]:\n",
" for dv in [30, 40, 50]:\n",
" for mlt in [1.0, 1.2, 1.5, 2.0]:\n",
" grid.append({\"turn\": t, \"dive\": dv, \"ml\": mlt, **summarize(predict(aar, {\"turn\": t, \"dive\": dv, \"ml\": mlt}))})\n",
"g = pd.DataFrame(grid)\n",
"ok = g[g[\"์ฌํ์จ\"] >= 0.95].sort_values([\"์ ํ๋\", \"ml\"], ascending=[False, True])\n",
"best = ok.iloc[0][[\"turn\", \"dive\", \"ml\"]].to_dict()\n",
"print(\"\\n[2. ๋ฌด์์ ๋ฐ๊พธ๋] ๊ฒฉ์ ํ์ ์์ 5๊ฐ\")\n",
"print(ok.head().to_string(index=False))\n",
"\n",
"# 3) ์ฌ๋ณด์ ์ ํ ๋น๊ต(๋ชจ์ ์ด์ฉ ์๋ฃ + ํ๊ท ์ํ ์๋ฃ)\n",
"S1 = predict(aar, best)\n",
"after = summarize(S1)\n",
"print(f\"\\n์ฌ๋ณด์ ๊ฐ: ์ ํ์จ {best['turn']:.0f}๋/์ด, ๊ธ๊ฐํ {best['dive']:.0f}m, ML ์ ์ {best['ml']:.1f}\")\n",
"print(\"์ฌ๋ณด์ ํ:\", after)\n",
"print(f\"์ข
ํฉ ์ ํ๋ {before['์ ํ๋']:.3f} -> {after['์ ํ๋']:.3f} ({(after['์ ํ๋'] - before['์ ํ๋']) * 100:+.1f}%p, ๋ชฉํ +10%p: {'๋ฌ์ฑ' if after['์ ํ๋'] - before['์ ํ๋'] >= 0.1 else '๋ฏธ๋ฌ'})\")\n",
"h0, h1 = summarize(predict(hold, init)), summarize(predict(hold, best))\n",
"print(f\"ํ๊ท ์ํ(๋ณ๋ 200๊ฑด): ์ ํ๋ {h0['์ ํ๋']:.3f} -> {h1['์ ํ๋']:.3f}, ์ฌํ์จ {h0['์ฌํ์จ']:.3f} -> {h1['์ฌํ์จ']:.3f}\")\n",
"print(\"์ฌ๋ณด์ ํ ๋ฏธํ:\", S1[S1.truth & ~S1.pred][\"kind\"].tolist(), \"/ ์คํ:\", S1[~S1.truth & S1.pred][\"kind\"].tolist())\n",
"repeat = set(log[\"๋นํ\"]) & set(aar_log(S1)[\"๋นํ\"])\n",
"print(f\"์ฌ๋ณด์ ํ์๋ ๋ฐ๋ณต๋ ์ค๋ฅ: {len(repeat)}๊ฑด {sorted(repeat)[:6]}\")\n",
"print(\"[3. ๋จ์ ๊ณผ์ ] ๋ฐ๋ณต ์ค๋ฅ๋ ๊ท์น ์๊ณ๊ฐ๋ง์ผ๋ก ํ๋ฆฌ์ง ์๋ ๊ฒฝ์ฐ๋ก, ํด๋น ๋ก๊ทธ๋ฅผ ํ์ ํ์ต ์๋ฃ์ ์ถ๊ฐ(์ง์ ํ์ต)ํด ์ฌํ์ต ๋์์ผ๋ก ๋ฑ๋กํ๋ค.\")\n",
"\n",
"tuned = f\"rules:\\n turn_rate_dps: {best['turn']:.0f}\\n dive_drop_m: {best['dive']:.0f}\\nml:\\n quantile: 0.995\\n score_threshold: {best['ml']:.1f}\\nconstraints: {{min_recall: 0.95}}\"\n",
"print(\"\\n[์ฌ๋ณด์ ํ๋ผ๋ฏธํฐ(isr03_tuned_params.yaml ๋ด์ฉ)]\\n\" + tuned)\n",
"\n",
"plt.figure(figsize=(6, 4))\n",
"for t in sorted(g[\"turn\"].unique()):\n",
" s = g[(g[\"turn\"] == t) & (g[\"dive\"] == best[\"dive\"])]\n",
" plt.plot(s[\"ml\"], s[\"์ ํ๋\"], \"o-\", label=f\"turn>{t} deg/s\")\n",
"plt.xlabel(\"ML score threshold\"); plt.ylabel(\"accuracy\"); plt.legend(); plt.title(\"AAR re-tuning grid (synthetic)\")\n",
"plt.show()\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"๐ก [์ฐธ๊ณ ] ๋ฏธ์
09 ์์ ์คํ ๊ฒฐ๊ณผ (Output)
\n",
"\n",
"```text\n",
"[1. ๋ฌด์์ด ์ผ์ด๋ฌ๋] ์ด๊ธฐ ์ค์ : {'์ ํ๋': 0.865, '์ฌํ์จ': 1.0, '์ค๊ฒฝ๋ณด์จ': 0.213, '์ ์ ์คํ': 27, '๋ฏธํ': 0}\n",
"๊ตฌ๋ถ ์์ธ \n",
"์คํ ๋ํ ํ๊ฐ(windy) 7\n",
" ์ธก๋ Uํด ๊ธ์ ํ 20\n",
"\n",
"[์ญํ๋ฅ] ํ์ ํ์ต ๋ก๊ทธ 80๊ฑด + ์ธก๋ยท๊ฐํ ๋ก๊ทธ 55๊ฑด์ผ๋ก ML ์ฌํ์ต\n",
"\n",
"[2. ๋ฌด์์ ๋ฐ๊พธ๋] ๊ฒฉ์ ํ์ ์์ 5๊ฐ\n",
" turn dive ml ์ ํ๋ ์ฌํ์จ ์ค๊ฒฝ๋ณด์จ ์ ์ ์คํ ๋ฏธํ\n",
" 60 40 1.2 0.980 0.98 0.020 2 2\n",
" 60 50 1.2 0.980 0.98 0.020 2 2\n",
" 90 40 1.2 0.980 0.98 0.020 2 2\n",
" 90 50 1.2 0.980 0.98 0.020 2 2\n",
" 45 40 1.2 0.965 0.98 0.049 5 2\n",
"\n",
"์ฌ๋ณด์ ๊ฐ: ์ ํ์จ 60๋/์ด, ๊ธ๊ฐํ 40m, ML ์ ์ 1.2\n",
"์ฌ๋ณด์ ํ: {'์ ํ๋': 0.98, '์ฌํ์จ': 0.98, '์ค๊ฒฝ๋ณด์จ': 0.02, '์ ์ ์คํ': 2, '๋ฏธํ': 2}\n",
"์ข
ํฉ ์ ํ๋ 0.865 -> 0.980 (+11.5%p, ๋ชฉํ +10%p: ๋ฌ์ฑ)\n",
"ํ๊ท ์ํ(๋ณ๋ 200๊ฑด): ์ ํ๋ 0.850 -> 0.980, ์ฌํ์จ 1.000 -> 0.960\n",
"์ฌ๋ณด์ ํ ๋ฏธํ: ['low_level', 'low_level'] / ์คํ: ['survey', 'survey']\n",
"์ฌ๋ณด์ ํ์๋ ๋ฐ๋ณต๋ ์ค๋ฅ: 2๊ฑด ['FL0019', 'FL0039']\n",
"[3. ๋จ์ ๊ณผ์ ] ๋ฐ๋ณต ์ค๋ฅ๋ ๊ท์น ์๊ณ๊ฐ๋ง์ผ๋ก ํ๋ฆฌ์ง ์๋ ๊ฒฝ์ฐ๋ก, ํด๋น ๋ก๊ทธ๋ฅผ ํ์ ํ์ต ์๋ฃ์ ์ถ๊ฐ(์ง์ ํ์ต)ํด ์ฌํ์ต ๋์์ผ๋ก ๋ฑ๋กํ๋ค.\n",
"\n",
"[์ฌ๋ณด์ ํ๋ผ๋ฏธํฐ(isr03_tuned_params.yaml ๋ด์ฉ)]\n",
"rules:\n",
" turn_rate_dps: 60\n",
" dive_drop_m: 40\n",
"ml:\n",
" quantile: 0.995\n",
" score_threshold: 1.2\n",
"constraints: {min_recall: 0.95}\n",
"```\n",
" \n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## ๐ฏ ๋ฏธ์
10. ๋๋ก ๋นํ๋ก๊ทธ ์ด์ ํ์ง ์์คํ
ํตํฉ ๊ฒ์ฆ ๋ฐ ISR ์ ๋ณดํ๋จ ๋ธ๋ฆฌํ ํจํค์ง ์์ฑ\n",
"**์์์๊ฐ**: ์ฝ 3.5์๊ฐ\n",
"\n",
"### ๐ ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ์ํ ์ง์นจ\n",
"#### ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ๋น์์ฑ\n",
"\n",
"๋๋ก ๋นํ๋ก๊ทธ ์ด์ ํ์ง ์์คํ
์ ์ต์ข
ํตํฉ ๋น๋ํ๊ณ ์ ๋ณด์์ ๋ธ๋ฆฌํ์ฉ ์ต์ข
๋ถ์ ๋ณด๊ณ ์ ๋ฐ ์ฌ์ฉ์ ๊ฐ์ด๋๋ฅผ ์์ฑํ๋ค. ์ค์ ์ ์ ํ์ฅ์์๋ ๋จ์ ํ์ง์ ๊ทธ์น์ง ์๊ณ ์ ์ํ ์ํ ํ๋จ๊ณผ ์ ๋ขฐ๋ ๋์ ์์ฌ๊ฒฐ์ ์ด ํ์์ ์ด๋ฏ๋ก, **๋ก๊ทธ ๊ฒ์ฆ๋ถํฐ ์ ์ฒ๋ฆฌ, ๊ท์นยทML ํ์ง, ์ ๋ขฐ๋ ์์ง, ๊ฒํ ๋๊ธฐ์ด, ๋์๋ณด๋๊น์ง ์ ์ฒด ์์คํ
์ ํตํฉํ๊ณ ISR ์ ๋ณดํ๋จ ๋ธ๋ฆฌํ ํจํค์ง๋ฅผ ์์ฑํ๋ค.** ๊ณผ์
์ ์์ํ์ฌ ์คํ์ ๋ฐฉ์งํ๊ณ ์์ ๋์๋ ฅ์ ๊ทน๋ํํด์ผ ํฉ๋๋ค.\n",
"\n",
"์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ณธ ๋ฏธ์
์์๋ **์ฌ๋ณด์ ํ๋ผ๋ฏธํฐ๋ก ์ ์ฒด ๊ณผ์ ์ ํ ๋ฒ์ ์คํํ๋ ํตํฉ ์ฝ๋๋ฅผ ์์ฑํ๊ณ ์์ ๋ก๊ทธ๊ฐ ์๋ ์ฐจ๋จ๋๋์ง ํ์ธํ๊ณ ์ ํจ์ฑ์ ์ ๊ฒํ๋** ์ค์ต์ ์ฒด๊ณ์ ์ผ๋ก ์ํํฉ๋๋ค.\n",
"\n",
"ํ์ต์๋ ์ค์ต๊ฐ์ด๋์ ์ ์๋ ์ด 4๊ฐ ์ธ๋ถ ํ๋(๊ธฐ๋ณธ ๊ธฐ๋ฅ ๊ตฌํ โ ๊ฒฐ๊ณผ ์งํ ๊ฒ์ฆ โ ํ๋ผ๋ฏธํฐ ํ๋ ๋ฐ ๋์ ๊ณผ์ )์ ์์ฐจ์ ์ผ๋ก ์งํํ๋ฉฐ ํต์ฌ ํ์ ๊ธฐ์ค์ ๋ฌ์ฑํฉ๋๋ค.\n",
"\n",
"**์ด๋ฒ ๊ฒฐ๊ณผ๋ฌผ:** `src/main_isr03.py, README.md, docs/isr03_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": [
"\"\"\"๋ก๊ทธ ๊ฒ์ฆยท์ ์ฒ๋ฆฌยท๊ท์น ํ์งยทML ์ด์ ํ์งยท์ด์ฉ ์ฃผ์ฒด ์ ๋ขฐ๋ ์์งยท์ ๋ณด๋ถ์๊ด ๊ฒํ ๋๊ธฐ์ดยท๋์๋ณด๋๊น์ง ๋๋ก ๋นํ๋ก๊ทธ ์ด์ ํ์ง ์์คํ
์ ์ฒด๋ฅผ ํ ํ์ผ๋ก ํตํฉ ์คํํ๊ณ , ์์ฒด ์ ๊ฒ(๊ธฐ๋ฅ ์ปค๋ฒ๋ฆฌ์ง ํฌํจ)๊ณผ ISR ์ ๋ณดํ๋จ ๋ธ๋ฆฌํ ์์ฝ์ ์ถ๋ ฅํ๋ค.\"\"\"\n",
"import hashlib\n",
"import json\n",
"import math\n",
"import time\n",
"import numpy as np\n",
"import pandas as pd\n",
"import matplotlib.pyplot as plt\n",
"from sklearn.cluster import KMeans\n",
"from sklearn.ensemble import IsolationForest\n",
"from sklearn.linear_model import LogisticRegression\n",
"from sklearn.pipeline import make_pipeline\n",
"from sklearn.preprocessing import StandardScaler\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# [๊ต์ก์ฉ๊ฐ์์๋ฃ] ํฉ์ฑ ๋๋ก ๋นํ๋ก๊ทธ ์์ฑ๊ธฐ (์ค์ ์ขํยท๊ธฐ์ฒดยท๋ถ๋ ์ ๋ณด ์์)\n",
"# ๊ฐ์ ๊ตฌ์ญ(์์ ๊ธฐ์ค ๋ยท๋ถ ๋ฐฉํฅ m ์ขํ)์์ 1์ด ๊ฐ๊ฒฉ ๋นํ๋ก๊ทธ์ ์ ๋ต(์ด์ ๊ตฌ๊ฐยท์ด์ฉ ์ฃผ์ฒด)์ ๋ง๋ ๋ค.\n",
"# ---------------------------------------------------------------------------\n",
"NFZ = {\"x\": 2500.0, \"y\": 2500.0, \"r\": 700.0} # ๊ฐ์ ๋น์ธ๊ฐ ๋นํ๊ตฌ์ญ\n",
"WATCH = {\"x\": -2000.0, \"y\": 1500.0, \"r\": 600.0} # ๊ฐ์ ๊ด์ฌ ๊ตฌ์ญ\n",
"SCENARIOS = [\"sharp_turn\", \"hover\", \"dive\", \"deviation\", \"nfz_entry\",\n",
" \"low_level\", \"rc_silent\", \"infiltration\", \"loiter\", \"speed_burst\"]\n",
"OPERATORS = [\"friendly\", \"civil\", \"hostile\"]\n",
"# ์๋๋ฆฌ์ค๋ณ ์ด์ฉ ์ฃผ์ฒด ๋ถํฌ: ์ผ๋ถ๋ฌ ํฌ๊ฒ ๊ฒน์น๊ฒ ์ค๊ณํ๋ค(๋นํ ํน์ฑ๋ง์ผ๋ก ์ด์ฉ ์ฃผ์ฒด๋ฅผ ๋จ์ ํ ์ ์๋ค).\n",
"OPERATOR_P = {\"normal\": (.45, .47, .08), \"survey\": (.15, .82, .03), \"sharp_turn\": (.40, .35, .25),\n",
" \"hover\": (.25, .45, .30), \"dive\": (.25, .25, .50), \"deviation\": (.30, .40, .30),\n",
" \"nfz_entry\": (.10, .50, .40), \"low_level\": (.35, .15, .50), \"rc_silent\": (.30, .15, .55),\n",
" \"infiltration\": (.12, .08, .80), \"loiter\": (.30, .20, .50), \"speed_burst\": (.30, .20, .50)}\n",
"PLATFORM_P = {\"friendly\": (.40, .55, .05), \"civil\": (.88, .02, .10), \"hostile\": (.55, .30, .15)} # commercial/military/unknown\n",
"FIRMWARE_P = {\"friendly\": (.30, .65, .05), \"civil\": (.85, .10, .05), \"hostile\": (.20, .50, .30)} # stock/custom/unknown\n",
"\n",
"\n",
"def _wrap(a):\n",
" return (a + 180.0) % 360.0 - 180.0\n",
"\n",
"\n",
"def _bearing(p, q):\n",
" return math.degrees(math.atan2(q[0] - p[0], q[1] - p[1])) % 360.0\n",
"\n",
"\n",
"def _away_from_nfz(rng, min_d):\n",
" while True:\n",
" c = rng.uniform(-3800, 3800, 2)\n",
" if np.hypot(c[0] - NFZ[\"x\"], c[1] - NFZ[\"y\"]) > min_d:\n",
" return c\n",
"\n",
"\n",
"def make_flight(fid, kind, rng, n=300, weather=None):\n",
" \"\"\"๋นํ 1ํ = (๋ก๊ทธ DataFrame, ๋ฉํ dict). kind: normal / survey / SCENARIOS ์ค ํ๋\"\"\"\n",
" weather = weather or str(rng.choice([\"clear\", \"night\", \"windy\", \"rain\"], p=[.4, .25, .2, .15]))\n",
" wind = {\"clear\": 1.0, \"night\": 1.0, \"windy\": 2.5, \"rain\": 1.8}[weather]\n",
" v0, alt0 = rng.uniform(11, 17), rng.uniform(70, 150)\n",
" nfz_c = np.array([NFZ[\"x\"], NFZ[\"y\"]])\n",
" if kind == \"infiltration\": # ์๊ฑฐ๋ฆฌ์์ ๋น์ธ๊ฐ ๊ตฌ์ญ ๋ฐฉํฅ์ผ๋ก ์ ๊ณ ๋ ์ง์ง\n",
" ang = rng.uniform(0, 2 * np.pi)\n",
" start = nfz_c + 4000 * np.array([np.cos(ang), np.sin(ang)])\n",
" plan = np.array([start, nfz_c])\n",
" v0, alt0 = rng.uniform(9, 11), rng.uniform(25, 35)\n",
" else:\n",
" if kind == \"nfz_entry\": # ๋น์ธ๊ฐ ๊ตฌ์ญ 1.6km ๋ฐ์์ ์์ฐฐํ๋ค๊ฐ ์ง์
\n",
" ang = rng.uniform(0, 2 * np.pi)\n",
" c = nfz_c + 1600 * np.array([np.cos(ang), np.sin(ang)])\n",
" else:\n",
" c = _away_from_nfz(rng, 2600)\n",
" if kind == \"survey\": # ๋ฏผ๊ฐ ์ธก๋ํ ์๋ณต(์ง๊ทธ์ฌ๊ทธ) ๊ฒฝ๋ก\n",
" w, h = rng.uniform(500, 700), 120.0\n",
" plan = np.array([c + [(-w if i % 4 in (0, 3) else w), i // 2 * h - 360] for i in range(8)])\n",
" else: # ์ฌ๊ฐ ์์ฐฐ ๊ฒฝ๋ก(์จ์ดํฌ์ธํธ 4๊ฐ)\n",
" a, b, th = rng.uniform(300, 600), rng.uniform(200, 400), rng.uniform(0, np.pi)\n",
" R = np.array([[np.cos(th), -np.sin(th)], [np.sin(th), np.cos(th)]])\n",
" plan = c + np.array([[-a, -b], [a, -b], [a, b], [-a, b]]) @ R.T\n",
" s0 = int(rng.integers(80, 160))\n",
" span = {\"sharp_turn\": 30, \"hover\": 60, \"dive\": 8, \"deviation\": 70, \"nfz_entry\": 120, \"low_level\": 120,\n",
" \"rc_silent\": 120, \"infiltration\": n - 20, \"loiter\": 90, \"speed_burst\": 25}.get(kind, 0)\n",
" if kind == \"infiltration\":\n",
" s0 = 20\n",
" win = (s0, s0 + span)\n",
" turns = set(rng.choice(np.arange(s0, s0 + 30), 3, replace=False).tolist()) if kind == \"sharp_turn\" else set()\n",
" # ๋์๋ฅผ ๋ฏธ๋ฆฌ ํ ๋ฒ์ ๋ฝ์ ๋๊ณ (์๋ ํฅ์) 1์ด ๋จ์๋ก ๊ธฐ์ฒด ์ด๋์ ์ ๋ถํ๋ค.\n",
" e_h, e_v, e_a = rng.normal(0, 0.8 * wind, n), rng.normal(0, 0.3 * wind, n), rng.normal(0, 0.4 * wind, n)\n",
" rc_arr = np.clip(rng.normal(rng.uniform(60, 92), 6, n), 0, 100)\n",
" gust = (rng.random(n) < (0.004 if weather == \"windy\" else 0.0)) * rng.uniform(20, 35, n)\n",
" burst, turn_amt = rng.uniform(23, 28), rng.choice([-1, 1], n) * rng.uniform(100, 160, n)\n",
" side = float(rng.choice([-90, 90]))\n",
" low_alt, rng_rc = rng.uniform(35, 55), rng.uniform(0, 20, n) # ์ ๊ณ ๋ ๋ชฉํ, ์กฐ์ข
์ ํธ ๋์ ์ ์์ฌ ์์ ๊ฐ\n",
" x, y, wi = float(plan[0][0]), float(plan[0][1]), 1\n",
" hdg, alt = _bearing((x, y), plan[1]), alt0\n",
" max_rate = 40.0 if kind == \"survey\" else 12.0\n",
" dev_target, X, Y, A, V, HD = None, np.zeros(n), np.zeros(n), np.zeros(n), np.zeros(n), np.zeros(n)\n",
" for t in range(n):\n",
" inw = win[0] <= t < win[1]\n",
" tx, ty = plan[1] if kind == \"infiltration\" else plan[wi % len(plan)]\n",
" spd_cmd, alt_cmd = v0, alt0\n",
" if inw:\n",
" if kind == \"deviation\":\n",
" if dev_target is None:\n",
" r = math.radians(hdg + side)\n",
" dev_target = (x + 900 * math.sin(r), y + 900 * math.cos(r))\n",
" tx, ty = dev_target\n",
" elif kind == \"nfz_entry\":\n",
" tx, ty = NFZ[\"x\"], NFZ[\"y\"]\n",
" elif kind == \"hover\":\n",
" spd_cmd = 0.0\n",
" elif kind == \"speed_burst\":\n",
" spd_cmd = burst\n",
" elif kind == \"low_level\":\n",
" alt_cmd = low_alt\n",
" if kind in (\"rc_silent\", \"infiltration\"):\n",
" rc_arr[t] = rng_rc[t]\n",
" if inw and kind == \"loiter\":\n",
" hdg = (hdg + 9.0 + e_h[t] / wind) % 360\n",
" else:\n",
" d = _wrap(math.degrees(math.atan2(tx - x, ty - y)) - hdg)\n",
" hdg = (hdg + max(-max_rate, min(max_rate, d)) + e_h[t]) % 360\n",
" if t in turns:\n",
" hdg = (hdg + turn_amt[t]) % 360\n",
" v = abs(spd_cmd + e_v[t]) if spd_cmd > 0 else abs(e_v[t] / wind)\n",
" x += v * math.sin(math.radians(hdg)); y += v * math.cos(math.radians(hdg))\n",
" if kind != \"infiltration\" and math.hypot(x - tx, y - ty) < 40 and not (inw and kind in (\"deviation\", \"nfz_entry\")):\n",
" wi += 1\n",
" if inw and kind == \"dive\":\n",
" alt -= 9.0 + e_v[t]\n",
" else:\n",
" alt += max(-3.0, min(3.0, alt_cmd - alt)) + e_a[t]\n",
" alt -= gust[t] # ๋ํ ํ๊ฐ๊ธฐ๋ฅ(์ ์ ๋นํ์์๋ ๋ฐ์)\n",
" if kind == \"dive\" and t >= win[1]:\n",
" alt0 = min(alt0, alt)\n",
" alt = max(alt, 3.0)\n",
" X[t], Y[t], A[t], V[t], HD[t] = x, y, alt, v, hdg\n",
" gps_sd = {\"clear\": 2.0, \"night\": 2.0, \"windy\": 3.0, \"rain\": 4.5}[weather] # ์
์ฒํ์ผ์๋ก GPS ์ก์(๋ค์ค๊ฒฝ๋ก) ์ฆ๊ฐ\n",
" df = pd.DataFrame({\"t\": np.arange(n), \"x\": X + rng.normal(0, gps_sd, n), \"y\": Y + rng.normal(0, gps_sd, n),\n",
" \"alt\": A + rng.normal(0, 0.5, n), \"speed\": V + rng.normal(0, 0.2, n),\n",
" \"heading\": (HD + rng.normal(0, 1.5, n)) % 360, \"rc\": rc_arr.round(1),\n",
" \"sats\": rng.integers(10, 15, n), \"hdop\": rng.uniform(0.6, 1.2, n).round(2),\n",
" \"x_true\": X, \"y_true\": Y})\n",
" df[\"anomaly\"] = (df[\"t\"] >= win[0]) & (df[\"t\"] < win[1]) & (kind in SCENARIOS)\n",
" op = OPERATORS[int(rng.choice(3, p=OPERATOR_P[kind]))]\n",
" meta = {\"id\": fid, \"kind\": kind, \"weather\": weather, \"operator\": op, \"plan\": plan, \"window\": win if kind in SCENARIOS else None,\n",
" \"platform\": [\"commercial\", \"military\", \"unknown\"][int(rng.choice(3, p=PLATFORM_P[op]))],\n",
" \"firmware\": [\"stock\", \"custom\", \"unknown\"][int(rng.choice(3, p=FIRMWARE_P[op]))]}\n",
" return df, meta\n",
"\n",
"\n",
"def corrupt(df, rng, spoof=True, emi=True, drop=True, dup=True):\n",
" \"\"\"์ ์ฅ ์ก์ ์ฃผ์
: GPS ์คํธํ(์์น ์ ํยทํ๋ฅ), ์ ์๊ธฐ ๊ฐ์ญ(๊ณ ๋ ์คํ์ดํฌ), ์ผ์ ๋จ์ (๊ฒฐ์ธก), ์ค๋ณต ๊ธฐ๋ก.\"\"\"\n",
" d = df.copy()\n",
" d[\"spoof_true\"] = False\n",
" if spoof:\n",
" a = int(rng.integers(40, len(d) - 60)); L = int(rng.integers(25, 45))\n",
" th = rng.uniform(0, 2 * np.pi)\n",
" drift = rng.uniform(22, 28) * np.arange(L)[:, None] * np.array([np.cos(th), np.sin(th)])\n",
" jump = rng.uniform(250, 400) * np.array([np.cos(th + 1), np.sin(th + 1)])\n",
" d.loc[a:a + L - 1, [\"x\", \"y\"]] += drift + jump\n",
" d.loc[a:a + L - 1, \"spoof_true\"] = True\n",
" if emi:\n",
" idx = rng.choice(len(d), 6, replace=False)\n",
" d.loc[idx, \"alt\"] += rng.choice([-1, 1], 6) * rng.uniform(60, 150, 6)\n",
" if drop:\n",
" a = int(rng.integers(10, len(d) - 20))\n",
" d.loc[a:a + int(rng.integers(4, 9)), [\"x\", \"y\", \"alt\", \"speed\", \"heading\"]] = np.nan\n",
" d = d.drop(index=rng.choice(len(d), 5, replace=False))\n",
" if dup:\n",
" d = pd.concat([d, d.sample(5, random_state=int(rng.integers(1e6)))]).sort_values(\"t\", kind=\"stable\")\n",
" return d.reset_index(drop=True)\n",
"\n",
"\n",
"def make_dataset(n=200, seed=7, anomaly_ratio=0.5, scenarios=SCENARIOS):\n",
" rng = np.random.default_rng(seed)\n",
" out = []\n",
" for i in range(n):\n",
" if i % 10 < anomaly_ratio * 10:\n",
" kind = scenarios[(i // 10 * int(anomaly_ratio * 10) + i % 10) % len(scenarios)]\n",
" else:\n",
" kind = \"survey\" if i % 10 == 9 else \"normal\"\n",
" df, meta = make_flight(f\"FL{i:04d}\", kind, rng)\n",
" out.append((df, meta))\n",
" return out\n",
"# ---------------------------------------------------------------------------\n",
"\n",
"# ---- ์ ์ฒ๋ฆฌยท๊ท์น ํ์ง ๊ณตํต ํจ์ ----\n",
"SPOOF_THR = {\"clear\": 12.0, \"night\": 12.0, \"windy\": 15.0, \"rain\": 19.0} # ๊ธฐ์๋ณ GPS-์ถ์ธกํญ๋ฒ ๋ถ์ผ์น ํ์ฉ์น(m)\n",
"\n",
"\n",
"def clean(df, weather=\"clear\"):\n",
" \"\"\"์ ์ฒ๋ฆฌ: ์ค๋ณต ์ ๊ฑฐยท์ ๋ ฌ -> 1์ด ์ฌํ๋ณธํ -> ๊ฒฐ์ธก ๋ณด๊ฐ -> ๊ณ ๋ ์คํ์ดํฌ ์ ๊ฑฐ -> GPS ์คํธํ ๊ตฌ๊ฐ ์ถ์ธกํญ๋ฒ ๋ณด์ .\"\"\"\n",
" d = df.drop_duplicates(\"t\").sort_values(\"t\").set_index(\"t\")\n",
" d = d.reindex(np.arange(int(d.index.min()), int(d.index.max()) + 1))\n",
" n_missing = int(d[\"x\"].isna().sum())\n",
" num = [\"x\", \"y\", \"alt\", \"speed\", \"rc\", \"sats\", \"hdop\"]\n",
" d[num] = d[num].interpolate(limit_direction=\"both\")\n",
" hd = d[\"heading\"].dropna() # ๋ฐฉ์๋ 0/360 ๊ฒฝ๊ณ๋ฅผ ํ์ด์ ๋ณด๊ฐ\n",
" un = pd.Series(np.degrees(np.unwrap(np.radians(hd.to_numpy(float)))), index=hd.index)\n",
" d[\"heading\"] = un.reindex(d.index).interpolate(limit_direction=\"both\").to_numpy() % 360\n",
" med = d[\"alt\"].rolling(5, center=True, min_periods=1).median()\n",
" spike = (d[\"alt\"] - med).abs() > 25\n",
" d.loc[spike, \"alt\"] = med[spike]\n",
" x, y = d[\"x\"].to_numpy(float), d[\"y\"].to_numpy(float)\n",
" r = np.radians(d[\"heading\"].to_numpy(float)); v = d[\"speed\"].to_numpy(float)\n",
" dr = np.c_[v * np.sin(r), v * np.cos(r)] # ๋ณด๊ณ ์๋ยท๋ฐฉ์๋ก ๊ณ์ฐํ 1์ด ์ด๋๋(์ถ์ธกํญ๋ฒ)\n",
" xc, yc, flag = x.copy(), y.copy(), np.zeros(len(x), bool)\n",
" thr = SPOOF_THR.get(weather, 12.0)\n",
" ok_run = 0\n",
" for i in range(1, len(x)):\n",
" px, py = xc[i - 1] + dr[i, 0], yc[i - 1] + dr[i, 1] # ์ถ์ธกํญ๋ฒ ์์ ์์น\n",
" step_ok = np.hypot(x[i] - x[i - 1] - dr[i, 0], y[i] - y[i - 1] - dr[i, 1]) <= thr\n",
" ok_run = ok_run + 1 if step_ok else 0\n",
" if flag[i - 1]:\n",
" # ์คํธํ ๊ตฌ๊ฐ: GPS๊ฐ ์์ ์์น๋ก ๋์์ค๊ฑฐ๋, GPS ์ด๋๋์ด 5์ด ์ฐ์ ๋ฌผ๋ฆฌ์ ์ผ๋ก ๋ง์ผ๋ฉด GPS๋ก ๋ณต๊ท\n",
" if np.hypot(x[i] - px, y[i] - py) < thr * 2 or ok_run >= 5:\n",
" xc[i], yc[i] = x[i], y[i]\n",
" else:\n",
" xc[i], yc[i], flag[i] = px, py, True\n",
" elif not step_ok:\n",
" xc[i], yc[i], flag[i] = px, py, True # GPS ์ด๋๋์ด ๋ฌผ๋ฆฌ์ ์ผ๋ก ๋ง์ง ์์ -> ์คํธํ ์์ฌ\n",
" else:\n",
" xc[i], yc[i] = xc[i - 1] + x[i] - x[i - 1], yc[i - 1] + y[i] - y[i - 1]\n",
" d[\"x\"], d[\"y\"], d[\"spoof_flag\"] = xc, yc, flag\n",
" d = d.reset_index()\n",
" d.attrs.update({\"missing\": n_missing, \"spikes\": int(spike.sum()), \"spoof_pts\": int(flag.sum())})\n",
" return d\n",
"\n",
"\n",
"def xtrack(px, py, plan):\n",
" \"\"\"๊ณํ ๊ฒฝ๋ก(์จ์ดํฌ์ธํธ ํ๊ณก์ ) ๋๋น ์ดํ ๊ฑฐ๋ฆฌ(m).\"\"\"\n",
" P = np.asarray(plan, float)\n",
" segs = [(P[i], P[(i + 1) % len(P)]) for i in range(len(P) if len(P) > 2 else 1)]\n",
" best = np.full(len(px), np.inf)\n",
" for a, b in segs:\n",
" ab = b - a\n",
" u = np.clip(((px - a[0]) * ab[0] + (py - a[1]) * ab[1]) / max(ab @ ab, 1e-9), 0, 1)\n",
" best = np.minimum(best, np.hypot(px - a[0] - u * ab[0], py - a[1] - u * ab[1]))\n",
" return best\n",
"\n",
"\n",
"RULES = {\"turn_rate\": 45.0, \"hover_speed\": 1.5, \"hover_sec\": 20, \"dive_m\": 30.0, \"xtrack_m\": 250.0}\n",
"\n",
"\n",
"def rule_flags(d, plan, R=RULES):\n",
" \"\"\"๊ท์น ๊ธฐ๋ฐ ์ด์ ๋นํ ํ์ (๊ธฐ๋ก ๋จ์). ๊ธ์ ํยทํธ๋ฒ๋งยท๊ธ๊ฐํยท๊ฒฝ๋ก ์ดํยท๋น์ธ๊ฐ ๊ตฌ์ญ ์ง์
.\"\"\"\n",
" hr = np.abs(_wrap(np.diff(d[\"heading\"].to_numpy(float), prepend=d[\"heading\"].iloc[0])))\n",
" slow = (d[\"speed\"] < R[\"hover_speed\"]).to_numpy()\n",
" run = np.zeros(len(slow), int)\n",
" for i in range(len(slow)):\n",
" run[i] = run[i - 1] + 1 if slow[i] and i else int(slow[i])\n",
" hover = np.zeros(len(slow), bool)\n",
" for i in np.where(run >= R[\"hover_sec\"])[0]:\n",
" hover[i - R[\"hover_sec\"] + 1:i + 1] = True\n",
" alt = d[\"alt\"].to_numpy(float)\n",
" drop = alt - np.r_[alt[5:], np.full(5, alt[-1])]\n",
" return {\"๊ธ์ ํ\": hr > R[\"turn_rate\"], \"ํธ๋ฒ๋ง\": hover, \"๊ธ๊ฐํ\": drop > R[\"dive_m\"],\n",
" \"๊ฒฝ๋ก์ดํ\": xtrack(d[\"x\"].to_numpy(float), d[\"y\"].to_numpy(float), plan) > R[\"xtrack_m\"],\n",
" \"๋น์ธ๊ฐ๊ตฌ์ญ\": np.hypot(d[\"x\"] - NFZ[\"x\"], d[\"y\"] - NFZ[\"y\"]).to_numpy() < NFZ[\"r\"]}\n",
"\n",
"# ---- ๊ตฌ๊ฐ(30์ด ์ฐฝ) ํน์ง + ๋น์ง๋ ์ด์ ํ์ง ๊ณตํต ํจ์ ----\n",
"WFEAT = [\"alt_mean\", \"alt_min\", \"alt_std\", \"vz_min\", \"spd_mean\", \"spd_std\", \"spd_max\", \"turn_sum\", \"turn_max\",\n",
" \"straight\", \"rc_mean\", \"rc_zero\", \"nfz_min\", \"nfz_close\", \"xtrack_max\"]\n",
"\n",
"\n",
"def window_feats(d, plan, win=30, step=10):\n",
" \"\"\"30์ด ์ฐฝ์ 10์ด์ฉ ์ฎ๊ธฐ๋ฉฐ ๊ณ ๋ยท์๋ยท์ ํยท์ง์ง์ฑยท์กฐ์ข
์ ํธยท๋น์ธ๊ฐ ๊ตฌ์ญ ์ ๊ทผยท๊ฒฝ๋ก ์ดํ ํน์ง์ ๊ณ์ฐํ๋ค.\"\"\"\n",
" x, y, alt = d[\"x\"].to_numpy(float), d[\"y\"].to_numpy(float), d[\"alt\"].to_numpy(float)\n",
" spd, rc = d[\"speed\"].to_numpy(float), d[\"rc\"].to_numpy(float)\n",
" hr = np.abs(_wrap(np.diff(d[\"heading\"].to_numpy(float), prepend=d[\"heading\"].iloc[0])))\n",
" vz = np.diff(alt, prepend=alt[0])\n",
" nd = np.hypot(x - NFZ[\"x\"], y - NFZ[\"y\"])\n",
" xt = xtrack(x, y, plan)\n",
" step_len = np.hypot(np.diff(x, prepend=x[0]), np.diff(y, prepend=y[0]))\n",
" rows, starts = [], []\n",
" for s in range(0, len(d) - win + 1, step):\n",
" e = s + win\n",
" path = step_len[s + 1:e].sum()\n",
" rows.append([alt[s:e].mean(), alt[s:e].min(), alt[s:e].std(), vz[s:e].min(), spd[s:e].mean(), spd[s:e].std(),\n",
" spd[s:e].max(), hr[s:e].sum(), hr[s:e].max(), np.hypot(x[e - 1] - x[s], y[e - 1] - y[s]) / max(path, 1.0),\n",
" rc[s:e].mean(), (rc[s:e] < 5).mean(), nd[s:e].min(), (nd[s] - nd[e - 1]) / win, xt[s:e].max()])\n",
" starts.append(int(d[\"t\"].iloc[s]))\n",
" return np.array(rows), starts\n",
"\n",
"\n",
"class FlightAnomalyModel:\n",
" \"\"\"ํ์ ๋นํ๋ก๊ทธ(๋ผ๋ฒจ ์์)๋ก ํต์ ํจํด ๊ตฐ์ง(KMeans)๊ณผ ๊ณ ๋ฆฝ ์ฒ(IsolationForest)์ ํ์ตํ๊ณ ,\n",
" ๋ ์ ์๋ฅผ ํ์ต ๊ตฌ๊ฐ ๋ถ์์๋ก ๋๋ 1.0์ ๋์ผ๋ฉด 'ํต์ ํจํด์์ ๋ฒ์ด๋จ'์ผ๋ก ๋ณธ๋ค.\"\"\"\n",
"\n",
" def __init__(self, q=0.999, k=4):\n",
" self.q, self.k = q, k\n",
"\n",
" def fit(self, X):\n",
" self.sc = StandardScaler().fit(X)\n",
" Z = self.sc.transform(X)\n",
" self.iso = IsolationForest(n_estimators=150, random_state=0).fit(Z)\n",
" self.km = KMeans(self.k, n_init=5, random_state=0).fit(Z)\n",
" self.set_q(self.q, X)\n",
" return self\n",
"\n",
" def _raw(self, X):\n",
" Z = self.sc.transform(X)\n",
" return -self.iso.score_samples(Z), self.km.transform(Z).min(axis=1)\n",
"\n",
" def set_q(self, q, X):\n",
" a, b = self._raw(X)\n",
" self.q, self.t_if, self.t_km = q, np.quantile(a, q), np.quantile(b, q)\n",
"\n",
" def score(self, X):\n",
" a, b = self._raw(X)\n",
" return np.maximum(a / self.t_if, b / self.t_km)\n",
"\n",
"\n",
"AFEAT = [\"ml_score\", \"low_alt\", \"rc_weak\", \"nfz_min_km\", \"hover_s\", \"max_speed\", \"night\",\n",
" \"pf_commercial\", \"pf_military\", \"pf_unknown\", \"fw_stock\", \"fw_custom\", \"fw_unknown\"]\n",
"\n",
"\n",
"def attr_features(d, m, ml_score):\n",
" \"\"\"์ด์ฉ ์ฃผ์ฒด ์ถ์ ์ฉ ๋นํ ๋จ์ ํน์ง. ๊ธฐ์ฒดยทํ์จ์ด ์ ๋ณด๋ ์์ฉ ๊ธฐ์ฒด๊ฐ ๋ชจ๋ ์ฃผ์ฒด์ ์ฐ์ด๋ฏ๋ก ์ฝํ ๋จ์์ผ ๋ฟ์ด๋ค.\"\"\"\n",
" return [min(ml_score, 10.0), float((d[\"alt\"] < 60).mean()), float((d[\"rc\"] < 25).mean()),\n",
" float(np.hypot(d[\"x\"] - NFZ[\"x\"], d[\"y\"] - NFZ[\"y\"]).min() / 1000), float((d[\"speed\"] < 1.5).sum()),\n",
" float(d[\"speed\"].max()), float(m[\"weather\"] == \"night\"),\n",
" *[float(m[\"platform\"] == p) for p in (\"commercial\", \"military\", \"unknown\")],\n",
" *[float(m[\"firmware\"] == f) for f in (\"stock\", \"custom\", \"unknown\")]]\n",
"\n",
"CONFIG = {\"turn_rate\": 60, \"dive_m\": 40, \"ml_threshold\": 1.2, \"ml_quantile\": 0.995, # ๋ฏธ์
9 ์ฌ๋ณด์ ๊ฐ\n",
" \"p_high\": 0.60, \"h_max\": 0.80, \"p_med\": 0.25}\n",
"CALLED = set()\n",
"\n",
"\n",
"def cover(fn):\n",
" \"\"\"๊ธฐ๋ฅ ์ปค๋ฒ๋ฆฌ์ง ์ธก์ ์ฉ ํ์: ํตํฉ ์คํ ์ค ํธ์ถ๋ ๊ธฐ๋ฅ์ ๊ธฐ๋กํ๋ค.\"\"\"\n",
" def wrap(*a, **k):\n",
" CALLED.add(fn.__name__)\n",
" return fn(*a, **k)\n",
" wrap.__name__ = fn.__name__\n",
" return wrap\n",
"\n",
"\n",
"@cover\n",
"def validate_log(df, m):\n",
" errs = [f\"{c} ๋๋ฝ\" for c in (\"t\", \"x\", \"y\", \"alt\", \"speed\", \"heading\", \"rc\") if c not in df]\n",
" if m[\"weather\"] not in SPOOF_THR:\n",
" errs.append(\"weather ํ์ฉ๊ฐ ์๋\")\n",
" if not errs and ((df[\"speed\"] > 60).any() or (df[\"rc\"] > 100).any()):\n",
" errs.append(\"๋ฒ์ ์ค๋ฅ\")\n",
" return errs\n",
"\n",
"\n",
"@cover\n",
"def grade(p, h, q_ok):\n",
" if p >= CONFIG[\"p_high\"] and h <= CONFIG[\"h_max\"] and q_ok:\n",
" return \"High\"\n",
" return \"Medium\" if p >= CONFIG[\"p_med\"] else \"Low\"\n",
"\n",
"\n",
"@cover\n",
"def statement(fid, g, p, h, kinds):\n",
" return f\"{fid}: ์ด์ ๋นํ({kinds}), ์ ๋ ์ด์ฉ ๊ฐ๋ฅ์ฑ {g}(p={p:.2f}, ๋ถํ์ค์ฑ {h:.2f}) - ์ ๋ณด๋ถ์๊ด ๊ฒํ ํ์\"\n",
"\n",
"\n",
"class Queue:\n",
" def __init__(self):\n",
" self.items, self.log = [], []\n",
"\n",
" @cover\n",
" def submit(self, fid, g, text):\n",
" prev = self.log[-1][\"hash\"] if self.log else \"GENESIS\"\n",
" body = {\"event\": \"SUBMIT\", \"id\": fid, \"grade\": g, \"text\": text}\n",
" self.log.append({**body, \"prev\": prev, \"hash\": hashlib.sha256((prev + json.dumps(body, ensure_ascii=False, sort_keys=True)).encode()).hexdigest()})\n",
" self.items.append({**body, \"status\": \"PENDING\"})\n",
"\n",
" @cover\n",
" def verify(self):\n",
" prev = \"GENESIS\"\n",
" for r in self.log:\n",
" body = {k: v for k, v in r.items() if k not in (\"prev\", \"hash\")}\n",
" if r[\"prev\"] != prev or r[\"hash\"] != hashlib.sha256((prev + json.dumps(body, ensure_ascii=False, sort_keys=True)).encode()).hexdigest():\n",
" return False\n",
" prev = r[\"hash\"]\n",
" return True\n",
"\n",
"\n",
"clean, rule_flags, window_feats, attr_features, corrupt = map(cover, (clean, rule_flags, window_feats, attr_features, corrupt))\n",
"FlightAnomalyModel.score = cover(FlightAnomalyModel.score)\n",
"TARGETS = {\"validate_log\", \"clean\", \"rule_flags\", \"window_feats\", \"score\", \"attr_features\", \"grade\", \"statement\", \"submit\", \"verify\", \"corrupt\", \"dashboard\"}\n",
"\n",
"\n",
"def analyze(df, m, det):\n",
" d = clean(df, m[\"weather\"])\n",
" f = rule_flags(d, m[\"plan\"], {**RULES, \"turn_rate\": CONFIG[\"turn_rate\"], \"dive_m\": CONFIG[\"dive_m\"]})\n",
" s = float(det.score(window_feats(d, m[\"plan\"])[0]).max())\n",
" hits = [k for k, v in f.items() if v.any()] + ([\"ML์ด์\"] if s > CONFIG[\"ml_threshold\"] else [])\n",
" return d, hits, s\n",
"\n",
"\n",
"# ---------------- 1. ํ์ต(ํ์ ํจํด + ๊ณผ๊ฑฐ ๊ฒํ ์๋ฃ ์ฌ๋ก) ----------------\n",
"t_start = time.perf_counter()\n",
"routine = make_dataset(80, 21, anomaly_ratio=0.0) + [(df, m) for df, m in make_dataset(200, 22, anomaly_ratio=0.0) if m[\"kind\"] == \"survey\" or m[\"weather\"] == \"windy\"][:55]\n",
"det = FlightAnomalyModel(q=CONFIG[\"ml_quantile\"]).fit(np.vstack([window_feats(clean(df, m[\"weather\"]), m[\"plan\"])[0] for df, m in routine]))\n",
"past = []\n",
"for df, m in make_dataset(160, 31):\n",
" d, hits, s = analyze(df, m, det)\n",
" past.append((attr_features(d, m, s), m[\"operator\"]))\n",
"eng = make_pipeline(StandardScaler(), LogisticRegression(max_iter=2000, C=0.5)).fit([p[0] for p in past], [p[1] for p in past])\n",
"\n",
"# ---------------- 2. ์ด์ฉ ์๋ฃ ์ฒ๋ฆฌ ----------------\n",
"rng = np.random.default_rng(10)\n",
"ops = make_dataset(100, 11)\n",
"ops = [(corrupt(df, rng) if rng.random() < 0.25 else df, m) for df, m in ops]\n",
"ops[7] = (ops[7][0].drop(columns=[\"rc\"]), ops[7][1]) # ์์ ๋ก๊ทธ 1๊ฑด ์ฃผ์
(๊ฒ์ฆ ๋จ๊ณ์์ ์ฐจ๋จ๋์ด์ผ ํจ)\n",
"queue, rejected, res, ms = Queue(), {}, [], []\n",
"for df, m in ops:\n",
" errs = validate_log(df, m)\n",
" if errs:\n",
" rejected[m[\"id\"]] = errs; continue\n",
" t0 = time.perf_counter()\n",
" d, hits, s = analyze(df, m, det)\n",
" P = eng.predict_proba([attr_features(d, m, s)])[0]\n",
" ph = P[list(eng.classes_).index(\"hostile\")]; H = float(-(P * np.log(np.clip(P, 1e-9, 1))).sum() / np.log(3))\n",
" q_ok = d.attrs[\"spoof_pts\"] < 10 and d.attrs[\"missing\"] < 15\n",
" g = grade(ph, H, q_ok) if hits else \"-\"\n",
" if hits:\n",
" queue.submit(m[\"id\"], g, statement(m[\"id\"], g, ph, H, \",\".join(hits)))\n",
" ms.append((time.perf_counter() - t0) * 1000)\n",
" res.append({\"id\": m[\"id\"], \"kind\": m[\"kind\"], \"op\": m[\"operator\"], \"truth\": m[\"kind\"] in SCENARIOS, \"pred\": bool(hits), \"grade\": g, \"d\": d})\n",
"\n",
"# ---------------- 3. ์ฑ๋ฅ ํ๊ฐ ----------------\n",
"R = pd.DataFrame(res)\n",
"acc = (R.truth == R.pred).mean(); rec = (R.truth & R.pred).sum() / R.truth.sum(); far = (~R.truth & R.pred).sum() / max(R.pred.sum(), 1)\n",
"high = R[R.grade == \"High\"]; hfar = (high.op != \"hostile\").mean() if len(high) else 0.0\n",
"\n",
"# ---------------- 4. ๋์๋ณด๋ ----------------\n",
"@cover\n",
"def dashboard():\n",
" plt.style.use(\"dark_background\")\n",
" fig, ax = plt.subplots(2, 2, figsize=(12, 8))\n",
" for _, r in R[R.pred].iterrows():\n",
" ax[0, 0].plot(r.d[\"x\"], r.d[\"y\"], lw=0.8, color={\"High\": \"red\", \"Medium\": \"orange\", \"Low\": \"gray\"}[r.grade])\n",
" ax[0, 0].add_patch(plt.Circle((NFZ[\"x\"], NFZ[\"y\"]), NFZ[\"r\"], color=\"red\", alpha=.2)); ax[0, 0].set_aspect(\"equal\")\n",
" ax[0, 0].set_title(\"Flagged flights (color = attribution grade)\")\n",
" R[R.pred][\"grade\"].value_counts().reindex([\"High\", \"Medium\", \"Low\"]).fillna(0).plot.bar(ax=ax[0, 1], color=[\"red\", \"orange\", \"gray\"])\n",
" ax[0, 1].set_title(\"Review queue by grade (all pending)\")\n",
" R[R.pred][\"kind\"].value_counts().plot.barh(ax=ax[1, 0], color=\"deepskyblue\"); ax[1, 0].set_title(\"Flagged flights by true scenario\")\n",
" ax[1, 1].axis(\"off\")\n",
" ax[1, 1].text(0, .9, f\"Accuracy {acc:.1%}\\nRecall {rec:.1%}\\nFalse alarm {far:.1%}\\nQueue {len(queue.items)} pending\\nLatency {np.mean(ms):.0f} ms/flight\",\n",
" family=\"monospace\", fontsize=13, va=\"top\")\n",
" fig.suptitle(\"ISR03 integrated drone-log analysis - training data only\"); plt.tight_layout(); plt.show()\n",
"\n",
"\n",
"dashboard()\n",
"\n",
"# ---------------- 5. ์์ฒด ์ ๊ฒ(ํตํฉ ํ
์คํธ) ----------------\n",
"log_ok = queue.verify()\n",
"cov = len(CALLED & TARGETS) / len(TARGETS)\n",
"chk = {\n",
" \"์์ ๋ก๊ทธ ์ฐจ๋จ\": \"FL0007\" in rejected,\n",
" \"์ฒ๋ฆฌ ๋นํ 99๊ฑด\": len(res) == 99,\n",
" \"์ด์ ๋นํ ์ฌํ์จ 90% ์ด์\": rec >= 0.9,\n",
" \"์ ํ๋ 90% ์ด์\": acc >= 0.9,\n",
" \"์๋ ์ฑํ 0๊ฑด(์ ๋ถ ๊ฒํ ๋๊ธฐ)\": all(i[\"status\"] == \"PENDING\" for i in queue.items),\n",
" \"ํ์ง ๊ฑด ์ ๋ ๋๊ธฐ์ด ๋ฑ๋ก\": len(queue.items) == int(R.pred.sum()),\n",
" \"๊ฐ์ฌ ๋ก๊ทธ ๋ฌด๊ฒฐ์ฑ\": log_ok,\n",
" \"๋จ์ ํํ('ํ์ ') 0๊ฑด\": not any(\"ํ์ \" in i[\"text\"] for i in queue.items),\n",
" \"๋นํ 1๊ฑด 200ms ์ด๋ด\": float(np.mean(ms)) < 200,\n",
" \"๊ธฐ๋ฅ ์ปค๋ฒ๋ฆฌ์ง 85% ์ด์\": cov >= 0.85,\n",
"}\n",
"passed = sum(chk.values())\n",
"for k, v in chk.items():\n",
" print(f\"[{'PASS' if v else 'FAIL'}] {k}\")\n",
"print(f\"์์ฒด ์ ๊ฒ {passed}/{len(chk)} ํต๊ณผ, ๊ธฐ๋ฅ ์ปค๋ฒ๋ฆฌ์ง {cov:.0%} ({len(CALLED & TARGETS)}/{len(TARGETS)}), ํ์ดํ๋ผ์ธ ์คํ ์ฑ๊ณต {'์' if passed == len(chk) else '์๋์ค'}\")\n",
"\n",
"# ---------------- 6. ISR ์ ๋ณดํ๋จ ๋ธ๋ฆฌํ ์์ฝ ----------------\n",
"print(f\"\"\"\n",
"[๋๋ก ๋นํ๋ก๊ทธ ์ด์ ํ์ง ๋ธ๋ฆฌํ ์์ฝ - ๊ต์ก์ฉ๊ฐ์์๋ฃ]\n",
"1. ์ฒ๋ฆฌ: ๋นํ๋ก๊ทธ {len(ops)}๊ฑด ์ค {len(res)}๊ฑด ๋ถ์, ์์ ๋ก๊ทธ {len(rejected)}๊ฑด ์ฐจ๋จ({', '.join(rejected)})\n",
"2. ์ฑ๋ฅ: ์ ํ๋ {acc:.1%}, ์ด์ ์ฌํ์จ {rec:.1%}, ์ค๊ฒฝ๋ณด์จ {far:.1%}, ๋นํ๋น {np.mean(ms):.0f} ms, ์ ์ฒด {time.perf_counter() - t_start:.0f} s\n",
"3. ๊ฒํ ๋๊ธฐ: {len(queue.items)}๊ฑด (High {int((R.grade == 'High').sum())} / Medium {int((R.grade == 'Medium').sum())} / Low {int((R.grade == 'Low').sum())}), ์๋ ์ฑํ 0๊ฑด\n",
"4. ์ด์ฉ ์ฃผ์ฒด: High ๋ฑ๊ธ {len(high)}๊ฑด ์ค ์ค์ ๋น์ ๋ {int((high.op != 'hostile').sum())}๊ฑด({hfar:.0%}) -> ๋ฑ๊ธ์ ๊ฒํ ์ฐ์ ์์์ผ ๋ฟ ๊ท์ ๊ฒฐ๋ก ์ด ์๋\n",
"5. ํ๊ณ: ํฉ์ฑ ๋ก๊ทธ ๊ธฐ์ค ๊ฒฐ๊ณผ์ด๋ฉฐ, ์ด์ฉ ์ฃผ์ฒด ํ๋จ์ ์ถ์ฒยท๋ฌด๊ฒฐ์ฑ ํ์ธ๊ณผ 2์ธ ๊ต์ฐจ ๊ฒํ ๋ฅผ ๊ฑฐ์น ์ ๋ณด๋ถ์๊ด ์๊ฒฌ์ผ๋ก๋ง ๋ณด๊ณ ํ๋ค.\"\"\")\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"๐ก [์ฐธ๊ณ ] ๋ฏธ์
10 ์์ ์คํ ๊ฒฐ๊ณผ (Output)
\n",
"\n",
"```text\n",
"[PASS] ์์ ๋ก๊ทธ ์ฐจ๋จ\n",
"[PASS] ์ฒ๋ฆฌ ๋นํ 99๊ฑด\n",
"[PASS] ์ด์ ๋นํ ์ฌํ์จ 90% ์ด์\n",
"[PASS] ์ ํ๋ 90% ์ด์\n",
"[PASS] ์๋ ์ฑํ 0๊ฑด(์ ๋ถ ๊ฒํ ๋๊ธฐ)\n",
"[PASS] ํ์ง ๊ฑด ์ ๋ ๋๊ธฐ์ด ๋ฑ๋ก\n",
"[PASS] ๊ฐ์ฌ ๋ก๊ทธ ๋ฌด๊ฒฐ์ฑ\n",
"[PASS] ๋จ์ ํํ('ํ์ ') 0๊ฑด\n",
"[PASS] ๋นํ 1๊ฑด 200ms ์ด๋ด\n",
"[PASS] ๊ธฐ๋ฅ ์ปค๋ฒ๋ฆฌ์ง 85% ์ด์\n",
"์์ฒด ์ ๊ฒ 10/10 ํต๊ณผ, ๊ธฐ๋ฅ ์ปค๋ฒ๋ฆฌ์ง 100% (12/12), ํ์ดํ๋ผ์ธ ์คํ ์ฑ๊ณต ์\n",
"\n",
"[๋๋ก ๋นํ๋ก๊ทธ ์ด์ ํ์ง ๋ธ๋ฆฌํ ์์ฝ - ๊ต์ก์ฉ๊ฐ์์๋ฃ]\n",
"1. ์ฒ๋ฆฌ: ๋นํ๋ก๊ทธ 100๊ฑด ์ค 99๊ฑด ๋ถ์, ์์ ๋ก๊ทธ 1๊ฑด ์ฐจ๋จ(FL0007)\n",
"2. ์ฑ๋ฅ: ์ ํ๋ 96.0%, ์ด์ ์ฌํ์จ 98.0%, ์ค๊ฒฝ๋ณด์จ 5.8%, ๋นํ๋น 31 ms, ์ ์ฒด 12 s\n",
"3. ๊ฒํ ๋๊ธฐ: 52๊ฑด (High 7 / Medium 22 / Low 23), ์๋ ์ฑํ 0๊ฑด\n",
"4. ์ด์ฉ ์ฃผ์ฒด: High ๋ฑ๊ธ 7๊ฑด ์ค ์ค์ ๋น์ ๋ 2๊ฑด(29%) -> ๋ฑ๊ธ์ ๊ฒํ ์ฐ์ ์์์ผ ๋ฟ ๊ท์ ๊ฒฐ๋ก ์ด ์๋\n",
"5. ํ๊ณ: ํฉ์ฑ ๋ก๊ทธ ๊ธฐ์ค ๊ฒฐ๊ณผ์ด๋ฉฐ, ์ด์ฉ ์ฃผ์ฒด ํ๋จ์ ์ถ์ฒยท๋ฌด๊ฒฐ์ฑ ํ์ธ๊ณผ 2์ธ ๊ต์ฐจ ๊ฒํ ๋ฅผ ๊ฑฐ์น ์ ๋ณด๋ถ์๊ด ์๊ฒฌ์ผ๋ก๋ง ๋ณด๊ณ ํ๋ค.\n",
"```\n",
" \n"
]
}
],
"metadata": {
"colab": {
"provenance": []
},
"kernelspec": {
"display_name": "Python 3",
"name": "python3"
},
"language_info": {
"name": "python"
}
},
"nbformat": 4,
"nbformat_minor": 0
}