{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# ๐ฏ [MOV06] ๋ฉํ์์ ์์ธกยท๋ฌผ๋ฅํ๋ฆ ์ถ์ ๊ด๋ฆฌ\n",
"### Google Colab ์ค์ต ํ๊ฒฝ (์์กยท๋ฌผ๋ฅยท์ฐฝ๊ณ / Level 3)\n",
"\n",
"[](https://colab.research.google.com/github/edunlab/milicontent2/blob/main/uploads/MOV06/MOV06_solution.ipynb)\n",
"\n",
"---\n",
"\n",
"## ๐ 1. ํ๋ก์ ํธ ๊ฐ์ ๋ฐ ๋ฐฐ๊ฒฝ\n",
"\n",
"| ํญ๋ชฉ | ๋ด์ฉ |\n",
"| :--- | :--- |\n",
"| **๊ณผ์ ๋ถ๋ฅ** | ์์กยท๋ฌผ๋ฅยท์ฐฝ๊ณ (Level 3) |\n",
"| **ํ์ต ์์ ์๊ฐ** | 10๊ฐ ๋ฏธ์
(5์ผ / ์ด 28.5์๊ฐ) |\n",
"| **ํ๊ฒฝ ์ฌ์** | Python 3.10+, numpy, pandas, matplotlib, scipy, scikit-learn |\n",
"\n",
"### ๐ ํ๋ก์ ํธ ๋ชฉํ\n",
"๋ฉํ๋ฌผ๋ฅ์์๋ ์๋ น๋ถ๋์ ๋ฉํ ์์๋ฅผ ์์ธกํ๋ ์ผ๊ณผ ์์ก์ ๋จ์ด ์ง๊ธ ์ด๋์ ์๊ณ ์ธ์ ๋์ฐฉํ๋์ง ์ถ์ ํ๋ ์ผ์ด ๋ฐ๋ก ์ด๋ฃจ์ด์ง๋ฉด ์ง์ฐ์ ๋ค๋ฆ๊ฒ ์๊ฒ ๋๋ค. ๋ณธ ํ๋ก์ ํธ๋ ๋ฉํ์์ ์์ธก๊ณผ ๋ฌผ๋ฅํ๋ฆ ์ถ์ ์ ๊ฒฐํฉํด end-to-end ๋ฌผ๋ฅ ๊ฐ์์ฑ์ ์ตํ๋๋ก, ๊ต์ก์ฉ ๊ฐ์ ๋
ธ์ ์์ ์ ๋จ์ GPS ์์นยทํ๋ฌผ ์ผ์ยทํต์ ์ํ ํ
๋ ๋ฉํธ๋ฆฌ๋ฅผ ์ฝ๋๋ก ๋ง๋ค์ด ๋์ฐฉ ์์ ์๊ฐ(ETA)๊ณผ ์ง์ฐ ์์ธ์ ์ถ์ ํ๋ค. ํ์ต์๋ ํ
๋ ๋ฉํธ๋ฆฌ ๋ฐ์ดํฐ์
์ ์, GPS ์ด์๊ฐ ์ ์ , ํ์ฌ ์๋ ๊ธฐ๋ฐ ETA ๊ธฐ์ค์ , ๋๋กยท๊ธฐ์ยท์ ์ฒด๋ฅผ ๋ฐ์ํ ETA ๋ชจ๋ธ, ์ดํยท๋ถ์ ์ ์ฐจ ๊ฒฝ๋ณด, ๊ด์ ๋์๋ณด๋์ ์ง์ฐ ์์ธ ์ค๋ช
, ํ์ฅ ํ์ธยท๋์ฒด ์์ก ์น์ธ(HITL), 100๊ฐ ์ ๋จ ์คํธ๋ฆผ ๋ถํ ์ํ, AAR ๊ธฐ๋ฐ ๊ฐ์ค์น ์ฌํ๋๊ณผ ROI, ์ฌํ์ต ํ์ดํ๋ผ์ธ ํตํฉ์ ์ฐจ๋ก๋ก ์ํํ๋ค. ์ต์ข
๋ชฉํ๋ ์ ๋จ ์ด๋ ์ํ๋ฅผ ์ถ์ ํด ๋์ฐฉ ์๊ฐ๊ณผ ์ง์ฐ ์์ธ์ ์ค๋ช
ํ๊ณ 7์ผ ๋ฉํ ์์๊น์ง ์์ธกํ๋, ๋์ฒด ์์ก ์ง์๋ ๋ฐ๋์ ์ฌ๋์ด ๊ฒฐ์ ํ๋ ๋ฌผ๋ฅ ์ถ์ ๋๊ตฌ๋ฅผ ์์ฑํ๋ ๊ฒ์ด๋ค.\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 ์์น, ํ๋ฌผ ์ผ์, ํต์ ์ํ ํ
๋ ๋ฉํธ๋ฆฌ ๋ฐ์ดํฐ์
์ ์\n",
"**์์์๊ฐ**: ์ฝ 1.5์๊ฐ\n",
"\n",
"### ๐ ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ์ํ ์ง์นจ\n",
"#### ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ๋น์์ฑ\n",
"\n",
"๊ตฐ์ ์์ก์ ๋จ GPS ์์น, ํ๋ฌผ ์ผ์, ํ
๋ ๋ฉํธ๋ฆฌ ๋ฐ์ดํฐ์
์ ์ ์ํ๋ค. ์ค์ ์ ์ ํ์ฅ์์๋ ๋จ์ ํ์ง์ ๊ทธ์น์ง ์๊ณ ์ ์ํ ์ํ ํ๋จ๊ณผ ์ ๋ขฐ๋ ๋์ ์์ฌ๊ฒฐ์ ์ด ํ์์ ์ด๋ฏ๋ก, **๊ฐ์ ์์ก ๋
ธ์ ์ ์ด๋ํ๋ ์ ๋จ์ GPS ์์น, ํ๋ฌผ ์ผ์, ํต์ ์ํ ํ
๋ ๋ฉํธ๋ฆฌ ๋ฐ์ดํฐ์
์ ์ ์ํ๊ณ ๋ฉํ ๋ฌผ๋ฅ ์ถ์ ์
๋ฌด ํ๋ก์ธ์ค ๋งต๊ณผ ํผ์ฒ ๋ฐ์ดํฐ ์ฌ์ ์ ๋ง๋ ๋ค.** ๊ณผ์
์ ์์ํ์ฌ ์คํ์ ๋ฐฉ์งํ๊ณ ์์ ๋์๋ ฅ์ ๊ทน๋ํํด์ผ ํฉ๋๋ค.\n",
"\n",
"์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ณธ ๋ฏธ์
์์๋ **์ ๋จ ์๋ณ, ์๊ฐ, ์ขํ, ์๋, ๋ฐฉํฅ, ํต์ ๊ฐ๋, ํ๋ฌผ ์จ๋ยท์ถฉ๊ฒฉ, ์ ๋จ ๊ท๋ชจ, ํ๋ฌผ ๊ตฌ๋ถ, ์ฐ์ฒ ์ฌ๋ถ์ ๋ชจ์ ์ ๋ต ๋ผ๋ฒจ์ ํ๋์ ํ์ฉ๊ฐ์ ์ ์ํํ๊ณ ๊ฒ์ฆํ๋** ์ค์ต์ ์ฒด๊ณ์ ์ผ๋ก ์ํํฉ๋๋ค.\n",
"\n",
"ํ์ต์๋ ์ค์ต๊ฐ์ด๋์ ์ ์๋ ์ด 4๊ฐ ์ธ๋ถ ํ๋(๊ธฐ๋ณธ ๊ธฐ๋ฅ ๊ตฌํ โ ๊ฒฐ๊ณผ ์งํ ๊ฒ์ฆ โ ํ๋ผ๋ฏธํฐ ํ๋ ๋ฐ ๋์ ๊ณผ์ )์ ์์ฐจ์ ์ผ๋ก ์งํํ๋ฉฐ ํต์ฌ ํ์ ๊ธฐ์ค์ ๋ฌ์ฑํฉ๋๋ค.\n",
"\n",
"**์ด๋ฒ ๊ฒฐ๊ณผ๋ฌผ:** `docs/mov06_logistics_domain_spec.md ๋ฐ ๋ฐ์ดํฐ ๋์
๋๋ฆฌ` ํํ์ ๊ฒฐ๊ณผ๋ฌผ์ ์์ฑํ๋ค.\n",
"\n",
"### ๐ ํ๊ฐ ๋ฐ ๊ฒ์ฆ ๊ธฐ์ค (PASS ์กฐ๊ฑด)\n",
"#### ๋ฏธ์
ํ์ \n",
"\n",
"**ํ๊ฐ ๊ตฌ๋ถ:** ํ์\n",
"\n",
"**ํต์ฌ ํ๊ฐ์์ญ:** ๊ตฐ์ ์ค๋ฌด ์
๋ฌด ํ๋ก์ธ์ค์์ ์ผ์น์ฑ ๋ฐ ๋ถ์ ๋ชฉํ์ ๋ช
ํ์ฑ\n",
"\n",
"#### ๋ฏธ์
PASS ์กฐ๊ฑด\n",
"\n",
"- ์ ๋ ๊ธฐ์ค: ์
๋ฌด ํ๋ก์ธ์ค ๋งต ์์ฑ ์๋ฃ, ํ์ ํผ์ฒ(Feature) ๋ชฉ๋ก 20๊ฐ ์ด์ ์ ์\n",
"\n",
"\n",
"- ์งํ ๊ธฐ์ค: ๋๋ฉ์ธ ๋ถ์์ ๋ฐ ๋ฐ์ดํฐ ์๊ตฌ๋ช
์ธ์ ์น์ธ\n",
"\n",
"\n",
"- ๊ฒ์ฆ ๋ฐฉ๋ฒ: ๊ตฐ์ ๋ด๋น ์ค๋ฌด์ ์ธํฐ๋ทฐ ๋ฐ ๊ธฐ์กด ์ ์ฐ์ฒด๊ณ ๋ฐ์ดํฐ ๊ตฌ์กฐ ๋์กฐ\n",
"\n",
"\n",
"#### ์ฑ์ ์ ํ์ ๋ฉ๋ชจ\n",
"\n",
"> ๋จ์ ์ฝ๋ ์คํ ์ฌ๋ถ๋ฟ๋ง ์๋๋ผ ์ถ๋ ฅ๋ ์์น์ ๊ทผ๊ฑฐ ์๋ฃ๊ฐ ๊ตฐ ์ค๋ฌด ์๊ตฌ์ฌํญ์ ๋ถํฉํ๋์ง ๋ฉด๋ฐํ ํ์ธํ๋ค.\n",
"\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"\"\"\"๊ตฐ์ ์์ก์ ๋จ์ GPS ์์นยทํ๋ฌผ ์ผ์ยทํต์ ์ํ ํ
๋ ๋ฉํธ๋ฆฌ ๋ฐ์ดํฐ์
์ ์ ์ํ๊ณ , ํ์ฉ๊ฐ ๊ฒ์ฆ๊ธฐ์ ์ฃผ์
์ค๋ฅ ์ํ, ์ค์ ์์ฑ ๋ฐ์ดํฐ์์ ๋์กฐ, ๋ฉํ ๋ฌผ๋ฅ ํ๋ฆ ์ถ์ ์
๋ฌด ํ๋ก์ธ์ค ๋งต(20๊ฐ ์ด์ ํผ์ฒ ๋ชฉ๋ก ํฌํจ)์ ๋ง๋ ๋ค.\"\"\"\n",
"import copy\n",
"import numpy as np\n",
"import pandas as pd\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# [๊ต์ก์ฉ๊ฐ์์๋ฃ] ๊ฐ์ ์์ก ๋
ธ์ ยท์์ก์ ๋จ ํ
๋ ๋ฉํธ๋ฆฌ ์์ฑ๊ธฐ (์ค์ ๋๋กยท์ง๋ช
ยท๋ถ๋ ์์)\n",
"# ๊ฐ์ ๋
ธ์ (์ถ๋ฐ ๊ฑฐ์ -> ๋์ฐฉ ๊ฑฐ์ , 5๊ฐ ๊ตฌ๊ฐ)์ 5๋ถ ๊ฐ๊ฒฉ์ผ๋ก ๋ณด๊ณ ํ๋ ์ ๋จ GPSยทํ๋ฌผ ์ผ์ยทํต์ ์ํ ์๋ฃ๋ฅผ ๋ง๋ ๋ค.\n",
"# ---------------------------------------------------------------------------\n",
"PTS = [(0, 0), (30, 12), (62, 14), (88, 38), (112, 42), (135, 66)] # ๋
ธ์ ๊ผญ์ง์ (km, ๊ฐ์ ์ขํ)\n",
"ROUTE = pd.DataFrame({\"seg\": [\"R1\", \"R2\", \"R3\", \"R4\", \"R5\"], \"road\": [\"paved\", \"unpaved\", \"paved\", \"paved\", \"unpaved\"],\n",
" \"limit\": [70, 50, 70, 50, 50], \"kind\": [\"์ผ๋ฐ\", \"์ผ๋ฐ\", \"์ผ๋ฐ\", \"๊ต๋ยทํฐ๋\", \"์ผ๋ฐ\"], \"threat\": [0, 0, 1, 0, 0]})\n",
"ROUTE[\"x0\"], ROUTE[\"y0\"] = [p[0] for p in PTS[:-1]], [p[1] for p in PTS[:-1]]\n",
"ROUTE[\"x1\"], ROUTE[\"y1\"] = [p[0] for p in PTS[1:]], [p[1] for p in PTS[1:]]\n",
"ROUTE[\"km\"] = np.hypot(ROUTE[\"x1\"] - ROUTE[\"x0\"], ROUTE[\"y1\"] - ROUTE[\"y0\"])\n",
"ROUTE[\"c0\"] = np.r_[0, np.cumsum(ROUTE[\"km\"])[:-1]]\n",
"ROUTE[\"c1\"] = np.cumsum(ROUTE[\"km\"])\n",
"LEN = float(ROUTE[\"km\"].sum())\n",
"ROAD_F = {\"paved\": 1.0, \"unpaved\": 0.75}\n",
"C1L, C0L, KML = ROUTE[\"c1\"].tolist(), ROUTE[\"c0\"].tolist(), ROUTE[\"km\"].tolist() # ๋ฐ๋ณต ๊ณ์ฐ์ฉ ๋ฆฌ์คํธ(์๋)\n",
"BASE_V = [l * ROAD_F[r] for l, r in zip(ROUTE[\"limit\"], ROUTE[\"road\"])]\n",
"THR, UNP, TUN = ROUTE[\"threat\"].tolist(), (ROUTE[\"road\"] == \"unpaved\").tolist(), (ROUTE[\"kind\"] == \"๊ต๋ยทํฐ๋\").tolist()\n",
"\n",
"\n",
"def seg_of(s):\n",
" # [TODO : ํ์ ์ค์ต 1] ๊ตฐ์ ์์ก์ ๋จ GPS ์์น, ํ๋ฌผ ์ผ์, ํต์ ์ํ ํ
๋ ๋ฉํธ๋ฆฌ ๋ฐ์ดํฐ์
์ ์ ๊ด๋ จ ํต์ฌ ๋ก์ง์ ๊ตฌํํ์ธ์.\n",
" # ํํธ: ์๋จ ์ฃผ์ ๋ฐ ๋ฐ์ดํฐ ์คํค๋ง/์ง์นจ์ ์ฐธ๊ณ ํ์ฌ ์์ฑํฉ๋๋ค.\n",
" # <<< ์ฌ๊ธฐ์ ์ฝ๋๋ฅผ ์์ฑํ์ธ์ >>>\n",
" pass # ์์ฑ ํ pass๋ฅผ ์ ๊ฑฐํ์ธ์.\n",
"def route_xy(s):\n",
" \"\"\"๋
ธ์ ์ ์งํ๊ฑฐ๋ฆฌ s(km)์ ์ขํ์ ์งํ ๋ฐฉํฅ(๋).\"\"\"\n",
" # [TODO : ํ์ ์ค์ต 1] ๊ตฐ์ ์์ก์ ๋จ GPS ์์น, ํ๋ฌผ ์ผ์, ํต์ ์ํ ํ
๋ ๋ฉํธ๋ฆฌ ๋ฐ์ดํฐ์
์ ์ ๊ด๋ จ ํต์ฌ ๋ก์ง์ ๊ตฌํํ์ธ์.\n",
" # ํํธ: ์๋จ ์ฃผ์ ๋ฐ ๋ฐ์ดํฐ ์คํค๋ง/์ง์นจ์ ์ฐธ๊ณ ํ์ฌ ์์ฑํฉ๋๋ค.\n",
" # <<< ์ฌ๊ธฐ์ ์ฝ๋๋ฅผ ์์ฑํ์ธ์ >>>\n",
" pass # ์์ฑ ํ pass๋ฅผ ์ ๊ฑฐํ์ธ์.\n",
"def project(x, y):\n",
" \"\"\"GPS ์ขํ๋ฅผ ๋
ธ์ ์ ํฌ์ํด (์งํ๊ฑฐ๋ฆฌ km, ๋
ธ์ ๊ณผ์ ์์ง ์ดํ km)๋ฅผ ๊ตฌํ๋ค.\"\"\"\n",
" x, y = np.atleast_1d(x).astype(float), np.atleast_1d(y).astype(float)\n",
" best_s, best_d = np.zeros(len(x)), np.full(len(x), np.inf)\n",
" for r in ROUTE.itertuples():\n",
" dx, dy = r.x1 - r.x0, r.y1 - r.y0\n",
" f = np.clip(((x - r.x0) * dx + (y - r.y0) * dy) / (dx * dx + dy * dy), 0, 1)\n",
" d = np.hypot(x - (r.x0 + f * dx), y - (r.y0 + f * dy))\n",
" better = d < best_d\n",
" best_s, best_d = np.where(better, r.c0 + f * r.km, best_s), np.where(better, d, best_d)\n",
" return best_s, best_d\n",
"\n",
"\n",
"def cong(clock_min):\n",
" h = (clock_min / 60) % 24\n",
" return 0.82 if (7 <= h < 9 or 17 <= h < 19) else 1.0 # ์ถํด๊ทผ ์๊ฐ๋ ์ ์ฒด ๊ณ์(๊ฐ์ )\n",
"\n",
"\n",
"def simulate_trip(rng, tid, start_min, rain, size, cargo):\n",
" \"\"\"1๋ถ ๋จ์๋ก ์ด๋์ ๋ชจ์ํ๊ณ 5๋ถ๋ง๋ค ํ
๋ ๋ฉํธ๋ฆฌ๋ฅผ ๋จ๊ธด๋ค. ์ดํยท๋ถ์ ์ ์ฐจ ์ด๋ฒคํธ๋ ์ ๋ต ๋ผ๋ฒจ๋ก ํจ๊ป ์ ์ฅํ๋ค.\"\"\"\n",
" s, t, stop_left, rows = 0.0, 0, 0, []\n",
" dev = (rng.random() < 0.12)\n",
" dev_t0 = int(rng.integers(30, 120)) if dev else -1\n",
" dev_len = int(rng.integers(12, 25)) if dev else 0\n",
" cause = \"\"\n",
" while s < LEN:\n",
" i = seg_of(s)\n",
" v = BASE_V[i] * (0.88 if rain else 1.0) * (0.85 if (rain and UNP[i]) else 1.0) * cong(start_min + t) \\\n",
" * (0.7 if THR[i] else 1.0) * (1 - 0.012 * size) * rng.normal(1, 0.06)\n",
" off = 0.0\n",
" if dev and dev_t0 <= t < dev_t0 + dev_len:\n",
" ph = (t - dev_t0) / dev_len\n",
" off, v = 4.0 * (1 - abs(2 * ph - 1)), v * 0.4 # ๋
ธ์ ์ดํ(์ผ๊ฐํ ํํ๋ก ์ต๋ 4km)\n",
" if stop_left > 0:\n",
" v, stop_left = 0.0, stop_left - 1\n",
" elif rng.random() < 0.0015:\n",
" stop_left, cause = int(rng.integers(8, 36)), (\"maint\" if rng.random() < 0.7 else \"urgent\")\n",
" s += v / 60\n",
" t += 1\n",
" if t % 5 == 0 or s >= LEN:\n",
" x, y, hd = route_xy(min(s, LEN))\n",
" nx, ny = np.cos(np.radians(hd)), -np.sin(np.radians(hd))\n",
" j = seg_of(min(s, LEN)); tun = TUN[j]\n",
" rows.append(dict(trip=tid, t=t, clock=(start_min + t) % 1440, x=x + off * nx + rng.normal(0, 0.03), y=y + off * ny + rng.normal(0, 0.03),\n",
" speed=max(v * rng.normal(1, 0.03), 0.0), heading=(hd + rng.normal(0, 3)) % 360, rssi=-62 - 14 * tun + rng.normal(0, 3),\n",
" cargo_temp=18 + 3 * np.sin(t / 40) + rng.normal(0, 0.3), shock_g=abs(rng.normal(0.25, 0.1)) * (2.0 if UNP[j] else 1.0),\n",
" s_true=min(s, LEN), off_true=off, stopped_true=int(v == 0.0), rain=int(rain), size=size, cargo=cargo, stop_cause=cause if v == 0.0 else \"\"))\n",
" arr = t\n",
" for r in rows:\n",
" r[\"eta_true\"] = arr - r[\"t\"]\n",
" return rows\n",
"\n",
"\n",
"def make_telemetry(seed=21, n_trips=200):\n",
" \"\"\"n_trips๊ฐ ์์ก์ ๋จ ์ด๋ ๊ธฐ๋ก(๋ ์ง ์). ์ ๋จ ๊ท๋ชจ 3~12๋, ํ๋ฌผ ๊ตฌ๋ถ ์ผ๋ฐ/๋์ฅ/๋ฏผ๊ฐ.\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" rows = []\n",
" for k in range(n_trips):\n",
" rows += simulate_trip(rng, k, int(rng.integers(5 * 60, 15 * 60)), bool(rng.random() < 0.2), int(rng.integers(3, 13)), str(rng.choice([\"์ผ๋ฐ\", \"๋์ฅ\", \"๋ฏผ๊ฐ\"], p=[.6, .25, .15])))\n",
" return pd.DataFrame(rows)\n",
"\n",
"\n",
"def mape(y, p):\n",
" y, p = np.asarray(y, float), np.asarray(p, float)\n",
" return float(np.mean(np.abs(y - p) / np.abs(y)) * 100)\n",
"\n",
"\n",
"def mae(y, p):\n",
" return float(np.mean(np.abs(np.asarray(y, float) - np.asarray(p, float))))\n",
"# ---------------------------------------------------------------------------\n",
"# ---------------------------------------------------------------------------\n",
"# ETA ์์ธก ํผ์ฒ: ๊ด์ธก ์์ ๊น์ง์ ํ
๋ ๋ฉํธ๋ฆฌ์ ๋จ์ ๋
ธ์ ๊ตฌ์ฑ(ํฌ์ฅยท์ํยท๊ต๋ํฐ๋ ๊ตฌ๊ฐ ๊ฑฐ๋ฆฌ)๋ง ์ฌ์ฉ\n",
"# ---------------------------------------------------------------------------\n",
"FEATS = [\"remaining_km\", \"progress\", \"speed\", \"speed_ma3\", \"speed_std3\", \"stop_n\", \"stop_min\", \"since_stop\", \"hour_sin\", \"hour_cos\", \"cong\", \"rain\", \"size\",\n",
" \"k_๋์ฅ\", \"k_๋ฏผ๊ฐ\", \"rem_unpaved_km\", \"rem_threat_km\", \"rem_bridge_km\", \"off_km\", \"rssi\", \"base_eta\"]\n",
"\n",
"\n",
"def rem_composition(s):\n",
" \"\"\"์งํ๊ฑฐ๋ฆฌ s ์ดํ ๋จ์ ๊ตฌ๊ฐ ์ค ๋นํฌ์ฅยท์ํยท๊ต๋ํฐ๋ ๊ฑฐ๋ฆฌ(km).\"\"\"\n",
" left = np.clip(ROUTE[\"c1\"].values[None, :] - np.maximum(np.atleast_1d(s)[:, None], ROUTE[\"c0\"].values[None, :]), 0, None)\n",
" return left @ np.array(UNP, float), left @ np.array(THR, float), left @ np.array(TUN, float)\n",
"\n",
"\n",
"def add_features(df):\n",
" d = df.sort_values([\"trip\", \"t\"]).copy()\n",
" d[\"s_obs\"], d[\"off_km\"] = project(d[\"x\"].values, d[\"y\"].values)\n",
" d[\"remaining_km\"] = LEN - d[\"s_obs\"]\n",
" d[\"progress\"] = d[\"s_obs\"] / LEN\n",
" g = d.groupby(\"trip\")[\"speed\"]\n",
" d[\"speed_ma3\"] = g.rolling(3, min_periods=1).mean().reset_index(level=0, drop=True)\n",
" d[\"speed_std3\"] = g.rolling(3, min_periods=1).std().reset_index(level=0, drop=True).fillna(0)\n",
" d[\"is_stop\"] = (d[\"speed\"] < 3).astype(int)\n",
" new_stop = (d[\"is_stop\"] == 1) & (d.groupby(\"trip\")[\"is_stop\"].shift(1).fillna(0) == 0)\n",
" d[\"stop_n\"] = new_stop.astype(int).groupby(d[\"trip\"]).cumsum()\n",
" d[\"stop_min\"] = d.groupby(\"trip\")[\"is_stop\"].cumsum() * 5\n",
" blk = (d[\"is_stop\"] == 1).astype(int).groupby(d[\"trip\"]).cumsum() # ์ ์ฐจ ๋์ ํ์๋ก ๊ตฌ๊ฐ์ ๋๋์ด ๋ง์ง๋ง ์ ์ฐจ ์ดํ ๊ฒฝ๊ณผ ๊ณ์ฐ\n",
" d[\"since_stop\"] = d.groupby([\"trip\", blk]).cumcount() * 5\n",
" d[\"hour_sin\"], d[\"hour_cos\"] = np.sin(2 * np.pi * d[\"clock\"] / 1440), np.cos(2 * np.pi * d[\"clock\"] / 1440)\n",
" d[\"cong\"] = d[\"clock\"].map(cong)\n",
" d[\"k_๋์ฅ\"], d[\"k_๋ฏผ๊ฐ\"] = (d[\"cargo\"] == \"๋์ฅ\").astype(int), (d[\"cargo\"] == \"๋ฏผ๊ฐ\").astype(int)\n",
" d[\"rem_unpaved_km\"], d[\"rem_threat_km\"], d[\"rem_bridge_km\"] = rem_composition(d[\"s_obs\"].values)\n",
" d[\"base_eta\"] = np.minimum(d[\"remaining_km\"] / np.maximum(d[\"speed\"], 5.0) * 60, 600) # ํ์ฌ ์๋ ๊ธฐ๋ฐ ๋จ์ ETA(๋ถ, ์ ์ฐจ ์ ํญ์ฃผ ๋ฐฉ์ง๋ก 5km/h ํํยท600๋ถ ์ํ)\n",
" return d.reset_index(drop=True)\n",
"# ---------------------------------------------------------------------------\n",
"\n",
"# 1) ๋ฐ์ดํฐ ์ฌ์ : (ํ๋, ๊ตฌ๋ถ, ํ, ํ์ฉ ๋ฒ์/๊ฐ, ์ค๋ช
)\n",
"DICT = [(\"trip\", \"์๋ณ\", \"int\", \"0~\", \"์์ก์ ๋จ ์ด๋ ๋ฒํธ(๋ ์ง ์)\"), (\"t\", \"GPS\", \"int\", \"5~720\", \"์ถ๋ฐ ํ ๊ฒฝ๊ณผ ์๊ฐ(๋ถ)\"), (\"clock\", \"GPS\", \"int\", \"0~1439\", \"๋ณด๊ณ ์๊ฐ(0์๋ถํฐ ๋ถ)\"),\n",
" (\"x\", \"GPS\", \"float\", \"-10~160\", \"๊ฐ์ ์ขํ x(km)\"), (\"y\", \"GPS\", \"float\", \"-10~90\", \"๊ฐ์ ์ขํ y(km)\"), (\"speed\", \"GPS\", \"float\", \"0~120\", \"๋ณด๊ณ ์๋(km/h)\"),\n",
" (\"heading\", \"GPS\", \"float\", \"0~360\", \"์งํ ๋ฐฉํฅ(๋)\"), (\"rssi\", \"ํต์ \", \"float\", \"-110~-40\", \"ํต์ ์์ ๊ฐ๋(dBm)\"), (\"cargo_temp\", \"ํ๋ฌผ์ผ์\", \"float\", \"-5~40\", \"ํ๋ฌผ์นธ ์จ๋(C)\"),\n",
" (\"shock_g\", \"ํ๋ฌผ์ผ์\", \"float\", \"0~5\", \"์ถฉ๊ฒฉ ๊ฐ์๋(g)\"), (\"rain\", \"๋งฅ๋ฝ\", \"int\", \"0|1\", \"์ฐ์ฒ ์ฌ๋ถ\"), (\"size\", \"๋งฅ๋ฝ\", \"int\", \"3~12\", \"์ ๋จ ์ฐจ๋ ์\"),\n",
" (\"cargo\", \"๋งฅ๋ฝ\", \"str\", \"์ผ๋ฐ|๋์ฅ|๋ฏผ๊ฐ\", \"ํ๋ฌผ ๊ตฌ๋ถ\"), (\"s_true\", \"๋ผ๋ฒจ\", \"float\", \"0~LEN\", \"์ค์ ์งํ๊ฑฐ๋ฆฌ(๋ชจ์ ์ ๋ต)\"), (\"off_true\", \"๋ผ๋ฒจ\", \"float\", \"0~\", \"์ค์ ๋
ธ์ ์ดํ๊ฑฐ๋ฆฌ(๋ชจ์ ์ ๋ต)\"),\n",
" (\"stopped_true\", \"๋ผ๋ฒจ\", \"int\", \"0|1\", \"์ค์ ์ ์ฐจ ์ฌ๋ถ(๋ชจ์ ์ ๋ต)\"), (\"stop_cause\", \"๋ผ๋ฒจ\", \"str\", \"maint|urgent|''\", \"์ ์ฐจ ์์ธ(๋น๊ณํ ์ ๋น/๊ธด๊ธ ๋ถ์ถ ๋๊ธฐ)\"), (\"eta_true\", \"๋ผ๋ฒจ\", \"int\", \"0~\", \"์ค์ ๋์ฐฉ๊น์ง ๋จ์ ์๊ฐ(๋ถ)\"),\n",
" (\"s_obs\", \"ํ์\", \"float\", \"0~LEN\", \"GPS๋ฅผ ๋
ธ์ ์ ํฌ์ํ ์งํ๊ฑฐ๋ฆฌ\"), (\"off_km\", \"ํ์\", \"float\", \"0~\", \"๋
ธ์ ๊ณผ์ ์์ง ์ดํ๊ฑฐ๋ฆฌ\"), (\"remaining_km\", \"ํ์\", \"float\", \"0~LEN\", \"๋จ์ ๊ฑฐ๋ฆฌ\"),\n",
" (\"progress\", \"ํ์\", \"float\", \"0~1\", \"์งํ๋ฅ \"), (\"speed_ma3\", \"ํ์\", \"float\", \"0~\", \"์ต๊ทผ 3ํ ํ๊ท ์๋\"), (\"speed_std3\", \"ํ์\", \"float\", \"0~\", \"์ต๊ทผ 3ํ ์๋ ํ์คํธ์ฐจ\"),\n",
" (\"is_stop\", \"ํ์\", \"int\", \"0|1\", \"์ ์ฐจ ํ์ (3km/h ๋ฏธ๋ง)\"), (\"stop_n\", \"ํ์\", \"int\", \"0~\", \"๋์ ์ ์ฐจ ํ์\"), (\"stop_min\", \"ํ์\", \"int\", \"0~\", \"๋์ ์ ์ฐจ ์๊ฐ(๋ถ)\"),\n",
" (\"since_stop\", \"ํ์\", \"int\", \"0~\", \"๋ง์ง๋ง ์ ์ฐจ ์ดํ ๊ฒฝ๊ณผ(๋ถ)\"), (\"hour_sin\", \"ํ์\", \"float\", \"-1~1\", \"์๊ฐ ์ํ(์ฌ์ธ)\"), (\"hour_cos\", \"ํ์\", \"float\", \"-1~1\", \"์๊ฐ ์ํ(์ฝ์ฌ์ธ)\"),\n",
" (\"cong\", \"ํ์\", \"float\", \"0.5~1\", \"์๊ฐ๋ ์ ์ฒด ๊ณ์\"), (\"k_๋์ฅ\", \"ํ์\", \"int\", \"0|1\", \"๋์ฅ ํ๋ฌผ ๋๋ฏธ\"), (\"k_๋ฏผ๊ฐ\", \"ํ์\", \"int\", \"0|1\", \"๋ฏผ๊ฐ ํ๋ฌผ ๋๋ฏธ\"),\n",
" (\"rem_unpaved_km\", \"ํ์\", \"float\", \"0~\", \"๋จ์ ๋นํฌ์ฅ ๊ฑฐ๋ฆฌ\"), (\"rem_threat_km\", \"ํ์\", \"float\", \"0~\", \"๋จ์ ์ํ(์ ์ฒด) ๊ตฌ์ญ ๊ฑฐ๋ฆฌ\"), (\"rem_bridge_km\", \"ํ์\", \"float\", \"0~\", \"๋จ์ ๊ต๋ยทํฐ๋ ๊ฑฐ๋ฆฌ\"),\n",
" (\"base_eta\", \"ํ์\", \"float\", \"0~600\", \"ํ์ฌ ์๋ ๊ธฐ๋ฐ ๋จ์ ETA(๋ถ)\")]\n",
"dd = pd.DataFrame(DICT, columns=[\"ํ๋\", \"๊ตฌ๋ถ\", \"ํ\", \"ํ์ฉ๊ฐ\", \"์ค๋ช
\"])\n",
"cnt = dd[\"๊ตฌ๋ถ\"].value_counts().to_dict()\n",
"print(f\"[1] ๋ฐ์ดํฐ ์ฌ์ {len(dd)}๊ฐ ํ๋: {cnt}\")\n",
"\n",
"# 2) ์ค์ ์์ฑ ๋ฐ์ดํฐ์ ๋์กฐ\n",
"raw = make_telemetry(n_trips=20)\n",
"feat = add_features(raw)\n",
"extra_cols = sorted(set(feat.columns) - set(dd[\"ํ๋\"]))\n",
"missing = sorted(set(dd[\"ํ๋\"]) - set(feat.columns))\n",
"fe = dd[~dd[\"๊ตฌ๋ถ\"].isin([\"์๋ณ\", \"๋ผ๋ฒจ\"])]\n",
"print(f\"[2] ๋ฐ์ดํฐ ์ฌ์ ๋์กฐ({len(raw)}ํ): ์ฌ์ ๋๋ฝ ์ปฌ๋ผ {extra_cols or '์์'}, ๋ฐ์ดํฐ์ ์๋ ํ๋ {missing or '์์'}\")\n",
"print(f\" ์์ธกยท์ถ์ ํผ์ฒ ํ๋ณด {len(fe)}๊ฐ(๋ชฉํ 20๊ฐ ์ด์: {'๋ฌ์ฑ' if len(fe) >= 20 else '๋ฏธ๋ฌ'}), ๋ผ๋ฒจ(๋ชจ์ ์ ๋ต) {int((dd['๊ตฌ๋ถ'] == '๋ผ๋ฒจ').sum())}๊ฐ๋ ์ค์ ์ด์ฉ์์๋ ์กด์ฌํ์ง ์๋๋ค\")\n",
"\n",
"# 3) ํ์ฉ๊ฐ ๊ฒ์ฆ๊ธฐ์ ์ค๋ฅ ์ฃผ์
\n",
"RULES = {\"x\": (-10, 160), \"y\": (-10, 90), \"speed\": (0, 120), \"heading\": (0, 360), \"rssi\": (-110, -40), \"cargo_temp\": (-5, 40), \"shock_g\": (0, 5), \"t\": (5, 720), \"size\": (3, 12)}\n",
"\n",
"\n",
"def validate(rec):\n",
" errs = []\n",
" for k, (lo, hi) in RULES.items():\n",
" v = rec.get(k)\n",
" if v is None or (isinstance(v, float) and np.isnan(v)):\n",
" errs.append(f\"{k}: ๊ฒฐ์ธก\")\n",
" elif not lo <= v <= hi:\n",
" errs.append(f\"{k}: ๋ฒ์ ๋ฐ({v:.1f})\")\n",
" if rec.get(\"cargo\") not in (\"์ผ๋ฐ\", \"๋์ฅ\", \"๋ฏผ๊ฐ\"):\n",
" errs.append(\"cargo: ํ์ฉ๋์ง ์์ ๊ฐ\")\n",
" return errs\n",
"\n",
"\n",
"recs = raw.drop(columns=[\"stop_cause\"]).to_dict(\"records\")\n",
"n_err = sum(1 for r in recs if validate(r))\n",
"print(f\"[3] ์ ์ ์ํ {len(recs)}๊ฑด ๊ฒ์ฆ ์ค๋ฅ {n_err}๊ฑด\")\n",
"bad = [(\"GPS ์ขํ sentinel\", {**recs[0], \"x\": -999.0, \"y\": -999.0}), (\"์๋ ๋จ์ ์ค๋ฅ\", {**recs[1], \"speed\": 1800.0}), (\"๋ฐฉํฅ ๋ฒ์ ์ด๊ณผ\", {**recs[2], \"heading\": 400.0}),\n",
" (\"์ผ์ sentinel\", {**recs[3], \"cargo_temp\": -999.0}), (\"์๋ ๊ฒฐ์ธก\", {**recs[4], \"speed\": float(\"nan\")}), (\"ํ๋ฌผ ๊ตฌ๋ถ ์ค๊ธฐ\", {**recs[5], \"cargo\": \"๊ธฐํ\"})]\n",
"caught = 0\n",
"for name, r in bad:\n",
" e = validate(copy.deepcopy(r))\n",
" caught += bool(e)\n",
" print(f\" - {name}: {'; '.join(e)}\")\n",
"print(f\" ์ฃผ์
์ค๋ฅ {len(bad)}์ข
์ค {caught}์ข
๊ฒ์ถ\")\n",
"\n",
"# 4) ๋ฉํ ๋ฌผ๋ฅ ํ๋ฆ ์ถ์ ์
๋ฌด ํ๋ก์ธ์ค ๋งต\n",
"proc = [(\"๋ฉํ ์์ ์ ์\", \"์์ก๊ณํ์ฅ๊ต\", \"์๋ น๋ถ๋ ์์ฒญ ์์\", \"์ผ์ผ ๋ฉํ ์์\", 30), (\"์ ๋จ ํธ์ฑยท์ถ๋ฐ ๋ณด๊ณ \", \"์์กํต์ ์\", \"์์ฐยทํ๋ฌผ ๋ชฉ๋ก\", \"trip, size, cargo\", 25),\n",
" (\"GPSยท์ผ์ ํ
๋ ๋ฉํธ๋ฆฌ ์์ \", \"๊ด์ ๋ด๋น\", \"5๋ถ ๊ฐ๊ฒฉ ๋ณด๊ณ \", \"x, y, speed, rssi\", 5), (\"๋ฐ์ดํฐ ์ ์ ยท๋ณด์ \", \"๋ถ์ ๋ด๋น\", \"์์ ํ
๋ ๋ฉํธ๋ฆฌ\", \"์ ์ ๋ฐ์ดํฐ\", 15),\n",
" (\"ETA ์ฐ์ถ\", \"๋ถ์ ๋ด๋น\", \"์ ์ ๋ฐ์ดํฐ, ๋
ธ์ ๊ตฌ์ฑ\", \"ETA(๋ถ)\", 10), (\"์ดํยท์ ์ฐจ ๊ฐ์ง\", \"๊ด์ ๋ด๋น\", \"์์นยท์๋\", \"์ด์ ๊ฒฝ๋ณด\", 5),\n",
" (\"์ง์ฐ ๊ฒฝ๋ณด ํ์ธ\", \"ํ์ฅํ์ธ์\", \"๊ฒฝ๋ณด, ETA ๋ณ๋\", \"ํ์ธ ๊ฒฐ๊ณผ\", 30), (\"๋์ฒด ์์ก ์ง์\", \"์์กํต์ ๊ด\", \"ํ์ธ ๊ฒฐ๊ณผ\", \"์ง์(์น์ธ)\", 20),\n",
" (\"๋์ฐฉ ํ์ธยท์ค์ ๋์กฐ\", \"์๋ น๋ถ๋\", \"๋์ฐฉ ๋ณด๊ณ \", \"ETA ์ค์ฐจ ๊ธฐ๋ก\", 25)]\n",
"pm = pd.DataFrame(proc, columns=[\"๋จ๊ณ\", \"๋ด๋น\", \"์
๋ ฅ\", \"์ฐ์ถ\", \"ํํ(๋ถ)\"])\n",
"pm.index = range(1, len(pm) + 1)\n",
"print(f\"\\n[4] ์
๋ฌด ํ๋ก์ธ์ค ๋งต {len(pm)}๋จ๊ณ, ํํ ํฉ๊ณ {pm['ํํ(๋ถ)'].sum()}๋ถ (๊ฐ์ฅ ๊ธด ๋จ๊ณ: {pm.loc[pm['ํํ(๋ถ)'].idxmax(), '๋จ๊ณ']})\")\n",
"print(pm.to_string())\n",
"print(\"\\nํ๋ก์ธ์ค ๋งต ์์ฑ ์๋ฃ, ํผ์ฒ ๋ชฉ๋ก 20๊ฐ ์ด์ ->\", \"๋ฌ์ฑ\" if len(fe) >= 20 and len(pm) >= 5 else \"๋ฏธ๋ฌ\")\n",
"print(\"๋ชจ๋ ๋
ธ์ ยท์ขํยท์์น๋ ๊ต์ก์ฉ ๊ฐ์ ์๋ฃ์ด๋ฉฐ ์ค์ ์ ๋จยท๋ถ๋ยท๋๋ก์ ๋ฌด๊ดํ๋ค.\")\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"๐ก [์ฐธ๊ณ ] ๋ฏธ์
01 ์์ ์คํ ๊ฒฐ๊ณผ (Output)
\n",
"\n",
"```text\n",
"[1] ๋ฐ์ดํฐ ์ฌ์ 37๊ฐ ํ๋: {'ํ์': 19, 'GPS': 6, '๋ผ๋ฒจ': 5, '๋งฅ๋ฝ': 3, 'ํ๋ฌผ์ผ์': 2, '์๋ณ': 1, 'ํต์ ': 1}\n",
"[2] ๋ฐ์ดํฐ ์ฌ์ ๋์กฐ(1059ํ): ์ฌ์ ๋๋ฝ ์ปฌ๋ผ ์์, ๋ฐ์ดํฐ์ ์๋ ํ๋ ์์\n",
" ์์ธกยท์ถ์ ํผ์ฒ ํ๋ณด 31๊ฐ(๋ชฉํ 20๊ฐ ์ด์: ๋ฌ์ฑ), ๋ผ๋ฒจ(๋ชจ์ ์ ๋ต) 5๊ฐ๋ ์ค์ ์ด์ฉ์์๋ ์กด์ฌํ์ง ์๋๋ค\n",
"[3] ์ ์ ์ํ 1059๊ฑด ๊ฒ์ฆ ์ค๋ฅ 0๊ฑด\n",
" - GPS ์ขํ sentinel: x: ๋ฒ์ ๋ฐ(-999.0); y: ๋ฒ์ ๋ฐ(-999.0)\n",
" - ์๋ ๋จ์ ์ค๋ฅ: speed: ๋ฒ์ ๋ฐ(1800.0)\n",
" - ๋ฐฉํฅ ๋ฒ์ ์ด๊ณผ: heading: ๋ฒ์ ๋ฐ(400.0)\n",
" - ์ผ์ sentinel: cargo_temp: ๋ฒ์ ๋ฐ(-999.0)\n",
" - ์๋ ๊ฒฐ์ธก: speed: ๊ฒฐ์ธก\n",
" - ํ๋ฌผ ๊ตฌ๋ถ ์ค๊ธฐ: cargo: ํ์ฉ๋์ง ์์ ๊ฐ\n",
" ์ฃผ์
์ค๋ฅ 6์ข
์ค 6์ข
๊ฒ์ถ\n",
"\n",
"[4] ์
๋ฌด ํ๋ก์ธ์ค ๋งต 9๋จ๊ณ, ํํ ํฉ๊ณ 165๋ถ (๊ฐ์ฅ ๊ธด ๋จ๊ณ: ๋ฉํ ์์ ์ ์)\n",
" ๋จ๊ณ ๋ด๋น ์
๋ ฅ ์ฐ์ถ ํํ(๋ถ)\n",
"1 ๋ฉํ ์์ ์ ์ ์์ก๊ณํ์ฅ๊ต ์๋ น๋ถ๋ ์์ฒญ ์์ ์ผ์ผ ๋ฉํ ์์ 30\n",
"2 ์ ๋จ ํธ์ฑยท์ถ๋ฐ ๋ณด๊ณ ์์กํต์ ์ ์์ฐยทํ๋ฌผ ๋ชฉ๋ก trip, size, cargo 25\n",
"3 GPSยท์ผ์ ํ
๋ ๋ฉํธ๋ฆฌ ์์ ๊ด์ ๋ด๋น 5๋ถ ๊ฐ๊ฒฉ ๋ณด๊ณ x, y, speed, rssi 5\n",
"4 ๋ฐ์ดํฐ ์ ์ ยท๋ณด์ ๋ถ์ ๋ด๋น ์์ ํ
๋ ๋ฉํธ๋ฆฌ ์ ์ ๋ฐ์ดํฐ 15\n",
"5 ETA ์ฐ์ถ ๋ถ์ ๋ด๋น ์ ์ ๋ฐ์ดํฐ, ๋
ธ์ ๊ตฌ์ฑ ETA(๋ถ) 10\n",
"6 ์ดํยท์ ์ฐจ ๊ฐ์ง ๊ด์ ๋ด๋น ์์นยท์๋ ์ด์ ๊ฒฝ๋ณด 5\n",
"7 ์ง์ฐ ๊ฒฝ๋ณด ํ์ธ ํ์ฅํ์ธ์ ๊ฒฝ๋ณด, ETA ๋ณ๋ ํ์ธ ๊ฒฐ๊ณผ 30\n",
"8 ๋์ฒด ์์ก ์ง์ ์์กํต์ ๊ด ํ์ธ ๊ฒฐ๊ณผ ์ง์(์น์ธ) 20\n",
"9 ๋์ฐฉ ํ์ธยท์ค์ ๋์กฐ ์๋ น๋ถ๋ ๋์ฐฉ ๋ณด๊ณ ETA ์ค์ฐจ ๊ธฐ๋ก 25\n",
"\n",
"ํ๋ก์ธ์ค ๋งต ์์ฑ ์๋ฃ, ํผ์ฒ ๋ชฉ๋ก 20๊ฐ ์ด์ -> ๋ฌ์ฑ\n",
"๋ชจ๋ ๋
ธ์ ยท์ขํยท์์น๋ ๊ต์ก์ฉ ๊ฐ์ ์๋ฃ์ด๋ฉฐ ์ค์ ์ ๋จยท๋ถ๋ยท๋๋ก์ ๋ฌด๊ดํ๋ค.\n",
"```\n",
" \n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## ๐ฏ ๋ฏธ์
02. ์์ก ๋ฐ์ดํฐ ์ ์ , ๋น์ ์ GPS ์ขํ ๋ณด์ ๋ฐ ์๋/์ด๋ ๋ฐฉํฅ ์ ์ฒ๋ฆฌ ๋ชจ๋ ๊ตฌํ\n",
"**์์์๊ฐ**: ์ฝ 2.0์๊ฐ\n",
"\n",
"### ๐ ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ์ํ ์ง์นจ\n",
"#### ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ๋น์์ฑ\n",
"\n",
"์์ก ๋ฐ์ดํฐ ์ ์ ๋ฐ ๋น์ ์ GPS ์ขํ ๋ณด์ ์ ์ฒ๋ฆฌ ๋ชจ๋์ ๊ตฌํํ๋ค. ์ค์ ์ ์ ํ์ฅ์์๋ ๋จ์ ํ์ง์ ๊ทธ์น์ง ์๊ณ ์ ์ํ ์ํ ํ๋จ๊ณผ ์ ๋ขฐ๋ ๋์ ์์ฌ๊ฒฐ์ ์ด ํ์์ ์ด๋ฏ๋ก, **์์ก ํ
๋ ๋ฉํธ๋ฆฌ์ ๋น์ ์ GPS ์ขํ์ ์๋ยท์ด๋ ๋ฐฉํฅ์ ๋ณด์ ํ๋ ์ ์ฒ๋ฆฌ ๋ชจ๋์ ๊ตฌํํ๋ค.** ๊ณผ์
์ ์์ํ์ฌ ์คํ์ ๋ฐฉ์งํ๊ณ ์์ ๋์๋ ฅ์ ๊ทน๋ํํด์ผ ํฉ๋๋ค.\n",
"\n",
"์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ณธ ๋ฏธ์
์์๋ **์ขํ sentinel, GPS ์ ํ, ์๋ ์์ยท๋จ์ ์ค๋ฅยท๊ฒฐ์ธก, ๋ฐฉํฅ ์ด์, ์ผ์ sentinel, ๋๋ฝ, ์ค๋ณต์ ์ฃผ์
ํ ๊ฐ์ ๊ธฐ๋ก์ ์ ์ ๊ท์น 10์ข
์ ์์๋๋ก ์ ์ฉ ์ํํ๊ณ ๊ฒ์ฆํ๋** ์ค์ต์ ์ฒด๊ณ์ ์ผ๋ก ์ํํฉ๋๋ค.\n",
"\n",
"ํ์ต์๋ ์ค์ต๊ฐ์ด๋์ ์ ์๋ ์ด 4๊ฐ ์ธ๋ถ ํ๋(๊ธฐ๋ณธ ๊ธฐ๋ฅ ๊ตฌํ โ ๊ฒฐ๊ณผ ์งํ ๊ฒ์ฆ โ ํ๋ผ๋ฏธํฐ ํ๋ ๋ฐ ๋์ ๊ณผ์ )์ ์์ฐจ์ ์ผ๋ก ์งํํ๋ฉฐ ํต์ฌ ํ์ ๊ธฐ์ค์ ๋ฌ์ฑํฉ๋๋ค.\n",
"\n",
"**์ด๋ฒ ๊ฒฐ๊ณผ๋ฌผ:** `src/etl/mov06_data_cleansing.py ๋ฐ ์ ์ ํ์ง ๋ณด๊ณ ์` ํํ์ ๊ฒฐ๊ณผ๋ฌผ์ ์์ฑํ๋ค.\n",
"\n",
"### ๐ ํ๊ฐ ๋ฐ ๊ฒ์ฆ ๊ธฐ์ค (PASS ์กฐ๊ฑด)\n",
"#### ๋ฏธ์
ํ์ \n",
"\n",
"**ํ๊ฐ ๊ตฌ๋ถ:** ํ์\n",
"\n",
"**ํต์ฌ ํ๊ฐ์์ญ:** ๊ตฐ์ ํน์ ์์ธ(๋น๊ณํ ์ ๋น, ๊ธด๊ธ ๋ถ์ถ ๋ฑ) ๋ฐ์ดํฐ ๋ณด์กด์ ํ๋น์ฑ\n",
"\n",
"#### ๋ฏธ์
PASS ์กฐ๊ฑด\n",
"\n",
"- ์ ๋ ๊ธฐ์ค: ๋ฐ์ดํฐ ์ ์ ๊ท์น 10์ข
์ ์ฉ, ๋ฐ์ดํฐ ์ ํฉ์ฑ ๊ฒ์ฆ ํต๊ณผ์จ 100%\n",
"\n",
"\n",
"- ์งํ ๊ธฐ์ค: ETL ํ์ดํ๋ผ์ธ ๊ตฌํ ๋ฐ ์ ์ ๋ ๋ง์คํฐ ๋ฐ์ดํฐ์
๊ตฌ์ถ\n",
"\n",
"\n",
"- ๊ฒ์ฆ ๋ฐฉ๋ฒ: ์ ํ ๋ฐ์ดํฐ ํต๊ณ๋(๊ธฐ์ ํต๊ณ) ๋น๊ต ๋ฐ ๋ฌด๊ฒฐ์ฑ ๊ฒ์ฆ\n",
"\n",
"\n",
"#### ์ฑ์ ์ ํ์ ๋ฉ๋ชจ\n",
"\n",
"> ๋จ์ ์ฝ๋ ์คํ ์ฌ๋ถ๋ฟ๋ง ์๋๋ผ ์ถ๋ ฅ๋ ์์น์ ๊ทผ๊ฑฐ ์๋ฃ๊ฐ ๊ตฐ ์ค๋ฌด ์๊ตฌ์ฌํญ์ ๋ถํฉํ๋์ง ๋ฉด๋ฐํ ํ์ธํ๋ค.\n",
"\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"\"\"\"์์ก์ ๋จ ํ
๋ ๋ฉํธ๋ฆฌ์ ๋น์ ์ GPS ์ขํยท์๋ยท๋ฐฉํฅ์ ๋ณด์ ํ๋ ์ ์ฒ๋ฆฌ ๋ชจ๋: ์ค์ผ ๊ธฐ๋ก์ ์ ์ ๊ท์น 10์ข
์ ์ ์ฉํ๊ณ , ์์น ์ฐจ๋ถ์ผ๋ก ์๋ยท๋ฐฉํฅ์ ์ฌ์ฐ์ถํ๋ฉฐ, ์ ์ฐจ(๋น๊ณํ ์ ๋นยท๊ธด๊ธ ๋ถ์ถ ๋๊ธฐ) ๊ธฐ๋ก์ ์ญ์ ํ์ง ์๊ณ ๋ณด์กดํ๋ค.\"\"\"\n",
"import numpy as np\n",
"import pandas as pd\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# [๊ต์ก์ฉ๊ฐ์์๋ฃ] ๊ฐ์ ์์ก ๋
ธ์ ยท์์ก์ ๋จ ํ
๋ ๋ฉํธ๋ฆฌ ์์ฑ๊ธฐ (์ค์ ๋๋กยท์ง๋ช
ยท๋ถ๋ ์์)\n",
"# ๊ฐ์ ๋
ธ์ (์ถ๋ฐ ๊ฑฐ์ -> ๋์ฐฉ ๊ฑฐ์ , 5๊ฐ ๊ตฌ๊ฐ)์ 5๋ถ ๊ฐ๊ฒฉ์ผ๋ก ๋ณด๊ณ ํ๋ ์ ๋จ GPSยทํ๋ฌผ ์ผ์ยทํต์ ์ํ ์๋ฃ๋ฅผ ๋ง๋ ๋ค.\n",
"# ---------------------------------------------------------------------------\n",
"PTS = [(0, 0), (30, 12), (62, 14), (88, 38), (112, 42), (135, 66)] # ๋
ธ์ ๊ผญ์ง์ (km, ๊ฐ์ ์ขํ)\n",
"ROUTE = pd.DataFrame({\"seg\": [\"R1\", \"R2\", \"R3\", \"R4\", \"R5\"], \"road\": [\"paved\", \"unpaved\", \"paved\", \"paved\", \"unpaved\"],\n",
" \"limit\": [70, 50, 70, 50, 50], \"kind\": [\"์ผ๋ฐ\", \"์ผ๋ฐ\", \"์ผ๋ฐ\", \"๊ต๋ยทํฐ๋\", \"์ผ๋ฐ\"], \"threat\": [0, 0, 1, 0, 0]})\n",
"ROUTE[\"x0\"], ROUTE[\"y0\"] = [p[0] for p in PTS[:-1]], [p[1] for p in PTS[:-1]]\n",
"ROUTE[\"x1\"], ROUTE[\"y1\"] = [p[0] for p in PTS[1:]], [p[1] for p in PTS[1:]]\n",
"ROUTE[\"km\"] = np.hypot(ROUTE[\"x1\"] - ROUTE[\"x0\"], ROUTE[\"y1\"] - ROUTE[\"y0\"])\n",
"ROUTE[\"c0\"] = np.r_[0, np.cumsum(ROUTE[\"km\"])[:-1]]\n",
"ROUTE[\"c1\"] = np.cumsum(ROUTE[\"km\"])\n",
"LEN = float(ROUTE[\"km\"].sum())\n",
"ROAD_F = {\"paved\": 1.0, \"unpaved\": 0.75}\n",
"C1L, C0L, KML = ROUTE[\"c1\"].tolist(), ROUTE[\"c0\"].tolist(), ROUTE[\"km\"].tolist() # ๋ฐ๋ณต ๊ณ์ฐ์ฉ ๋ฆฌ์คํธ(์๋)\n",
"BASE_V = [l * ROAD_F[r] for l, r in zip(ROUTE[\"limit\"], ROUTE[\"road\"])]\n",
"THR, UNP, TUN = ROUTE[\"threat\"].tolist(), (ROUTE[\"road\"] == \"unpaved\").tolist(), (ROUTE[\"kind\"] == \"๊ต๋ยทํฐ๋\").tolist()\n",
"\n",
"\n",
"def seg_of(s):\n",
" # [TODO : ํ์ ์ค์ต 2] ์์ก ๋ฐ์ดํฐ ์ ์ , ๋น์ ์ GPS ์ขํ ๋ณด์ ๋ฐ ์๋/์ด๋ ๋ฐฉํฅ ์ ์ฒ๋ฆฌ ๋ชจ๋ ๊ตฌํ ๊ด๋ จ ํต์ฌ ๋ก์ง์ ๊ตฌํํ์ธ์.\n",
" # ํํธ: ์๋จ ์ฃผ์ ๋ฐ ๋ฐ์ดํฐ ์คํค๋ง/์ง์นจ์ ์ฐธ๊ณ ํ์ฌ ์์ฑํฉ๋๋ค.\n",
" # <<< ์ฌ๊ธฐ์ ์ฝ๋๋ฅผ ์์ฑํ์ธ์ >>>\n",
" pass # ์์ฑ ํ pass๋ฅผ ์ ๊ฑฐํ์ธ์.\n",
"def route_xy(s):\n",
" \"\"\"๋
ธ์ ์ ์งํ๊ฑฐ๋ฆฌ s(km)์ ์ขํ์ ์งํ ๋ฐฉํฅ(๋).\"\"\"\n",
" # [TODO : ํ์ ์ค์ต 2] ์์ก ๋ฐ์ดํฐ ์ ์ , ๋น์ ์ GPS ์ขํ ๋ณด์ ๋ฐ ์๋/์ด๋ ๋ฐฉํฅ ์ ์ฒ๋ฆฌ ๋ชจ๋ ๊ตฌํ ๊ด๋ จ ํต์ฌ ๋ก์ง์ ๊ตฌํํ์ธ์.\n",
" # ํํธ: ์๋จ ์ฃผ์ ๋ฐ ๋ฐ์ดํฐ ์คํค๋ง/์ง์นจ์ ์ฐธ๊ณ ํ์ฌ ์์ฑํฉ๋๋ค.\n",
" # <<< ์ฌ๊ธฐ์ ์ฝ๋๋ฅผ ์์ฑํ์ธ์ >>>\n",
" pass # ์์ฑ ํ pass๋ฅผ ์ ๊ฑฐํ์ธ์.\n",
"def project(x, y):\n",
" \"\"\"GPS ์ขํ๋ฅผ ๋
ธ์ ์ ํฌ์ํด (์งํ๊ฑฐ๋ฆฌ km, ๋
ธ์ ๊ณผ์ ์์ง ์ดํ km)๋ฅผ ๊ตฌํ๋ค.\"\"\"\n",
" x, y = np.atleast_1d(x).astype(float), np.atleast_1d(y).astype(float)\n",
" best_s, best_d = np.zeros(len(x)), np.full(len(x), np.inf)\n",
" for r in ROUTE.itertuples():\n",
" dx, dy = r.x1 - r.x0, r.y1 - r.y0\n",
" f = np.clip(((x - r.x0) * dx + (y - r.y0) * dy) / (dx * dx + dy * dy), 0, 1)\n",
" d = np.hypot(x - (r.x0 + f * dx), y - (r.y0 + f * dy))\n",
" better = d < best_d\n",
" best_s, best_d = np.where(better, r.c0 + f * r.km, best_s), np.where(better, d, best_d)\n",
" return best_s, best_d\n",
"\n",
"\n",
"def cong(clock_min):\n",
" h = (clock_min / 60) % 24\n",
" return 0.82 if (7 <= h < 9 or 17 <= h < 19) else 1.0 # ์ถํด๊ทผ ์๊ฐ๋ ์ ์ฒด ๊ณ์(๊ฐ์ )\n",
"\n",
"\n",
"def simulate_trip(rng, tid, start_min, rain, size, cargo):\n",
" \"\"\"1๋ถ ๋จ์๋ก ์ด๋์ ๋ชจ์ํ๊ณ 5๋ถ๋ง๋ค ํ
๋ ๋ฉํธ๋ฆฌ๋ฅผ ๋จ๊ธด๋ค. ์ดํยท๋ถ์ ์ ์ฐจ ์ด๋ฒคํธ๋ ์ ๋ต ๋ผ๋ฒจ๋ก ํจ๊ป ์ ์ฅํ๋ค.\"\"\"\n",
" s, t, stop_left, rows = 0.0, 0, 0, []\n",
" dev = (rng.random() < 0.12)\n",
" dev_t0 = int(rng.integers(30, 120)) if dev else -1\n",
" dev_len = int(rng.integers(12, 25)) if dev else 0\n",
" cause = \"\"\n",
" while s < LEN:\n",
" i = seg_of(s)\n",
" v = BASE_V[i] * (0.88 if rain else 1.0) * (0.85 if (rain and UNP[i]) else 1.0) * cong(start_min + t) \\\n",
" * (0.7 if THR[i] else 1.0) * (1 - 0.012 * size) * rng.normal(1, 0.06)\n",
" off = 0.0\n",
" if dev and dev_t0 <= t < dev_t0 + dev_len:\n",
" ph = (t - dev_t0) / dev_len\n",
" off, v = 4.0 * (1 - abs(2 * ph - 1)), v * 0.4 # ๋
ธ์ ์ดํ(์ผ๊ฐํ ํํ๋ก ์ต๋ 4km)\n",
" if stop_left > 0:\n",
" v, stop_left = 0.0, stop_left - 1\n",
" elif rng.random() < 0.0015:\n",
" stop_left, cause = int(rng.integers(8, 36)), (\"maint\" if rng.random() < 0.7 else \"urgent\")\n",
" s += v / 60\n",
" t += 1\n",
" if t % 5 == 0 or s >= LEN:\n",
" x, y, hd = route_xy(min(s, LEN))\n",
" nx, ny = np.cos(np.radians(hd)), -np.sin(np.radians(hd))\n",
" j = seg_of(min(s, LEN)); tun = TUN[j]\n",
" rows.append(dict(trip=tid, t=t, clock=(start_min + t) % 1440, x=x + off * nx + rng.normal(0, 0.03), y=y + off * ny + rng.normal(0, 0.03),\n",
" speed=max(v * rng.normal(1, 0.03), 0.0), heading=(hd + rng.normal(0, 3)) % 360, rssi=-62 - 14 * tun + rng.normal(0, 3),\n",
" cargo_temp=18 + 3 * np.sin(t / 40) + rng.normal(0, 0.3), shock_g=abs(rng.normal(0.25, 0.1)) * (2.0 if UNP[j] else 1.0),\n",
" s_true=min(s, LEN), off_true=off, stopped_true=int(v == 0.0), rain=int(rain), size=size, cargo=cargo, stop_cause=cause if v == 0.0 else \"\"))\n",
" arr = t\n",
" for r in rows:\n",
" r[\"eta_true\"] = arr - r[\"t\"]\n",
" return rows\n",
"\n",
"\n",
"def make_telemetry(seed=21, n_trips=200):\n",
" \"\"\"n_trips๊ฐ ์์ก์ ๋จ ์ด๋ ๊ธฐ๋ก(๋ ์ง ์). ์ ๋จ ๊ท๋ชจ 3~12๋, ํ๋ฌผ ๊ตฌ๋ถ ์ผ๋ฐ/๋์ฅ/๋ฏผ๊ฐ.\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" rows = []\n",
" for k in range(n_trips):\n",
" rows += simulate_trip(rng, k, int(rng.integers(5 * 60, 15 * 60)), bool(rng.random() < 0.2), int(rng.integers(3, 13)), str(rng.choice([\"์ผ๋ฐ\", \"๋์ฅ\", \"๋ฏผ๊ฐ\"], p=[.6, .25, .15])))\n",
" return pd.DataFrame(rows)\n",
"\n",
"\n",
"def mape(y, p):\n",
" y, p = np.asarray(y, float), np.asarray(p, float)\n",
" return float(np.mean(np.abs(y - p) / np.abs(y)) * 100)\n",
"\n",
"\n",
"def mae(y, p):\n",
" return float(np.mean(np.abs(np.asarray(y, float) - np.asarray(p, float))))\n",
"# ---------------------------------------------------------------------------\n",
"\n",
"clean = make_telemetry(n_trips=30)\n",
"rng = np.random.default_rng(3)\n",
"dirty = clean.drop(columns=[\"stop_cause\"]).copy()\n",
"n = len(dirty)\n",
"ix = rng.choice(n, 90, replace=False)\n",
"dirty.loc[ix[:8], [\"x\", \"y\"]] = -999.0 # ์ขํ sentinel\n",
"dirty.loc[ix[8:16], \"x\"] += rng.choice([-45.0, 45.0], 8) # GPS ์ ํ(์๊ฐ ์ด๋)\n",
"dirty.loc[ix[16:22], \"speed\"] = -dirty.loc[ix[16:22], \"speed\"] - 1 # ์์ ์๋\n",
"dirty.loc[ix[22:28], \"speed\"] = dirty.loc[ix[22:28], \"speed\"] * 36 + 100 # ๋จ์ ์ค๋ฅ(๊ณผ๋)\n",
"dirty.loc[ix[28:36], \"speed\"] = np.nan\n",
"dirty.loc[ix[36:44], \"heading\"] = rng.choice([-30.0, 400.0, np.nan, 725.0], 8)\n",
"dirty.loc[ix[44:52], \"rssi\"] = -999.0\n",
"dirty.loc[ix[52:58], \"cargo_temp\"] = -999.0\n",
"dirty = dirty.drop(index=ix[58:70]).reset_index(drop=True) # ํจํท ๋๋ฝ 12๊ฑด\n",
"dirty = pd.concat([dirty, dirty.sample(10, random_state=1)], ignore_index=True) # ์ค๋ณต ์ ์ก 10๊ฑด\n",
"dirty = dirty.sample(frac=1, random_state=2).reset_index(drop=True) # ๋์ฐฉ ์์ ๋ค์์(์์ ์ค๋ฅ)\n",
"print(f\"[0] ์ค์ผ ๋ฐ์ดํฐ {len(dirty)}ํ(์๋ณธ {n}ํ): ์ขํ sentinel 8, GPS ์ ํ 8, ์๋ ์์ 6ยท๋จ์ ์ค๋ฅ 6ยท๊ฒฐ์ธก 8, ๋ฐฉํฅ ์ด์ 8, ์ผ์ sentinel 14, ๋๋ฝ 12, ์ค๋ณต 10, ์์ ๋ค์์\")\n",
"\n",
"log, w = [], dirty.copy()\n",
"\n",
"\n",
"def rule(name, k):\n",
" log.append((len(log) + 1, name, int(k)))\n",
"\n",
"\n",
"# R1 ์ ๋ ฌ + (trip, t) ์ค๋ณต ์ ๊ฑฐ\n",
"n0 = len(w); w = w.sort_values([\"trip\", \"t\"]).drop_duplicates([\"trip\", \"t\"]).reset_index(drop=True); rule(\"์๊ฐ์ ์ ๋ ฌยท(trip,t) ์ค๋ณต ์ ๊ฑฐ\", n0 - len(w))\n",
"# R2 ์ผ์ sentinel -> ๊ฒฐ์ธก\n",
"bad = (w[\"rssi\"] <= -900) | (w[\"cargo_temp\"] <= -900); w.loc[w[\"rssi\"] <= -900, \"rssi\"] = np.nan; w.loc[w[\"cargo_temp\"] <= -900, \"cargo_temp\"] = np.nan; rule(\"์ผ์ sentinel(-999) -> ๊ฒฐ์ธก\", bad.sum())\n",
"# R3 ์ขํ ๋ฒ์ ๋ฐ -> ๊ฒฐ์ธก\n",
"bad = ~(w[\"x\"].between(-10, 160) & w[\"y\"].between(-10, 90)); w.loc[bad, [\"x\", \"y\"]] = np.nan; rule(\"์ขํ ๋ฒ์ ๋ฐ(sentinel ๋ฑ) -> ๊ฒฐ์ธก\", bad.sum())\n",
"# R4 GPS ์ ํ: ์๋ค 5๊ฐ ์ค์๊ฐ์์ 8km ์ด์ ๋ฒ์ด๋ ์ขํ(๋
ธ์ ์ดํ์ ์ต๋ 4km์ด๋ฏ๋ก ๋ณด์กด)\n",
"med_x = w.groupby(\"trip\")[\"x\"].transform(lambda s: s.rolling(5, center=True, min_periods=2).median())\n",
"med_y = w.groupby(\"trip\")[\"y\"].transform(lambda s: s.rolling(5, center=True, min_periods=2).median())\n",
"jump = (np.hypot(w[\"x\"] - med_x, w[\"y\"] - med_y) > 8) & w[\"x\"].notna(); w.loc[jump, [\"x\", \"y\"]] = np.nan; rule(\"GPS ์ ํ(์ด์ ์ค์๊ฐ์์ 8km ์ด๊ณผ) -> ๊ฒฐ์ธก\", jump.sum())\n",
"# R5 ์ขํ ๊ฒฐ์ธก ์ ํ ๋ณด๊ฐ(์๊ฐ ๊ธฐ์ค)\n",
"miss = w[\"x\"].isna(); w[[\"x\", \"y\"]] = w.groupby(\"trip\")[[\"x\", \"y\"]].transform(lambda s: s.interpolate(limit_direction=\"both\")); rule(\"์ขํ ๊ฒฐ์ธก -> ์๊ฐ ์ ํ ๋ณด๊ฐ\", miss.sum())\n",
"# R6 ์์น ์ฐจ๋ถ์ผ๋ก ์๋ยท๋ฐฉํฅ ์ฐ์ถ\n",
"dt = w.groupby(\"trip\")[\"t\"].diff(); dx, dy = w.groupby(\"trip\")[\"x\"].diff(), w.groupby(\"trip\")[\"y\"].diff()\n",
"v_pos = np.hypot(dx, dy) / (dt / 60); h_pos = np.degrees(np.arctan2(dx, dy)) % 360\n",
"rule(\"๋ณด๊ณ ์๋์ ์์น ์ฐจ๋ถ ์๋ 30km/h ์ด๊ณผ ๋ถ์ผ์น ํ์\", ((w[\"speed\"] - v_pos).abs() > 30).sum())\n",
"# R7 ์๋ ์์ยท๋จ์ ์ค๋ฅ(120 ์ด๊ณผ) -> ๊ฒฐ์ธก\n",
"bad = (w[\"speed\"] < 0) | (w[\"speed\"] > 120); w.loc[bad, \"speed\"] = np.nan; rule(\"์๋ ์์ยท120km/h ์ด๊ณผ -> ๊ฒฐ์ธก\", bad.sum())\n",
"# R8 ์๋ ๊ฒฐ์ธก -> ์์น ์ฐจ๋ถ ์๋(์ํ 120), ๊ทธ๋๋ ์์ผ๋ฉด ์ง์ ๊ฐ\n",
"miss = w[\"speed\"].isna(); w.loc[miss, \"speed\"] = v_pos[miss].clip(0, 120); w[\"speed\"] = w.groupby(\"trip\")[\"speed\"].ffill().bfill(); rule(\"์๋ ๊ฒฐ์ธก -> ์์น ์ฐจ๋ถ ์๋ ๋์ฒด\", miss.sum())\n",
"# R9 ๋ฐฉํฅ ์ ๊ทํ(0~360), ๋ฒ์ ๋ฐยท๊ฒฐ์ธก์ ์์น ์ฐจ๋ถ ๋ฐฉํฅ\n",
"bad = ~w[\"heading\"].between(0, 360, inclusive=\"left\"); w.loc[bad, \"heading\"] = h_pos[bad]; w[\"heading\"] = w.groupby(\"trip\")[\"heading\"].ffill().bfill() % 360; rule(\"๋ฐฉํฅ ๋ฒ์ ๋ฐยท๊ฒฐ์ธก -> ์์น ์ฐจ๋ถ ๋ฐฉํฅ\", bad.sum())\n",
"# R10 ์ ์ฐจ ๊ธฐ๋ก ๋ณด์กด ํ์ + ํจํท ๋๋ฝ ๊ตฌ๊ฐ ํ์(์ญ์ ์์)\n",
"w[\"is_stop\"] = (w[\"speed\"] < 3).astype(int); w[\"gap\"] = (w.groupby(\"trip\")[\"t\"].diff() > 5).astype(int)\n",
"w[\"rssi\"] = w.groupby(\"trip\")[\"rssi\"].ffill().bfill(); w[\"cargo_temp\"] = w.groupby(\"trip\")[\"cargo_temp\"].ffill().bfill()\n",
"rule(\"์ ์ฐจ ํ ๋ณด์กด ํ์ยท๋๋ฝ ๊ตฌ๊ฐ ํ์(์ญ์ ์์)\", w[\"is_stop\"].sum() + w[\"gap\"].sum())\n",
"\n",
"print(\"\\n[1] ์ ์ ๊ท์น ์ ์ฉ ๋ก๊ทธ\")\n",
"print(pd.DataFrame(log, columns=[\"๊ท์น\", \"๋ด์ฉ\", \"์ฒ๋ฆฌ ๊ฑด์\"]).to_string(index=False))\n",
"print(f\" ์ ์ ๊ท์น {len(log)}์ข
์ ์ฉ(๋ชฉํ 10์ข
: {'๋ฌ์ฑ' if len(log) >= 10 else '๋ฏธ๋ฌ'})\")\n",
"\n",
"checks = {\"(trip,t) ์ ์ผ\": not w.duplicated([\"trip\", \"t\"]).any(), \"์๊ฐ ๋จ์กฐ ์ฆ๊ฐ\": bool((w.groupby(\"trip\")[\"t\"].diff().dropna() > 0).all()),\n",
" \"์ขํ ๋ฒ์\": bool(w[\"x\"].between(-10, 160).all() and w[\"y\"].between(-10, 90).all()), \"์๋ 0~120\": bool(w[\"speed\"].between(0, 120).all()),\n",
" \"๋ฐฉํฅ 0~360\": bool(w[\"heading\"].between(0, 360, inclusive=\"left\").all()), \"ํต์ ๊ฐ๋ ๋ฒ์\": bool(w[\"rssi\"].between(-110, -40).all()),\n",
" \"ํ๋ฌผ ์จ๋ ๋ฒ์\": bool(w[\"cargo_temp\"].between(-5, 40).all()), \"ํ์ ์ปฌ๋ผ ๊ฒฐ์ธก 0\": int(w[[\"x\", \"y\", \"speed\", \"heading\", \"rssi\", \"cargo_temp\"]].isna().sum().sum()) == 0}\n",
"print(\"\\n[2] ์ ํฉ์ฑ ๊ฒ์ฆ\")\n",
"for k, v in checks.items():\n",
" print(f\" [{'PASS' if v else 'FAIL'}] {k}\")\n",
"print(f\" ํต๊ณผ์จ {sum(checks.values()) / len(checks):.0%} (๋ชฉํ 100%)\")\n",
"\n",
"# ๋ณด์ ํจ๊ณผ: ์งํ๊ฑฐ๋ฆฌ ์ค์ฐจ(ํฌ์ ์งํ๊ฑฐ๋ฆฌ vs ์ค์ ์งํ๊ฑฐ๋ฆฌ), ์ ์ฐจ ๋ณด์กด\n",
"s_d, _ = project(dirty[\"x\"].where(dirty[\"x\"] > -900, np.nan).fillna(0).values, dirty[\"y\"].where(dirty[\"y\"] > -900, np.nan).fillna(0).values)\n",
"s_w, _ = project(w[\"x\"].values, w[\"y\"].values)\n",
"truth = clean.set_index([\"trip\", \"t\"])[\"s_true\"]\n",
"tru_d = truth.reindex(pd.MultiIndex.from_frame(dirty[[\"trip\", \"t\"]])).values\n",
"tru_w = truth.reindex(pd.MultiIndex.from_frame(w[[\"trip\", \"t\"]])).values\n",
"print(f\"\\n[3] ์งํ๊ฑฐ๋ฆฌ ์ค์ฐจ(MAE): ์ค์ผ {np.nanmean(np.abs(s_d - tru_d)):.2f}km -> ์ ์ ํ {np.nanmean(np.abs(s_w - tru_w)):.2f}km\")\n",
"stop_true = int(clean[\"stopped_true\"].sum()); stop_w = int(w[\"is_stop\"].sum())\n",
"print(f\" ์ ์ฐจ ๊ธฐ๋ก ๋ณด์กด: ์ค์ ์ ์ฐจ {stop_true}ํ(๋น๊ณํ ์ ๋น {int((clean.stop_cause == 'maint').sum())}, ๊ธด๊ธ ๋ถ์ถ ๋๊ธฐ {int((clean.stop_cause == 'urgent').sum())}) / ์ ์ ํ ์ ์ฐจ ํ {stop_w}ํ(์ฐจ์ด {stop_true - stop_w}ํ์ ์ ์ก ๋๋ฝ๋ถ) -> ์ ์ฐจ๋ฅผ ์ด์์น๋ก ์ญ์ ํ์ง ์์\")\n",
"print(\"[4] ์ ํ ๊ธฐ์ ํต๊ณ(speed)\")\n",
"st = pd.DataFrame({\"์ค์ผ\": dirty[\"speed\"].describe(), \"์ ์ ํ\": w[\"speed\"].describe(), \"์๋ณธ\": clean[\"speed\"].describe()}).loc[[\"count\", \"mean\", \"min\", \"max\"]].round(1)\n",
"print(st.to_string())\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"๐ก [์ฐธ๊ณ ] ๋ฏธ์
02 ์์ ์คํ ๊ฒฐ๊ณผ (Output)
\n",
"\n",
"```text\n",
"[0] ์ค์ผ ๋ฐ์ดํฐ 1601ํ(์๋ณธ 1603ํ): ์ขํ sentinel 8, GPS ์ ํ 8, ์๋ ์์ 6ยท๋จ์ ์ค๋ฅ 6ยท๊ฒฐ์ธก 8, ๋ฐฉํฅ ์ด์ 8, ์ผ์ sentinel 14, ๋๋ฝ 12, ์ค๋ณต 10, ์์ ๋ค์์\n",
"\n",
"[1] ์ ์ ๊ท์น ์ ์ฉ ๋ก๊ทธ\n",
" ๊ท์น ๋ด์ฉ ์ฒ๋ฆฌ ๊ฑด์\n",
" 1 ์๊ฐ์ ์ ๋ ฌยท(trip,t) ์ค๋ณต ์ ๊ฑฐ 10\n",
" 2 ์ผ์ sentinel(-999) -> ๊ฒฐ์ธก 14\n",
" 3 ์ขํ ๋ฒ์ ๋ฐ(sentinel ๋ฑ) -> ๊ฒฐ์ธก 9\n",
" 4 GPS ์ ํ(์ด์ ์ค์๊ฐ์์ 8km ์ด๊ณผ) -> ๊ฒฐ์ธก 7\n",
" 5 ์ขํ ๊ฒฐ์ธก -> ์๊ฐ ์ ํ ๋ณด๊ฐ 16\n",
" 6 ๋ณด๊ณ ์๋์ ์์น ์ฐจ๋ถ ์๋ 30km/h ์ด๊ณผ ๋ถ์ผ์น ํ์ 14\n",
" 7 ์๋ ์์ยท120km/h ์ด๊ณผ -> ๊ฒฐ์ธก 12\n",
" 8 ์๋ ๊ฒฐ์ธก -> ์์น ์ฐจ๋ถ ์๋ ๋์ฒด 20\n",
" 9 ๋ฐฉํฅ ๋ฒ์ ๋ฐยท๊ฒฐ์ธก -> ์์น ์ฐจ๋ถ ๋ฐฉํฅ 8\n",
" 10 ์ ์ฐจ ํ ๋ณด์กด ํ์ยท๋๋ฝ ๊ตฌ๊ฐ ํ์(์ญ์ ์์) 94\n",
" ์ ์ ๊ท์น 10์ข
์ ์ฉ(๋ชฉํ 10์ข
: ๋ฌ์ฑ)\n",
"\n",
"[2] ์ ํฉ์ฑ ๊ฒ์ฆ\n",
" [PASS] (trip,t) ์ ์ผ\n",
" [PASS] ์๊ฐ ๋จ์กฐ ์ฆ๊ฐ\n",
" [PASS] ์ขํ ๋ฒ์\n",
" [PASS] ์๋ 0~120\n",
" [PASS] ๋ฐฉํฅ 0~360\n",
" [PASS] ํต์ ๊ฐ๋ ๋ฒ์\n",
" [PASS] ํ๋ฌผ ์จ๋ ๋ฒ์\n",
" [PASS] ํ์ ์ปฌ๋ผ ๊ฒฐ์ธก 0\n",
" ํต๊ณผ์จ 100% (๋ชฉํ 100%)\n",
"\n",
"[3] ์งํ๊ฑฐ๋ฆฌ ์ค์ฐจ(MAE): ์ค์ผ 0.63km -> ์ ์ ํ 0.03km\n",
" ์ ์ฐจ ๊ธฐ๋ก ๋ณด์กด: ์ค์ ์ ์ฐจ 84ํ(๋น๊ณํ ์ ๋น 67, ๊ธด๊ธ ๋ถ์ถ ๋๊ธฐ 17) / ์ ์ ํ ์ ์ฐจ ํ 82ํ(์ฐจ์ด 2ํ์ ์ ์ก ๋๋ฝ๋ถ) -> ์ ์ฐจ๋ฅผ ์ด์์น๋ก ์ญ์ ํ์ง ์์\n",
"[4] ์ ํ ๊ธฐ์ ํต๊ณ(speed)\n",
" ์ค์ผ ์ ์ ํ ์๋ณธ\n",
"count 1593.0 1591.0 1603.0\n",
"mean 42.4 35.4 35.4\n",
"min -42.1 0.0 0.0\n",
"max 2522.8 74.1 74.1\n",
"```\n",
" \n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## ๐ฏ ๋ฏธ์
03. ํ์ฌ ์๋ ๊ธฐ๋ฐ ๋จ์ ๋์ฐฉ ์์ ์๊ฐ(ETA) ๊ณ์ฐ Baseline ์๊ณ ๋ฆฌ์ฆ ๊ตฌํ\n",
"**์์์๊ฐ**: ์ฝ 2.5์๊ฐ\n",
"\n",
"### ๐ ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ์ํ ์ง์นจ\n",
"#### ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ๋น์์ฑ\n",
"\n",
"ํ์ฌ ์๋ ๊ธฐ๋ฐ ๋จ์ ๋์ฐฉ ์์ ์๊ฐ(ETA) ๊ณ์ฐ Baseline ์๊ณ ๋ฆฌ์ฆ์ ๊ตฌํํ๋ค. ์ค์ ์ ์ ํ์ฅ์์๋ ๋จ์ ํ์ง์ ๊ทธ์น์ง ์๊ณ ์ ์ํ ์ํ ํ๋จ๊ณผ ์ ๋ขฐ๋ ๋์ ์์ฌ๊ฒฐ์ ์ด ํ์์ ์ด๋ฏ๋ก, **ํ์ฌ ์๋๋ก ๋จ์ ๊ฑฐ๋ฆฌ๋ฅผ ๋๋๋ ๋จ์ ETA๋ฅผ Baseline์ผ๋ก ๋ง๋ค๊ณ , EDA๋ก Baseline์ ํ๊ณ์ ํต์ฌ ์ง์ฐ ์์ธ์ ์ฐพ๋๋ค.** ๊ณผ์
์ ์์ํ์ฌ ์คํ์ ๋ฐฉ์งํ๊ณ ์์ ๋์๋ ฅ์ ๊ทน๋ํํด์ผ ํฉ๋๋ค.\n",
"\n",
"์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ณธ ๋ฏธ์
์์๋ **๋จ์ ๊ฑฐ๋ฆฌ๋ฅผ ํ์ฌ ์๋๋ก ๋๋ ETA์ MAPE์ MAE๋ฅผ ๊ณ์ฐํ๊ณ , ์ ์ฐจยท์ ์ฐจ ์ด๋ ฅยท์ฐ์ฒยท๋นํฌ์ฅยท์ํ ๊ตฌ์ญยท๋
ธ์ ์ดํ ์กฐ๊ฑด๋ณ ์ค์ฐจ๋ฅผ ๋น๊ตํด ๊ณผ์ยท๊ณผ๋ ์์ธก ์กฐ๊ฑด์ ์ฐพ๋๋ค ๊ณผ์ ์ ์งํํ๋** ์ค์ต์ ์ฒด๊ณ์ ์ผ๋ก ์ํํฉ๋๋ค.\n",
"\n",
"ํ์ต์๋ ์ค์ต๊ฐ์ด๋์ ์ ์๋ ์ด 4๊ฐ ์ธ๋ถ ํ๋(๊ธฐ๋ณธ ๊ธฐ๋ฅ ๊ตฌํ โ ๊ฒฐ๊ณผ ์งํ ๊ฒ์ฆ โ ํ๋ผ๋ฏธํฐ ํ๋ ๋ฐ ๋์ ๊ณผ์ )์ ์์ฐจ์ ์ผ๋ก ์งํํ๋ฉฐ ํต์ฌ ํ์ ๊ธฐ์ค์ ๋ฌ์ฑํฉ๋๋ค.\n",
"\n",
"**์ด๋ฒ ๊ฒฐ๊ณผ๋ฌผ:** `notebooks/mov06_eda_analysis.ipynb ๋ฐ EDA ์์ฝ ๋ฆฌํฌํธ` ํํ์ ๊ฒฐ๊ณผ๋ฌผ์ ์์ฑํ๋ค.\n",
"\n",
"### ๐ ํ๊ฐ ๋ฐ ๊ฒ์ฆ ๊ธฐ์ค (PASS ์กฐ๊ฑด)\n",
"#### ๋ฏธ์
ํ์ \n",
"\n",
"**ํ๊ฐ ๊ตฌ๋ถ:** ํ์\n",
"\n",
"**ํต์ฌ ํ๊ฐ์์ญ:** ๊ตฐ์ ์์ฌ๊ฒฐ์ ์ ์ง๊ด์ ์ธ ์ธ์ฌ์ดํธ ์ ๊ณต ์์ค\n",
"\n",
"#### ๋ฏธ์
PASS ์กฐ๊ฑด\n",
"\n",
"- ์ ๋ ๊ธฐ์ค: EDA ์๊ฐํ ์ฐจํธ 8์ข
์ด์ ์์ฑ, ์ ์๋ฏธํ ์๊ด๊ด๊ณ ํผ์ฒ 5๊ฐ ์ด์ ๋์ถ\n",
"\n",
"\n",
"- ์งํ ๊ธฐ์ค: EDA ๋
ธํธ๋ถ ์คํ ์๋ฃ ๋ฐ ํต์ฌ ๋ฐ๊ฒฌ์ (Key Findings) ๋ฌธ์ํ\n",
"\n",
"\n",
"- ๊ฒ์ฆ ๋ฐฉ๋ฒ: ํต๊ณ์ ๊ฐ์ค ๊ฒ์ (p-value, ์๊ด๊ณ์)์ ํตํ ์ ์์ฑ ๊ฒ์ฆ\n",
"\n",
"\n",
"#### ์ฑ์ ์ ํ์ ๋ฉ๋ชจ\n",
"\n",
"> ๋จ์ ์ฝ๋ ์คํ ์ฌ๋ถ๋ฟ๋ง ์๋๋ผ ์ถ๋ ฅ๋ ์์น์ ๊ทผ๊ฑฐ ์๋ฃ๊ฐ ๊ตฐ ์ค๋ฌด ์๊ตฌ์ฌํญ์ ๋ถํฉํ๋์ง ๋ฉด๋ฐํ ํ์ธํ๋ค.\n",
"\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"\"\"\"ํ์ฌ ์๋ ๊ธฐ๋ฐ ๋จ์ ETA(๋จ์ ๊ฑฐ๋ฆฌ / ํ์ฌ ์๋) Baseline์ ๋ง๋ค์ด ์ค์ฐจ๋ฅผ ์ธก์ ํ๊ณ , ์ํฉ๋ณ ์ค์ฐจ ๋น๊ตยทEDA ์ฐจํธ 8์ข
ยท์๊ด ํผ์ฒยทํ๋ ํ ๋ถ์์ผ๋ก Baseline์ ํ๊ณ์ ํต์ฌ ์ง์ฐ ์์ธ์ ์ฐพ๋๋ค.\"\"\"\n",
"import numpy as np\n",
"import pandas as pd\n",
"import matplotlib.pyplot as plt\n",
"from scipy import stats\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# [๊ต์ก์ฉ๊ฐ์์๋ฃ] ๊ฐ์ ์์ก ๋
ธ์ ยท์์ก์ ๋จ ํ
๋ ๋ฉํธ๋ฆฌ ์์ฑ๊ธฐ (์ค์ ๋๋กยท์ง๋ช
ยท๋ถ๋ ์์)\n",
"# ๊ฐ์ ๋
ธ์ (์ถ๋ฐ ๊ฑฐ์ -> ๋์ฐฉ ๊ฑฐ์ , 5๊ฐ ๊ตฌ๊ฐ)์ 5๋ถ ๊ฐ๊ฒฉ์ผ๋ก ๋ณด๊ณ ํ๋ ์ ๋จ GPSยทํ๋ฌผ ์ผ์ยทํต์ ์ํ ์๋ฃ๋ฅผ ๋ง๋ ๋ค.\n",
"# ---------------------------------------------------------------------------\n",
"PTS = [(0, 0), (30, 12), (62, 14), (88, 38), (112, 42), (135, 66)] # ๋
ธ์ ๊ผญ์ง์ (km, ๊ฐ์ ์ขํ)\n",
"ROUTE = pd.DataFrame({\"seg\": [\"R1\", \"R2\", \"R3\", \"R4\", \"R5\"], \"road\": [\"paved\", \"unpaved\", \"paved\", \"paved\", \"unpaved\"],\n",
" \"limit\": [70, 50, 70, 50, 50], \"kind\": [\"์ผ๋ฐ\", \"์ผ๋ฐ\", \"์ผ๋ฐ\", \"๊ต๋ยทํฐ๋\", \"์ผ๋ฐ\"], \"threat\": [0, 0, 1, 0, 0]})\n",
"ROUTE[\"x0\"], ROUTE[\"y0\"] = [p[0] for p in PTS[:-1]], [p[1] for p in PTS[:-1]]\n",
"ROUTE[\"x1\"], ROUTE[\"y1\"] = [p[0] for p in PTS[1:]], [p[1] for p in PTS[1:]]\n",
"ROUTE[\"km\"] = np.hypot(ROUTE[\"x1\"] - ROUTE[\"x0\"], ROUTE[\"y1\"] - ROUTE[\"y0\"])\n",
"ROUTE[\"c0\"] = np.r_[0, np.cumsum(ROUTE[\"km\"])[:-1]]\n",
"ROUTE[\"c1\"] = np.cumsum(ROUTE[\"km\"])\n",
"LEN = float(ROUTE[\"km\"].sum())\n",
"ROAD_F = {\"paved\": 1.0, \"unpaved\": 0.75}\n",
"C1L, C0L, KML = ROUTE[\"c1\"].tolist(), ROUTE[\"c0\"].tolist(), ROUTE[\"km\"].tolist() # ๋ฐ๋ณต ๊ณ์ฐ์ฉ ๋ฆฌ์คํธ(์๋)\n",
"BASE_V = [l * ROAD_F[r] for l, r in zip(ROUTE[\"limit\"], ROUTE[\"road\"])]\n",
"THR, UNP, TUN = ROUTE[\"threat\"].tolist(), (ROUTE[\"road\"] == \"unpaved\").tolist(), (ROUTE[\"kind\"] == \"๊ต๋ยทํฐ๋\").tolist()\n",
"\n",
"\n",
"def seg_of(s):\n",
" # [TODO : ํ์ ์ค์ต 3] ํ์ฌ ์๋ ๊ธฐ๋ฐ ๋จ์ ๋์ฐฉ ์์ ์๊ฐ(ETA) ๊ณ์ฐ Baseline ์๊ณ ๋ฆฌ์ฆ ๊ตฌํ ๊ด๋ จ ํต์ฌ ๋ก์ง์ ๊ตฌํํ์ธ์.\n",
" # ํํธ: ์๋จ ์ฃผ์ ๋ฐ ๋ฐ์ดํฐ ์คํค๋ง/์ง์นจ์ ์ฐธ๊ณ ํ์ฌ ์์ฑํฉ๋๋ค.\n",
" # <<< ์ฌ๊ธฐ์ ์ฝ๋๋ฅผ ์์ฑํ์ธ์ >>>\n",
" pass # ์์ฑ ํ pass๋ฅผ ์ ๊ฑฐํ์ธ์.\n",
"def route_xy(s):\n",
" \"\"\"๋
ธ์ ์ ์งํ๊ฑฐ๋ฆฌ s(km)์ ์ขํ์ ์งํ ๋ฐฉํฅ(๋).\"\"\"\n",
" # [TODO : ํ์ ์ค์ต 3] ํ์ฌ ์๋ ๊ธฐ๋ฐ ๋จ์ ๋์ฐฉ ์์ ์๊ฐ(ETA) ๊ณ์ฐ Baseline ์๊ณ ๋ฆฌ์ฆ ๊ตฌํ ๊ด๋ จ ํต์ฌ ๋ก์ง์ ๊ตฌํํ์ธ์.\n",
" # ํํธ: ์๋จ ์ฃผ์ ๋ฐ ๋ฐ์ดํฐ ์คํค๋ง/์ง์นจ์ ์ฐธ๊ณ ํ์ฌ ์์ฑํฉ๋๋ค.\n",
" # <<< ์ฌ๊ธฐ์ ์ฝ๋๋ฅผ ์์ฑํ์ธ์ >>>\n",
" pass # ์์ฑ ํ pass๋ฅผ ์ ๊ฑฐํ์ธ์.\n",
"def project(x, y):\n",
" \"\"\"GPS ์ขํ๋ฅผ ๋
ธ์ ์ ํฌ์ํด (์งํ๊ฑฐ๋ฆฌ km, ๋
ธ์ ๊ณผ์ ์์ง ์ดํ km)๋ฅผ ๊ตฌํ๋ค.\"\"\"\n",
" x, y = np.atleast_1d(x).astype(float), np.atleast_1d(y).astype(float)\n",
" best_s, best_d = np.zeros(len(x)), np.full(len(x), np.inf)\n",
" for r in ROUTE.itertuples():\n",
" dx, dy = r.x1 - r.x0, r.y1 - r.y0\n",
" f = np.clip(((x - r.x0) * dx + (y - r.y0) * dy) / (dx * dx + dy * dy), 0, 1)\n",
" d = np.hypot(x - (r.x0 + f * dx), y - (r.y0 + f * dy))\n",
" better = d < best_d\n",
" best_s, best_d = np.where(better, r.c0 + f * r.km, best_s), np.where(better, d, best_d)\n",
" return best_s, best_d\n",
"\n",
"\n",
"def cong(clock_min):\n",
" h = (clock_min / 60) % 24\n",
" return 0.82 if (7 <= h < 9 or 17 <= h < 19) else 1.0 # ์ถํด๊ทผ ์๊ฐ๋ ์ ์ฒด ๊ณ์(๊ฐ์ )\n",
"\n",
"\n",
"def simulate_trip(rng, tid, start_min, rain, size, cargo):\n",
" \"\"\"1๋ถ ๋จ์๋ก ์ด๋์ ๋ชจ์ํ๊ณ 5๋ถ๋ง๋ค ํ
๋ ๋ฉํธ๋ฆฌ๋ฅผ ๋จ๊ธด๋ค. ์ดํยท๋ถ์ ์ ์ฐจ ์ด๋ฒคํธ๋ ์ ๋ต ๋ผ๋ฒจ๋ก ํจ๊ป ์ ์ฅํ๋ค.\"\"\"\n",
" s, t, stop_left, rows = 0.0, 0, 0, []\n",
" dev = (rng.random() < 0.12)\n",
" dev_t0 = int(rng.integers(30, 120)) if dev else -1\n",
" dev_len = int(rng.integers(12, 25)) if dev else 0\n",
" cause = \"\"\n",
" while s < LEN:\n",
" i = seg_of(s)\n",
" v = BASE_V[i] * (0.88 if rain else 1.0) * (0.85 if (rain and UNP[i]) else 1.0) * cong(start_min + t) \\\n",
" * (0.7 if THR[i] else 1.0) * (1 - 0.012 * size) * rng.normal(1, 0.06)\n",
" off = 0.0\n",
" if dev and dev_t0 <= t < dev_t0 + dev_len:\n",
" ph = (t - dev_t0) / dev_len\n",
" off, v = 4.0 * (1 - abs(2 * ph - 1)), v * 0.4 # ๋
ธ์ ์ดํ(์ผ๊ฐํ ํํ๋ก ์ต๋ 4km)\n",
" if stop_left > 0:\n",
" v, stop_left = 0.0, stop_left - 1\n",
" elif rng.random() < 0.0015:\n",
" stop_left, cause = int(rng.integers(8, 36)), (\"maint\" if rng.random() < 0.7 else \"urgent\")\n",
" s += v / 60\n",
" t += 1\n",
" if t % 5 == 0 or s >= LEN:\n",
" x, y, hd = route_xy(min(s, LEN))\n",
" nx, ny = np.cos(np.radians(hd)), -np.sin(np.radians(hd))\n",
" j = seg_of(min(s, LEN)); tun = TUN[j]\n",
" rows.append(dict(trip=tid, t=t, clock=(start_min + t) % 1440, x=x + off * nx + rng.normal(0, 0.03), y=y + off * ny + rng.normal(0, 0.03),\n",
" speed=max(v * rng.normal(1, 0.03), 0.0), heading=(hd + rng.normal(0, 3)) % 360, rssi=-62 - 14 * tun + rng.normal(0, 3),\n",
" cargo_temp=18 + 3 * np.sin(t / 40) + rng.normal(0, 0.3), shock_g=abs(rng.normal(0.25, 0.1)) * (2.0 if UNP[j] else 1.0),\n",
" s_true=min(s, LEN), off_true=off, stopped_true=int(v == 0.0), rain=int(rain), size=size, cargo=cargo, stop_cause=cause if v == 0.0 else \"\"))\n",
" arr = t\n",
" for r in rows:\n",
" r[\"eta_true\"] = arr - r[\"t\"]\n",
" return rows\n",
"\n",
"\n",
"def make_telemetry(seed=21, n_trips=200):\n",
" \"\"\"n_trips๊ฐ ์์ก์ ๋จ ์ด๋ ๊ธฐ๋ก(๋ ์ง ์). ์ ๋จ ๊ท๋ชจ 3~12๋, ํ๋ฌผ ๊ตฌ๋ถ ์ผ๋ฐ/๋์ฅ/๋ฏผ๊ฐ.\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" rows = []\n",
" for k in range(n_trips):\n",
" rows += simulate_trip(rng, k, int(rng.integers(5 * 60, 15 * 60)), bool(rng.random() < 0.2), int(rng.integers(3, 13)), str(rng.choice([\"์ผ๋ฐ\", \"๋์ฅ\", \"๋ฏผ๊ฐ\"], p=[.6, .25, .15])))\n",
" return pd.DataFrame(rows)\n",
"\n",
"\n",
"def mape(y, p):\n",
" y, p = np.asarray(y, float), np.asarray(p, float)\n",
" return float(np.mean(np.abs(y - p) / np.abs(y)) * 100)\n",
"\n",
"\n",
"def mae(y, p):\n",
" return float(np.mean(np.abs(np.asarray(y, float) - np.asarray(p, float))))\n",
"# ---------------------------------------------------------------------------\n",
"# ---------------------------------------------------------------------------\n",
"# ETA ์์ธก ํผ์ฒ: ๊ด์ธก ์์ ๊น์ง์ ํ
๋ ๋ฉํธ๋ฆฌ์ ๋จ์ ๋
ธ์ ๊ตฌ์ฑ(ํฌ์ฅยท์ํยท๊ต๋ํฐ๋ ๊ตฌ๊ฐ ๊ฑฐ๋ฆฌ)๋ง ์ฌ์ฉ\n",
"# ---------------------------------------------------------------------------\n",
"FEATS = [\"remaining_km\", \"progress\", \"speed\", \"speed_ma3\", \"speed_std3\", \"stop_n\", \"stop_min\", \"since_stop\", \"hour_sin\", \"hour_cos\", \"cong\", \"rain\", \"size\",\n",
" \"k_๋์ฅ\", \"k_๋ฏผ๊ฐ\", \"rem_unpaved_km\", \"rem_threat_km\", \"rem_bridge_km\", \"off_km\", \"rssi\", \"base_eta\"]\n",
"\n",
"\n",
"def rem_composition(s):\n",
" \"\"\"์งํ๊ฑฐ๋ฆฌ s ์ดํ ๋จ์ ๊ตฌ๊ฐ ์ค ๋นํฌ์ฅยท์ํยท๊ต๋ํฐ๋ ๊ฑฐ๋ฆฌ(km).\"\"\"\n",
" left = np.clip(ROUTE[\"c1\"].values[None, :] - np.maximum(np.atleast_1d(s)[:, None], ROUTE[\"c0\"].values[None, :]), 0, None)\n",
" return left @ np.array(UNP, float), left @ np.array(THR, float), left @ np.array(TUN, float)\n",
"\n",
"\n",
"def add_features(df):\n",
" d = df.sort_values([\"trip\", \"t\"]).copy()\n",
" d[\"s_obs\"], d[\"off_km\"] = project(d[\"x\"].values, d[\"y\"].values)\n",
" d[\"remaining_km\"] = LEN - d[\"s_obs\"]\n",
" d[\"progress\"] = d[\"s_obs\"] / LEN\n",
" g = d.groupby(\"trip\")[\"speed\"]\n",
" d[\"speed_ma3\"] = g.rolling(3, min_periods=1).mean().reset_index(level=0, drop=True)\n",
" d[\"speed_std3\"] = g.rolling(3, min_periods=1).std().reset_index(level=0, drop=True).fillna(0)\n",
" d[\"is_stop\"] = (d[\"speed\"] < 3).astype(int)\n",
" new_stop = (d[\"is_stop\"] == 1) & (d.groupby(\"trip\")[\"is_stop\"].shift(1).fillna(0) == 0)\n",
" d[\"stop_n\"] = new_stop.astype(int).groupby(d[\"trip\"]).cumsum()\n",
" d[\"stop_min\"] = d.groupby(\"trip\")[\"is_stop\"].cumsum() * 5\n",
" blk = (d[\"is_stop\"] == 1).astype(int).groupby(d[\"trip\"]).cumsum() # ์ ์ฐจ ๋์ ํ์๋ก ๊ตฌ๊ฐ์ ๋๋์ด ๋ง์ง๋ง ์ ์ฐจ ์ดํ ๊ฒฝ๊ณผ ๊ณ์ฐ\n",
" d[\"since_stop\"] = d.groupby([\"trip\", blk]).cumcount() * 5\n",
" d[\"hour_sin\"], d[\"hour_cos\"] = np.sin(2 * np.pi * d[\"clock\"] / 1440), np.cos(2 * np.pi * d[\"clock\"] / 1440)\n",
" d[\"cong\"] = d[\"clock\"].map(cong)\n",
" d[\"k_๋์ฅ\"], d[\"k_๋ฏผ๊ฐ\"] = (d[\"cargo\"] == \"๋์ฅ\").astype(int), (d[\"cargo\"] == \"๋ฏผ๊ฐ\").astype(int)\n",
" d[\"rem_unpaved_km\"], d[\"rem_threat_km\"], d[\"rem_bridge_km\"] = rem_composition(d[\"s_obs\"].values)\n",
" d[\"base_eta\"] = np.minimum(d[\"remaining_km\"] / np.maximum(d[\"speed\"], 5.0) * 60, 600) # ํ์ฌ ์๋ ๊ธฐ๋ฐ ๋จ์ ETA(๋ถ, ์ ์ฐจ ์ ํญ์ฃผ ๋ฐฉ์ง๋ก 5km/h ํํยท600๋ถ ์ํ)\n",
" return d.reset_index(drop=True)\n",
"# ---------------------------------------------------------------------------\n",
"\n",
"df = make_telemetry(); d = add_features(df)\n",
"d = d[d[\"eta_true\"] >= 15].copy() # ๋์ฐฉ 15๋ถ ์ด๋ด๋ ์๋์ค์ฐจ๊ฐ ๊ณผ๋ํด์ ธ ํ๊ฐ์์ ์ ์ธ\n",
"d[\"err\"] = d[\"base_eta\"] - d[\"eta_true\"] # ์์ = ๋ฆ๊ฒ ๋์ฐฉํ๋ค๊ณ ๊ณผ๋ ์์ธก\n",
"print(f\"[1] Baseline ETA(๋จ์ ๊ฑฐ๋ฆฌ/ํ์ฌ ์๋) ํ๊ฐ {len(d)}ํ: MAPE {mape(d.eta_true, d.base_eta):.1f}%, MAE {mae(d.eta_true, d.base_eta):.1f}๋ถ, ํ๊ท ์ค์ฐจ {d.err.mean():+.1f}๋ถ\")\n",
"cond = {\"์ ์ฐจ ์ค(์๋ 3 ๋ฏธ๋ง)\": d.is_stop == 1, \"์ง์ ์ ์ฐจ ์ด๋ ฅ ์์\": d.stop_n > 0, \"์ฐ์ฒ\": d.rain == 1, \"๋นํฌ์ฅ ๋จ์ ๊ฑฐ๋ฆฌ 40km ์ด์\": d.rem_unpaved_km >= 40,\n",
" \"์ํ(์ ์ฒด) ๊ตฌ์ญ ์\": d.rem_threat_km > 0, \"๋
ธ์ ์ดํ 1km ์ด์\": d.off_km >= 1, \"๊ทธ ์ธ ํ์\": (d.is_stop == 0) & (d.stop_n == 0) & (d.rain == 0) & (d.off_km < 1)}\n",
"tab = pd.DataFrame([(k, int(m.sum()), mae(d.eta_true[m], d.base_eta[m]), d.err[m].mean()) for k, m in cond.items()], columns=[\"์กฐ๊ฑด\", \"๊ฑด์\", \"MAE(๋ถ)\", \"ํ๊ท ์ค์ฐจ(๋ถ)\"]).round(1)\n",
"print(\"[2] ์กฐ๊ฑด๋ณ Baseline ์ค์ฐจ(์์ = ๊ณผ๋ ์์ธก)\")\n",
"print(tab.to_string(index=False))\n",
"print(\" -> ์ ์ฐจยท์ง์ ์ ์ฐจ ์ด๋ ฅยท๋นํฌ์ฅยท์ํ ๊ตฌ์ญ์ด ์์ผ๋ฉด ํ์ฌ ์๋๋ง์ผ๋ก๋ ๋จ์ ์๊ฐ์ ํฌ๊ฒ ์๋ชป ๋ณธ๋ค(ํ๊ณ).\")\n",
"\n",
"rows = []\n",
"for f in FEATS:\n",
" if f == \"base_eta\":\n",
" continue\n",
" r, p = stats.spearmanr(d[f], d[\"err\"].abs())\n",
" rows.append((f, r, p))\n",
"cor = pd.DataFrame(rows, columns=[\"ํผ์ฒ\", \"์๊ด\", \"p\"]).assign(a=lambda x: x[\"์๊ด\"].abs()).sort_values(\"a\", ascending=False)\n",
"sig = cor[(cor.a >= 0.2) & (cor.p < 0.05)]\n",
"print(f\"\\n[3] Baseline ์ ๋์ค์ฐจ์ ์ ์ํ ์๊ด(|r|>=0.2, p<0.05) ํผ์ฒ {len(sig)}๊ฐ (๋ชฉํ 5๊ฐ ์ด์: {'๋ฌ์ฑ' if len(sig) >= 5 else '๋ฏธ๋ฌ'})\")\n",
"print(sig.head(6)[[\"ํผ์ฒ\", \"์๊ด\", \"p\"]].round(3).to_string(index=False))\n",
"print(\" (๋จ์ ๊ฑฐ๋ฆฌยท์งํ๋ฅ ยท๋จ์ ๊ตฌ๊ฐ ๊ตฌ์ฑ์ '๋ฉ์๋ก ์ค์ฐจ๊ฐ ํผ'์ ๋ฐ์ํ๋ฏ๋ก ํด์ ๋ ๊ฑฐ๋ฆฌ ํจ๊ณผ์ ๋ถ๋ฆฌํด์ ๋ณธ๋ค)\")\n",
"a, b = d.loc[d.stop_n > 0, \"err\"], d.loc[d.stop_n == 0, \"err\"]\n",
"t, p = stats.ttest_ind(a, b, equal_var=False)\n",
"print(f\" ๊ฐ์ค๊ฒ์ : ์ ์ฐจ ์ด๋ ฅ ์์ vs ์์ ์ค์ฐจ ํ๊ท {a.mean():+.1f}๋ถ vs {b.mean():+.1f}๋ถ, t={t:.1f}, p={p:.2g}\")\n",
"\n",
"trip_err = d.groupby(\"trip\")[\"err\"].apply(lambda s: s.abs().sum()).sort_values(ascending=False)\n",
"top = int(np.ceil(len(trip_err) * 0.2))\n",
"print(f\"\\n[4] ํ๋ ํ : ์ค์ฐจ ์์ 20% ์ด๋({top}๊ฑด)์ด ์ ์ฒด ETA ์ค์ฐจ์ {trip_err.iloc[:top].sum() / trip_err.sum():.0%}๋ฅผ ์ฐจ์ง -> ์ง์ฐ ์ด๋ ์ง์ค ๊ด๋ฆฌ ๋์\")\n",
"\n",
"fig, ax = plt.subplots(2, 4, figsize=(18, 8))\n",
"for tid in (3, 17, 30):\n",
" q = df[df.trip == tid]; ax[0, 0].plot(q.t, q.s_true, label=f\"trip {tid}\")\n",
"ax[0, 0].set_title(\"1 Progress by trip (km)\"); ax[0, 0].legend()\n",
"ax[0, 1].hist(df[\"speed\"], bins=40); ax[0, 1].set_title(\"2 Reported speed distribution\")\n",
"ax[0, 2].scatter(d[\"eta_true\"], d[\"base_eta\"], s=4, alpha=.3); ax[0, 2].plot([0, 400], [0, 400], \"r--\"); ax[0, 2].set_ylim(0, 650); ax[0, 2].set_title(\"3 Baseline ETA vs actual\")\n",
"ax[0, 3].hist(d[\"err\"].clip(-200, 300), bins=40); ax[0, 3].set_title(\"4 Baseline error (min)\")\n",
"ax[1, 0].boxplot([d.loc[d.rain == 0, \"err\"].clip(-200, 300), d.loc[d.rain == 1, \"err\"].clip(-200, 300)]); ax[1, 0].set_xticklabels([\"dry\", \"rain\"]); ax[1, 0].set_title(\"5 Rain effect on error\")\n",
"by_seg = df.assign(seg=[seg_of(s) for s in df.s_true]).groupby(\"seg\")[\"speed\"].mean(); ax[1, 1].bar([f\"R{i + 1}\" for i in by_seg.index], by_seg.values); ax[1, 1].set_title(\"6 Mean speed by route segment\")\n",
"hs = df.groupby(df[\"clock\"] // 60)[\"speed\"].mean(); ax[1, 2].plot(hs.index, hs.values, \"o-\"); ax[1, 2].set_title(\"7 Mean speed by hour of day\")\n",
"ax[1, 3].bar(range(len(trip_err)), (trip_err / trip_err.sum()).values[:], width=1.0); ax[1, 3].set_title(\"8 Pareto: error by trip (sorted)\")\n",
"plt.suptitle(\"MOV06 EDA (synthetic telemetry)\"); plt.tight_layout(); plt.show()\n",
"print(\"\\n[5] EDA ์ฐจํธ 8์ข
์์ฑ ์๋ฃ(๋ชฉํ 8์ข
์ด์: ๋ฌ์ฑ)\")\n",
"print(\"ํต์ฌ ๋ฐ๊ฒฌ: ์ ์ฐจ์ ๋นํฌ์ฅยท์ํ ๊ตฌ์ญ์ด ๋จ์ ์๊ฐ์ ๋๋ฆฌ์ง๋ง ํ์ฌ ์๋์๋ ๋ฐ์๋์ง ์๋๋ค. ์ด๋ M4 ํผ์ฒ ์ค๊ณ(๋จ์ ๊ตฌ๊ฐ ๊ตฌ์ฑ, ์ ์ฐจ ์ด๋ ฅ)์ ๊ทผ๊ฑฐ๊ฐ ๋๋ค.\")\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"๐ก [์ฐธ๊ณ ] ๋ฏธ์
03 ์์ ์คํ ๊ฒฐ๊ณผ (Output)
\n",
"\n",
"```text\n",
"[1] Baseline ETA(๋จ์ ๊ฑฐ๋ฆฌ/ํ์ฌ ์๋) ํ๊ฐ 9615ํ: MAPE 31.7%, MAE 45.0๋ถ, ํ๊ท ์ค์ฐจ +0.8๋ถ\n",
"[2] ์กฐ๊ฑด๋ณ Baseline ์ค์ฐจ(์์ = ๊ณผ๋ ์์ธก)\n",
" ์กฐ๊ฑด ๊ฑด์ MAE(๋ถ) ํ๊ท ์ค์ฐจ(๋ถ)\n",
" ์ ์ฐจ ์ค(์๋ 3 ๋ฏธ๋ง) 359 332.3 332.3\n",
" ์ง์ ์ ์ฐจ ์ด๋ ฅ ์์ 1872 83.7 57.3\n",
" ์ฐ์ฒ 2713 52.6 -0.4\n",
"๋นํฌ์ฅ ๋จ์ ๊ฑฐ๋ฆฌ 40km ์ด์ 3324 70.6 -4.8\n",
" ์ํ(์ ์ฒด) ๊ตฌ์ญ ์ 5986 57.5 -0.3\n",
" ๋
ธ์ ์ดํ 1km ์ด์ 63 291.5 291.5\n",
" ๊ทธ ์ธ ํ์ 5656 29.9 -14.6\n",
" -> ์ ์ฐจยท์ง์ ์ ์ฐจ ์ด๋ ฅยท๋นํฌ์ฅยท์ํ ๊ตฌ์ญ์ด ์์ผ๋ฉด ํ์ฌ ์๋๋ง์ผ๋ก๋ ๋จ์ ์๊ฐ์ ํฌ๊ฒ ์๋ชป ๋ณธ๋ค(ํ๊ณ).\n",
"\n",
"[3] Baseline ์ ๋์ค์ฐจ์ ์ ์ํ ์๊ด(|r|>=0.2, p<0.05) ํผ์ฒ 9๊ฐ (๋ชฉํ 5๊ฐ ์ด์: ๋ฌ์ฑ)\n",
" ํผ์ฒ ์๊ด p\n",
"rem_unpaved_km 0.657 0.0\n",
" remaining_km 0.640 0.0\n",
" progress -0.640 0.0\n",
" since_stop -0.578 0.0\n",
" rem_bridge_km 0.546 0.0\n",
" rem_threat_km 0.530 0.0\n",
" (๋จ์ ๊ฑฐ๋ฆฌยท์งํ๋ฅ ยท๋จ์ ๊ตฌ๊ฐ ๊ตฌ์ฑ์ '๋ฉ์๋ก ์ค์ฐจ๊ฐ ํผ'์ ๋ฐ์ํ๋ฏ๋ก ํด์ ๋ ๊ฑฐ๋ฆฌ ํจ๊ณผ์ ๋ถ๋ฆฌํด์ ๋ณธ๋ค)\n",
" ๊ฐ์ค๊ฒ์ : ์ ์ฐจ ์ด๋ ฅ ์์ vs ์์ ์ค์ฐจ ํ๊ท +57.3๋ถ vs -12.9๋ถ, t=19.7, p=5.6e-79\n",
"\n",
"[4] ํ๋ ํ : ์ค์ฐจ ์์ 20% ์ด๋(40๊ฑด)์ด ์ ์ฒด ETA ์ค์ฐจ์ 42%๋ฅผ ์ฐจ์ง -> ์ง์ฐ ์ด๋ ์ง์ค ๊ด๋ฆฌ ๋์\n",
"\n",
"[5] EDA ์ฐจํธ 8์ข
์์ฑ ์๋ฃ(๋ชฉํ 8์ข
์ด์: ๋ฌ์ฑ)\n",
"ํต์ฌ ๋ฐ๊ฒฌ: ์ ์ฐจ์ ๋นํฌ์ฅยท์ํ ๊ตฌ์ญ์ด ๋จ์ ์๊ฐ์ ๋๋ฆฌ์ง๋ง ํ์ฌ ์๋์๋ ๋ฐ์๋์ง ์๋๋ค. ์ด๋ M4 ํผ์ฒ ์ค๊ณ(๋จ์ ๊ตฌ๊ฐ ๊ตฌ์ฑ, ์ ์ฐจ ์ด๋ ฅ)์ ๊ทผ๊ฑฐ๊ฐ ๋๋ค.\n",
"```\n",
" \n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## ๐ฏ ๋ฏธ์
04. ๋๋ก ์ํ, ๊ธฐ์, ์ ์ํ ์ ์ฒด ๋ณ์๋ฅผ ๋ฐ์ํ ๋จธ์ ๋ฌ๋ ๊ธฐ๋ฐ ์ ๋ฐ ETA ์์ธก ๋ชจ๋ธ ๊ณ ๋ํ\n",
"**์์์๊ฐ**: ์ฝ 3.0์๊ฐ\n",
"\n",
"### ๐ ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ์ํ ์ง์นจ\n",
"#### ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ๋น์์ฑ\n",
"\n",
"๋๋ก ์ํ, ๊ธฐ์, ์ ์ฒด ๋ณ์๋ฅผ ๋ฐ์ํ ๋จธ์ ๋ฌ๋ ๊ธฐ๋ฐ ์ ๋ฐ ETA ์์ธก ๋ชจ๋ธ์ ๊ณ ๋ํํ๋ค. ์ค์ ์ ์ ํ์ฅ์์๋ ๋จ์ ํ์ง์ ๊ทธ์น์ง ์๊ณ ์ ์ํ ์ํ ํ๋จ๊ณผ ์ ๋ขฐ๋ ๋์ ์์ฌ๊ฒฐ์ ์ด ํ์์ ์ด๋ฏ๋ก, **๋๋ก ์ํ, ๊ธฐ์, ์ ์ฒด, ์ํ ๊ตฌ์ญ๊ณผ ์ ์ฐจ ์ด๋ ฅ์ ๋ฐ์ํ ๋จธ์ ๋ฌ๋ ๊ธฐ๋ฐ ์ ๋ฐ ETA ๋ชจ๋ธ์ ๋ง๋ค๊ณ ๊ธฐ์ค ์ค์ฐจ๋ฅผ ์ธก์ ํ๋ค.** ๊ณผ์
์ ์์ํ์ฌ ์คํ์ ๋ฐฉ์งํ๊ณ ์์ ๋์๋ ฅ์ ๊ทน๋ํํด์ผ ํฉ๋๋ค.\n",
"\n",
"์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ณธ ๋ฏธ์
์์๋ **๋จ์ ๊ฑฐ๋ฆฌ์ ๋จ์ ๊ตฌ๊ฐ ๊ตฌ์ฑ(๋นํฌ์ฅยท์ํยท๊ต๋ํฐ๋), ์ ์ฐจ ํ์ยท์๊ฐ, ์๊ฐ๋ ์ํ, ์๋ ์ด๋ํ๊ท ยทํ์คํธ์ฐจ, ์ดํ ๊ฑฐ๋ฆฌ ๋ฑ ํ์ ํผ์ฒ ์์ฑํ์ฌ ์์ธ์ ๋ถ์ํ๋** ์ค์ต์ ์ฒด๊ณ์ ์ผ๋ก ์ํํฉ๋๋ค.\n",
"\n",
"ํ์ต์๋ ์ค์ต๊ฐ์ด๋์ ์ ์๋ ์ด 4๊ฐ ์ธ๋ถ ํ๋(๊ธฐ๋ณธ ๊ธฐ๋ฅ ๊ตฌํ โ ๊ฒฐ๊ณผ ์งํ ๊ฒ์ฆ โ ํ๋ผ๋ฏธํฐ ํ๋ ๋ฐ ๋์ ๊ณผ์ )์ ์์ฐจ์ ์ผ๋ก ์งํํ๋ฉฐ ํต์ฌ ํ์ ๊ธฐ์ค์ ๋ฌ์ฑํฉ๋๋ค.\n",
"\n",
"**์ด๋ฒ ๊ฒฐ๊ณผ๋ฌผ:** `src/features/mov06_feature_engineering.py ๋ฐ ๊ธฐ์ค ๋ชจ๋ธ` ํํ์ ๊ฒฐ๊ณผ๋ฌผ์ ์์ฑํ๋ค.\n",
"\n",
"### ๐ ํ๊ฐ ๋ฐ ๊ฒ์ฆ ๊ธฐ์ค (PASS ์กฐ๊ฑด)\n",
"#### ๋ฏธ์
ํ์ \n",
"\n",
"**ํ๊ฐ ๊ตฌ๋ถ:** ํ์\n",
"\n",
"**ํต์ฌ ํ๊ฐ์์ญ:** ๊ตฐ์ ์ค๋ฌด์์ ๋ฉ๋ ๊ฐ๋ฅํ ํผ์ฒ ์ค๊ณ์ ๋
ผ๋ฆฌ์ฑ\n",
"\n",
"#### ๋ฏธ์
PASS ์กฐ๊ฑด\n",
"\n",
"- ์ ๋ ๊ธฐ์ค: ํ์ ํผ์ฒ 10๊ฐ ์ด์ ์์ฑ, ๊ธฐ์ค ๋ชจ๋ธ MAPE 25% ์ดํ ๋ฌ์ฑ\n",
"\n",
"\n",
"- ์งํ ๊ธฐ์ค: ํผ์ฒ ํ์ดํ๋ผ์ธ ๋ฐ ๋ฒ ์ด์ค๋ผ์ธ ๋ชจ๋ธ ์ฑ๋ฅ ์งํ ๋์ถ\n",
"\n",
"\n",
"- ๊ฒ์ฆ ๋ฐฉ๋ฒ: ๊ต์ฐจ ๊ฒ์ฆ(Time-series Cross Validation)์ ํตํ ๊ณผ์ ํฉ ๋ฐฉ์ง\n",
"\n",
"\n",
"#### ์ฑ์ ์ ํ์ ๋ฉ๋ชจ\n",
"\n",
"> ๋จ์ ์ฝ๋ ์คํ ์ฌ๋ถ๋ฟ๋ง ์๋๋ผ ์ถ๋ ฅ๋ ์์น์ ๊ทผ๊ฑฐ ์๋ฃ๊ฐ ๊ตฐ ์ค๋ฌด ์๊ตฌ์ฌํญ์ ๋ถํฉํ๋์ง ๋ฉด๋ฐํ ํ์ธํ๋ค.\n",
"\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"\"\"\"๋๋ก ์ํ(๋นํฌ์ฅยท๊ต๋ํฐ๋ ์์ฌ ๊ฑฐ๋ฆฌ), ๊ธฐ์(์ฐ์ฒ), ์ ์ฒด(์๊ฐ๋), ์ํ ๊ตฌ์ญ ์์ฌ ๊ฑฐ๋ฆฌ์ ์ ์ฐจ ์ด๋ ฅ์ ๋ฐ์ํ ๋จธ์ ๋ฌ๋ ์ ๋ฐ ETA ๋ชจ๋ธ์ ๋ง๋ค๊ณ , ์๊ณ์ด ๊ต์ฐจ๊ฒ์ฆ(์ด๋ ์์)์ผ๋ก MAPE๋ฅผ ์ธก์ ํ๋ฉฐ ๋ณ์๊ตฐ ์ ๊ฑฐ ์คํ์ผ๋ก ๊ฐ ๋ณ์๊ตฐ์ ๊ธฐ์ฌ๋ฅผ ํ์ธํ๋ค.\"\"\"\n",
"import numpy as np\n",
"import pandas as pd\n",
"import matplotlib.pyplot as plt\n",
"from sklearn.ensemble import HistGradientBoostingRegressor\n",
"from sklearn.inspection import permutation_importance\n",
"from sklearn.linear_model import Ridge\n",
"from sklearn.model_selection import TimeSeriesSplit\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# [๊ต์ก์ฉ๊ฐ์์๋ฃ] ๊ฐ์ ์์ก ๋
ธ์ ยท์์ก์ ๋จ ํ
๋ ๋ฉํธ๋ฆฌ ์์ฑ๊ธฐ (์ค์ ๋๋กยท์ง๋ช
ยท๋ถ๋ ์์)\n",
"# ๊ฐ์ ๋
ธ์ (์ถ๋ฐ ๊ฑฐ์ -> ๋์ฐฉ ๊ฑฐ์ , 5๊ฐ ๊ตฌ๊ฐ)์ 5๋ถ ๊ฐ๊ฒฉ์ผ๋ก ๋ณด๊ณ ํ๋ ์ ๋จ GPSยทํ๋ฌผ ์ผ์ยทํต์ ์ํ ์๋ฃ๋ฅผ ๋ง๋ ๋ค.\n",
"# ---------------------------------------------------------------------------\n",
"PTS = [(0, 0), (30, 12), (62, 14), (88, 38), (112, 42), (135, 66)] # ๋
ธ์ ๊ผญ์ง์ (km, ๊ฐ์ ์ขํ)\n",
"ROUTE = pd.DataFrame({\"seg\": [\"R1\", \"R2\", \"R3\", \"R4\", \"R5\"], \"road\": [\"paved\", \"unpaved\", \"paved\", \"paved\", \"unpaved\"],\n",
" \"limit\": [70, 50, 70, 50, 50], \"kind\": [\"์ผ๋ฐ\", \"์ผ๋ฐ\", \"์ผ๋ฐ\", \"๊ต๋ยทํฐ๋\", \"์ผ๋ฐ\"], \"threat\": [0, 0, 1, 0, 0]})\n",
"ROUTE[\"x0\"], ROUTE[\"y0\"] = [p[0] for p in PTS[:-1]], [p[1] for p in PTS[:-1]]\n",
"ROUTE[\"x1\"], ROUTE[\"y1\"] = [p[0] for p in PTS[1:]], [p[1] for p in PTS[1:]]\n",
"ROUTE[\"km\"] = np.hypot(ROUTE[\"x1\"] - ROUTE[\"x0\"], ROUTE[\"y1\"] - ROUTE[\"y0\"])\n",
"ROUTE[\"c0\"] = np.r_[0, np.cumsum(ROUTE[\"km\"])[:-1]]\n",
"ROUTE[\"c1\"] = np.cumsum(ROUTE[\"km\"])\n",
"LEN = float(ROUTE[\"km\"].sum())\n",
"ROAD_F = {\"paved\": 1.0, \"unpaved\": 0.75}\n",
"C1L, C0L, KML = ROUTE[\"c1\"].tolist(), ROUTE[\"c0\"].tolist(), ROUTE[\"km\"].tolist() # ๋ฐ๋ณต ๊ณ์ฐ์ฉ ๋ฆฌ์คํธ(์๋)\n",
"BASE_V = [l * ROAD_F[r] for l, r in zip(ROUTE[\"limit\"], ROUTE[\"road\"])]\n",
"THR, UNP, TUN = ROUTE[\"threat\"].tolist(), (ROUTE[\"road\"] == \"unpaved\").tolist(), (ROUTE[\"kind\"] == \"๊ต๋ยทํฐ๋\").tolist()\n",
"\n",
"\n",
"def seg_of(s):\n",
" # [TODO : ํ์ ์ค์ต 4] ๋๋ก ์ํ, ๊ธฐ์, ์ ์ํ ์ ์ฒด ๋ณ์๋ฅผ ๋ฐ์ํ ๋จธ์ ๋ฌ๋ ๊ธฐ๋ฐ ์ ๋ฐ ETA ์์ธก ๋ชจ๋ธ ๊ณ ๋ํ ๊ด๋ จ ํต์ฌ ๋ก์ง์ ๊ตฌํํ์ธ์.\n",
" # ํํธ: ์๋จ ์ฃผ์ ๋ฐ ๋ฐ์ดํฐ ์คํค๋ง/์ง์นจ์ ์ฐธ๊ณ ํ์ฌ ์์ฑํฉ๋๋ค.\n",
" # <<< ์ฌ๊ธฐ์ ์ฝ๋๋ฅผ ์์ฑํ์ธ์ >>>\n",
" pass # ์์ฑ ํ pass๋ฅผ ์ ๊ฑฐํ์ธ์.\n",
"def route_xy(s):\n",
" \"\"\"๋
ธ์ ์ ์งํ๊ฑฐ๋ฆฌ s(km)์ ์ขํ์ ์งํ ๋ฐฉํฅ(๋).\"\"\"\n",
" # [TODO : ํ์ ์ค์ต 4] ๋๋ก ์ํ, ๊ธฐ์, ์ ์ํ ์ ์ฒด ๋ณ์๋ฅผ ๋ฐ์ํ ๋จธ์ ๋ฌ๋ ๊ธฐ๋ฐ ์ ๋ฐ ETA ์์ธก ๋ชจ๋ธ ๊ณ ๋ํ ๊ด๋ จ ํต์ฌ ๋ก์ง์ ๊ตฌํํ์ธ์.\n",
" # ํํธ: ์๋จ ์ฃผ์ ๋ฐ ๋ฐ์ดํฐ ์คํค๋ง/์ง์นจ์ ์ฐธ๊ณ ํ์ฌ ์์ฑํฉ๋๋ค.\n",
" # <<< ์ฌ๊ธฐ์ ์ฝ๋๋ฅผ ์์ฑํ์ธ์ >>>\n",
" pass # ์์ฑ ํ pass๋ฅผ ์ ๊ฑฐํ์ธ์.\n",
"def project(x, y):\n",
" \"\"\"GPS ์ขํ๋ฅผ ๋
ธ์ ์ ํฌ์ํด (์งํ๊ฑฐ๋ฆฌ km, ๋
ธ์ ๊ณผ์ ์์ง ์ดํ km)๋ฅผ ๊ตฌํ๋ค.\"\"\"\n",
" x, y = np.atleast_1d(x).astype(float), np.atleast_1d(y).astype(float)\n",
" best_s, best_d = np.zeros(len(x)), np.full(len(x), np.inf)\n",
" for r in ROUTE.itertuples():\n",
" dx, dy = r.x1 - r.x0, r.y1 - r.y0\n",
" f = np.clip(((x - r.x0) * dx + (y - r.y0) * dy) / (dx * dx + dy * dy), 0, 1)\n",
" d = np.hypot(x - (r.x0 + f * dx), y - (r.y0 + f * dy))\n",
" better = d < best_d\n",
" best_s, best_d = np.where(better, r.c0 + f * r.km, best_s), np.where(better, d, best_d)\n",
" return best_s, best_d\n",
"\n",
"\n",
"def cong(clock_min):\n",
" h = (clock_min / 60) % 24\n",
" return 0.82 if (7 <= h < 9 or 17 <= h < 19) else 1.0 # ์ถํด๊ทผ ์๊ฐ๋ ์ ์ฒด ๊ณ์(๊ฐ์ )\n",
"\n",
"\n",
"def simulate_trip(rng, tid, start_min, rain, size, cargo):\n",
" \"\"\"1๋ถ ๋จ์๋ก ์ด๋์ ๋ชจ์ํ๊ณ 5๋ถ๋ง๋ค ํ
๋ ๋ฉํธ๋ฆฌ๋ฅผ ๋จ๊ธด๋ค. ์ดํยท๋ถ์ ์ ์ฐจ ์ด๋ฒคํธ๋ ์ ๋ต ๋ผ๋ฒจ๋ก ํจ๊ป ์ ์ฅํ๋ค.\"\"\"\n",
" s, t, stop_left, rows = 0.0, 0, 0, []\n",
" dev = (rng.random() < 0.12)\n",
" dev_t0 = int(rng.integers(30, 120)) if dev else -1\n",
" dev_len = int(rng.integers(12, 25)) if dev else 0\n",
" cause = \"\"\n",
" while s < LEN:\n",
" i = seg_of(s)\n",
" v = BASE_V[i] * (0.88 if rain else 1.0) * (0.85 if (rain and UNP[i]) else 1.0) * cong(start_min + t) \\\n",
" * (0.7 if THR[i] else 1.0) * (1 - 0.012 * size) * rng.normal(1, 0.06)\n",
" off = 0.0\n",
" if dev and dev_t0 <= t < dev_t0 + dev_len:\n",
" ph = (t - dev_t0) / dev_len\n",
" off, v = 4.0 * (1 - abs(2 * ph - 1)), v * 0.4 # ๋
ธ์ ์ดํ(์ผ๊ฐํ ํํ๋ก ์ต๋ 4km)\n",
" if stop_left > 0:\n",
" v, stop_left = 0.0, stop_left - 1\n",
" elif rng.random() < 0.0015:\n",
" stop_left, cause = int(rng.integers(8, 36)), (\"maint\" if rng.random() < 0.7 else \"urgent\")\n",
" s += v / 60\n",
" t += 1\n",
" if t % 5 == 0 or s >= LEN:\n",
" x, y, hd = route_xy(min(s, LEN))\n",
" nx, ny = np.cos(np.radians(hd)), -np.sin(np.radians(hd))\n",
" j = seg_of(min(s, LEN)); tun = TUN[j]\n",
" rows.append(dict(trip=tid, t=t, clock=(start_min + t) % 1440, x=x + off * nx + rng.normal(0, 0.03), y=y + off * ny + rng.normal(0, 0.03),\n",
" speed=max(v * rng.normal(1, 0.03), 0.0), heading=(hd + rng.normal(0, 3)) % 360, rssi=-62 - 14 * tun + rng.normal(0, 3),\n",
" cargo_temp=18 + 3 * np.sin(t / 40) + rng.normal(0, 0.3), shock_g=abs(rng.normal(0.25, 0.1)) * (2.0 if UNP[j] else 1.0),\n",
" s_true=min(s, LEN), off_true=off, stopped_true=int(v == 0.0), rain=int(rain), size=size, cargo=cargo, stop_cause=cause if v == 0.0 else \"\"))\n",
" arr = t\n",
" for r in rows:\n",
" r[\"eta_true\"] = arr - r[\"t\"]\n",
" return rows\n",
"\n",
"\n",
"def make_telemetry(seed=21, n_trips=200):\n",
" \"\"\"n_trips๊ฐ ์์ก์ ๋จ ์ด๋ ๊ธฐ๋ก(๋ ์ง ์). ์ ๋จ ๊ท๋ชจ 3~12๋, ํ๋ฌผ ๊ตฌ๋ถ ์ผ๋ฐ/๋์ฅ/๋ฏผ๊ฐ.\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" rows = []\n",
" for k in range(n_trips):\n",
" rows += simulate_trip(rng, k, int(rng.integers(5 * 60, 15 * 60)), bool(rng.random() < 0.2), int(rng.integers(3, 13)), str(rng.choice([\"์ผ๋ฐ\", \"๋์ฅ\", \"๋ฏผ๊ฐ\"], p=[.6, .25, .15])))\n",
" return pd.DataFrame(rows)\n",
"\n",
"\n",
"def mape(y, p):\n",
" y, p = np.asarray(y, float), np.asarray(p, float)\n",
" return float(np.mean(np.abs(y - p) / np.abs(y)) * 100)\n",
"\n",
"\n",
"def mae(y, p):\n",
" return float(np.mean(np.abs(np.asarray(y, float) - np.asarray(p, float))))\n",
"# ---------------------------------------------------------------------------\n",
"# ---------------------------------------------------------------------------\n",
"# ETA ์์ธก ํผ์ฒ: ๊ด์ธก ์์ ๊น์ง์ ํ
๋ ๋ฉํธ๋ฆฌ์ ๋จ์ ๋
ธ์ ๊ตฌ์ฑ(ํฌ์ฅยท์ํยท๊ต๋ํฐ๋ ๊ตฌ๊ฐ ๊ฑฐ๋ฆฌ)๋ง ์ฌ์ฉ\n",
"# ---------------------------------------------------------------------------\n",
"FEATS = [\"remaining_km\", \"progress\", \"speed\", \"speed_ma3\", \"speed_std3\", \"stop_n\", \"stop_min\", \"since_stop\", \"hour_sin\", \"hour_cos\", \"cong\", \"rain\", \"size\",\n",
" \"k_๋์ฅ\", \"k_๋ฏผ๊ฐ\", \"rem_unpaved_km\", \"rem_threat_km\", \"rem_bridge_km\", \"off_km\", \"rssi\", \"base_eta\"]\n",
"\n",
"\n",
"def rem_composition(s):\n",
" \"\"\"์งํ๊ฑฐ๋ฆฌ s ์ดํ ๋จ์ ๊ตฌ๊ฐ ์ค ๋นํฌ์ฅยท์ํยท๊ต๋ํฐ๋ ๊ฑฐ๋ฆฌ(km).\"\"\"\n",
" left = np.clip(ROUTE[\"c1\"].values[None, :] - np.maximum(np.atleast_1d(s)[:, None], ROUTE[\"c0\"].values[None, :]), 0, None)\n",
" return left @ np.array(UNP, float), left @ np.array(THR, float), left @ np.array(TUN, float)\n",
"\n",
"\n",
"def add_features(df):\n",
" d = df.sort_values([\"trip\", \"t\"]).copy()\n",
" d[\"s_obs\"], d[\"off_km\"] = project(d[\"x\"].values, d[\"y\"].values)\n",
" d[\"remaining_km\"] = LEN - d[\"s_obs\"]\n",
" d[\"progress\"] = d[\"s_obs\"] / LEN\n",
" g = d.groupby(\"trip\")[\"speed\"]\n",
" d[\"speed_ma3\"] = g.rolling(3, min_periods=1).mean().reset_index(level=0, drop=True)\n",
" d[\"speed_std3\"] = g.rolling(3, min_periods=1).std().reset_index(level=0, drop=True).fillna(0)\n",
" d[\"is_stop\"] = (d[\"speed\"] < 3).astype(int)\n",
" new_stop = (d[\"is_stop\"] == 1) & (d.groupby(\"trip\")[\"is_stop\"].shift(1).fillna(0) == 0)\n",
" d[\"stop_n\"] = new_stop.astype(int).groupby(d[\"trip\"]).cumsum()\n",
" d[\"stop_min\"] = d.groupby(\"trip\")[\"is_stop\"].cumsum() * 5\n",
" blk = (d[\"is_stop\"] == 1).astype(int).groupby(d[\"trip\"]).cumsum() # ์ ์ฐจ ๋์ ํ์๋ก ๊ตฌ๊ฐ์ ๋๋์ด ๋ง์ง๋ง ์ ์ฐจ ์ดํ ๊ฒฝ๊ณผ ๊ณ์ฐ\n",
" d[\"since_stop\"] = d.groupby([\"trip\", blk]).cumcount() * 5\n",
" d[\"hour_sin\"], d[\"hour_cos\"] = np.sin(2 * np.pi * d[\"clock\"] / 1440), np.cos(2 * np.pi * d[\"clock\"] / 1440)\n",
" d[\"cong\"] = d[\"clock\"].map(cong)\n",
" d[\"k_๋์ฅ\"], d[\"k_๋ฏผ๊ฐ\"] = (d[\"cargo\"] == \"๋์ฅ\").astype(int), (d[\"cargo\"] == \"๋ฏผ๊ฐ\").astype(int)\n",
" d[\"rem_unpaved_km\"], d[\"rem_threat_km\"], d[\"rem_bridge_km\"] = rem_composition(d[\"s_obs\"].values)\n",
" d[\"base_eta\"] = np.minimum(d[\"remaining_km\"] / np.maximum(d[\"speed\"], 5.0) * 60, 600) # ํ์ฌ ์๋ ๊ธฐ๋ฐ ๋จ์ ETA(๋ถ, ์ ์ฐจ ์ ํญ์ฃผ ๋ฐฉ์ง๋ก 5km/h ํํยท600๋ถ ์ํ)\n",
" return d.reset_index(drop=True)\n",
"# ---------------------------------------------------------------------------\n",
"\n",
"df = make_telemetry(); d = add_features(df)\n",
"print(f\"[1] ํ์ ํผ์ฒ {len(FEATS) - 5}๊ฐ ํฌํจ ์ด {len(FEATS)}๊ฐ ์ฌ์ฉ(๋จ์ ๊ฑฐ๋ฆฌยท๊ตฌ์ฑ, ์ ์ฐจ ์ด๋ ฅ, ์๊ฐ๋ ์ํ, ์๋ ์ด๋ํ๊ท ยทํ์คํธ์ฐจ, ์ดํ๊ฑฐ๋ฆฌ ๋ฑ). ํ๊ฐ๋ ๋์ฐฉ 15๋ถ ์ด์ ๋จ์ ๊ด์ธก๋ง ์ฌ์ฉ\")\n",
"trips = np.sort(d[\"trip\"].unique())\n",
"tscv = TimeSeriesSplit(n_splits=4)\n",
"rows = []\n",
"for k, (a, b) in enumerate(tscv.split(trips)):\n",
" tr, te = d[d.trip.isin(trips[a])], d[d.trip.isin(trips[b])]\n",
" te = te[te.eta_true >= 15]\n",
" gbr = HistGradientBoostingRegressor(max_iter=150, max_depth=4, learning_rate=0.08, random_state=0).fit(tr[FEATS], tr[\"eta_true\"])\n",
" rdg = Ridge(alpha=1.0).fit(tr[FEATS], tr[\"eta_true\"])\n",
" rows.append((k + 1, len(trips[a]), len(trips[b]), mape(te.eta_true, te.base_eta), mape(te.eta_true, rdg.predict(te[FEATS])), mape(te.eta_true, gbr.predict(te[FEATS]))))\n",
"cv = pd.DataFrame(rows, columns=[\"fold\", \"ํ์ต ์ด๋\", \"๊ฒ์ฆ ์ด๋\", \"๋จ์ ETA MAPE(%)\", \"Ridge MAPE(%)\", \"GBR MAPE(%)\"]).round(2)\n",
"print(\"[2] ์๊ณ์ด ๊ต์ฐจ๊ฒ์ฆ(์ด๋ ์์๋๋ก ํ์ต ๊ตฌ๊ฐ์ด ๋์ด๋จ, ๋ฏธ๋ ์ ๋ณด ๋์ ์์)\")\n",
"print(cv.to_string(index=False))\n",
"mm = cv[\"GBR MAPE(%)\"].mean()\n",
"print(f\" GBR ํ๊ท MAPE {mm:.2f}% (๋ชฉํ 25% ์ดํ: {'๋ฌ์ฑ' if mm <= 25 else '๋ฏธ๋ฌ'}), ๋จ์ ETA ํ๊ท {cv['๋จ์ ETA MAPE(%)'].mean():.1f}%\")\n",
"\n",
"# ๋ณ์๊ตฐ ์ ๊ฑฐ ์คํ(ํ์ต 160 ์ด๋ / ๊ฒ์ฆ 40 ์ด๋): ๊ฐ ๋ณ์๊ตฐ์ ๋นผ๋ฉด MAPE๊ฐ ์ผ๋ง๋ ๋๋น ์ง๋๊ฐ\n",
"tr, te = d[d.trip < 160], d[(d.trip >= 160) & (d.eta_true >= 15)]\n",
"groups = {\"๋๋ก ์ํ(๋นํฌ์ฅยท๊ต๋ํฐ๋ ์์ฌ)\": [\"rem_unpaved_km\", \"rem_bridge_km\"], \"๊ธฐ์(์ฐ์ฒ)\": [\"rain\"], \"์ ์ฒด(์๊ฐ๋)\": [\"hour_sin\", \"hour_cos\", \"cong\"],\n",
" \"์ํยท์ ์ฒด ๊ตฌ์ญ ์์ฌ ๊ฑฐ๋ฆฌ\": [\"rem_threat_km\"], \"์ ์ฐจ ์ด๋ ฅ\": [\"stop_n\", \"stop_min\", \"since_stop\"], \"์ดํยทํต์ \": [\"off_km\", \"rssi\"]}\n",
"\n",
"\n",
"def fit_eval(cols):\n",
" m = HistGradientBoostingRegressor(max_iter=150, max_depth=4, learning_rate=0.08, random_state=0).fit(tr[cols], tr[\"eta_true\"])\n",
" return mape(te.eta_true, m.predict(te[cols])), m\n",
"\n",
"\n",
"full, m_full = fit_eval(FEATS)\n",
"ab = [(g, fit_eval([f for f in FEATS if f not in cols])[0]) for g, cols in groups.items()]\n",
"abt = pd.DataFrame(ab, columns=[\"์ ๊ฑฐํ ๋ณ์๊ตฐ\", \"MAPE(%)\"]).assign(์ฆ๊ฐ=lambda x: x[\"MAPE(%)\"] - full).sort_values(\"์ฆ๊ฐ\", ascending=False).round(2)\n",
"print(f\"\\n[3] ๋ณ์๊ตฐ ์ ๊ฑฐ ์คํ(์ ์ฒด ์ฌ์ฉ MAPE {full:.2f}%)\")\n",
"print(abt.to_string(index=False))\n",
"pi = permutation_importance(m_full, te[FEATS], te[\"eta_true\"], n_repeats=3, random_state=0, scoring=\"neg_mean_absolute_error\")\n",
"imp = pd.Series(pi.importances_mean, index=FEATS).sort_values(ascending=False)\n",
"imp = imp / imp.sum()\n",
"print(\" ์์ด ์ค์๋ ์์ 5:\", \", \".join(f\"{k} {v:.2f}\" for k, v in imp.head(5).items()), f\"/ 1% ๋ฏธ๋ง {int((imp < 0.01).sum())}๊ฐ(์ฐจ์ ์ถ์ ํ๋ณด)\")\n",
"base_all = mape(te.eta_true, te.base_eta)\n",
"print(f\"\\n[4] ๊ฒ์ฆ 40๊ฐ ์ด๋: ๋จ์ ETA MAPE {base_all:.1f}% -> GBR {full:.1f}%, MAE {mae(te.eta_true, te.base_eta):.1f}๋ถ -> {mae(te.eta_true, m_full.predict(te[FEATS])):.1f}๋ถ\")\n",
"print(\" ํด์: ๋
ธ์ ์ด ํ๋๋ฟ์ด๋ผ ๋จ์ ๋นํฌ์ฅยท์ํยท๊ต๋ํฐ๋ ๊ฑฐ๋ฆฌ๋ ์งํ๋ฅ ๋ก ๋์ฒด๋์ด ๋จ๋
์ ๊ฑฐ ํจ๊ณผ๊ฐ ์๋ค. ๋
ธ์ ์ด ์ฌ๋ฌ ๊ฐ๋ฉด ์ด ๋ณ์๋ค์ด ํ์๋ค. ์ฐ์ฒ๊ณผ ์๊ฐ๋ ์ ์ฒด๋ ์งํ๋ฅ ๋ก ๋์ฒด๋์ง ์์ ์ ๊ฑฐ ์ ์ค์ฐจ๊ฐ ์ปค์ง๋ค.\")\n",
"\n",
"fig, ax = plt.subplots(1, 3, figsize=(16, 4.5))\n",
"ax[0].plot(cv[\"fold\"], cv[\"๋จ์ ETA MAPE(%)\"], \"o-\", label=\"simple\"); ax[0].plot(cv[\"fold\"], cv[\"GBR MAPE(%)\"], \"s-\", label=\"GBR\"); ax[0].legend(); ax[0].set_title(\"Time-series CV MAPE (%)\")\n",
"ax[1].barh([\"road\", \"weather\", \"congestion\", \"threat zone\", \"stops\", \"off-route/comm\"][::-1], abt.set_index(\"์ ๊ฑฐํ ๋ณ์๊ตฐ\").reindex(list(groups))[\"์ฆ๊ฐ\"].values[::-1]); ax[1].set_title(\"MAPE increase when group removed\")\n",
"imp.head(10)[::-1].plot.barh(ax=ax[2]); ax[2].set_title(\"Feature importance (top 10)\")\n",
"plt.tight_layout(); plt.show()\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"๐ก [์ฐธ๊ณ ] ๋ฏธ์
04 ์์ ์คํ ๊ฒฐ๊ณผ (Output)
\n",
"\n",
"```text\n",
"[1] ํ์ ํผ์ฒ 16๊ฐ ํฌํจ ์ด 21๊ฐ ์ฌ์ฉ(๋จ์ ๊ฑฐ๋ฆฌยท๊ตฌ์ฑ, ์ ์ฐจ ์ด๋ ฅ, ์๊ฐ๋ ์ํ, ์๋ ์ด๋ํ๊ท ยทํ์คํธ์ฐจ, ์ดํ๊ฑฐ๋ฆฌ ๋ฑ). ํ๊ฐ๋ ๋์ฐฉ 15๋ถ ์ด์ ๋จ์ ๊ด์ธก๋ง ์ฌ์ฉ\n",
"[2] ์๊ณ์ด ๊ต์ฐจ๊ฒ์ฆ(์ด๋ ์์๋๋ก ํ์ต ๊ตฌ๊ฐ์ด ๋์ด๋จ, ๋ฏธ๋ ์ ๋ณด ๋์ ์์)\n",
" fold ํ์ต ์ด๋ ๊ฒ์ฆ ์ด๋ ๋จ์ ETA MAPE(%) Ridge MAPE(%) GBR MAPE(%)\n",
" 1 40 40 28.01 10.46 8.53\n",
" 2 80 40 34.83 11.07 7.65\n",
" 3 120 40 30.67 11.36 6.66\n",
" 4 160 40 31.28 10.64 7.23\n",
" GBR ํ๊ท MAPE 7.52% (๋ชฉํ 25% ์ดํ: ๋ฌ์ฑ), ๋จ์ ETA ํ๊ท 31.2%\n",
"\n",
"[3] ๋ณ์๊ตฐ ์ ๊ฑฐ ์คํ(์ ์ฒด ์ฌ์ฉ MAPE 7.23%)\n",
" ์ ๊ฑฐํ ๋ณ์๊ตฐ MAPE(%) ์ฆ๊ฐ\n",
" ๊ธฐ์(์ฐ์ฒ) 8.80 1.57\n",
" ์ ์ฒด(์๊ฐ๋) 8.52 1.29\n",
" ์ ์ฐจ ์ด๋ ฅ 7.27 0.04\n",
" ์ดํยทํต์ 7.27 0.03\n",
" ์ํยท์ ์ฒด ๊ตฌ์ญ ์์ฌ ๊ฑฐ๋ฆฌ 7.23 0.00\n",
"๋๋ก ์ํ(๋นํฌ์ฅยท๊ต๋ํฐ๋ ์์ฌ) 7.22 -0.01\n",
" ์์ด ์ค์๋ ์์ 5: remaining_km 0.78, rain 0.09, base_eta 0.04, progress 0.04, size 0.02 / 1% ๋ฏธ๋ง 15๊ฐ(์ฐจ์ ์ถ์ ํ๋ณด)\n",
"\n",
"[4] ๊ฒ์ฆ 40๊ฐ ์ด๋: ๋จ์ ETA MAPE 31.3% -> GBR 7.2%, MAE 43.6๋ถ -> 8.7๋ถ\n",
" ํด์: ๋
ธ์ ์ด ํ๋๋ฟ์ด๋ผ ๋จ์ ๋นํฌ์ฅยท์ํยท๊ต๋ํฐ๋ ๊ฑฐ๋ฆฌ๋ ์งํ๋ฅ ๋ก ๋์ฒด๋์ด ๋จ๋
์ ๊ฑฐ ํจ๊ณผ๊ฐ ์๋ค. ๋
ธ์ ์ด ์ฌ๋ฌ ๊ฐ๋ฉด ์ด ๋ณ์๋ค์ด ํ์๋ค. ์ฐ์ฒ๊ณผ ์๊ฐ๋ ์ ์ฒด๋ ์งํ๋ฅ ๋ก ๋์ฒด๋์ง ์์ ์ ๊ฑฐ ์ ์ค์ฐจ๊ฐ ์ปค์ง๋ค.\n",
"```\n",
" \n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## ๐ฏ ๋ฏธ์
05. ์์ก์ ๋จ ๊ฒฝ๋ก ์ดํ ๋ฐ ๋ถ์ ์ ์ฐจ ๊ฐ์ง ์ ๋ขฐ๋ ๊ฒฝ๋ณด ์์ง ๊ฐ๋ฐ\n",
"**์์์๊ฐ**: ์ฝ 3.5์๊ฐ\n",
"\n",
"### ๐ ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ์ํ ์ง์นจ\n",
"#### ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ๋น์์ฑ\n",
"\n",
"์์ก์ ๋จ ๊ฒฝ๋ก ์ดํ ๋ฐ ๋ถ์ ์ ์ฐจ ๊ฐ์ง ์ ๋ขฐ๋ ๊ฒฝ๋ณด ์์ง์ ๊ฐ๋ฐํ๋ค. ์ค์ ์ ์ ํ์ฅ์์๋ ๋จ์ ํ์ง์ ๊ทธ์น์ง ์๊ณ ์ ์ํ ์ํ ํ๋จ๊ณผ ์ ๋ขฐ๋ ๋์ ์์ฌ๊ฒฐ์ ์ด ํ์์ ์ด๋ฏ๋ก, **์์ก์ ๋จ์ ๊ฒฝ๋ก ์ดํ๊ณผ ๋ถ์ ์ ์ฐจ๋ฅผ ๊ฐ์งํ๊ณ ์ ๋ขฐ๋๋ฅผ ๋งค๊ฒจ ๊ฒฝ๋ณด ๋ฑ๊ธ์ ๋ด๋ ์์ง์ ๊ฐ๋ฐํ๋ค.** ๊ณผ์
์ ์์ํ์ฌ ์คํ์ ๋ฐฉ์งํ๊ณ ์์ ๋์๋ ฅ์ ๊ทน๋ํํด์ผ ํฉ๋๋ค.\n",
"\n",
"์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ณธ ๋ฏธ์
์์๋ **์ดํ ๊ฑฐ๋ฆฌ์ ์ ์ฐจ ์ง์ ์๊ฐ์ผ๋ก 0~1 ์ ๋ขฐ๋๋ฅผ ๊ณ์ฐํด 0.5 ์ด์์ ๊ฒฝ๋ณด๋ก ๋ณด๊ณ , ์ด๋ฒคํธ ๋จ์ ์ฌํ์จยทํ์ง ์ง์ฐยท์ค๊ฒฝ๋ณด๋ฅผ ๊นจ๋ํ ๊ด์ธก๊ณผ GPS ์ก์ ์กฐ๊ฑด์์ ๋น๊ต ์ํํ๊ณ ๊ฒ์ฆํ๋** ์ค์ต์ ์ฒด๊ณ์ ์ผ๋ก ์ํํฉ๋๋ค.\n",
"\n",
"ํ์ต์๋ ์ค์ต๊ฐ์ด๋์ ์ ์๋ ์ด 5๊ฐ ์ธ๋ถ ํ๋(๊ธฐ๋ณธ ๊ธฐ๋ฅ ๊ตฌํ โ ๊ฒฐ๊ณผ ์งํ ๊ฒ์ฆ โ ํ๋ผ๋ฏธํฐ ํ๋ ๋ฐ ๋์ ๊ณผ์ )์ ์์ฐจ์ ์ผ๋ก ์งํํ๋ฉฐ ํต์ฌ ํ์ ๊ธฐ์ค์ ๋ฌ์ฑํฉ๋๋ค.\n",
"\n",
"**์ด๋ฒ ๊ฒฐ๊ณผ๋ฌผ:** `src/models/mov06_advanced_model.py` ํํ์ ๊ฒฐ๊ณผ๋ฌผ์ ์์ฑํ๋ค.\n",
"\n",
"### ๐ ํ๊ฐ ๋ฐ ๊ฒ์ฆ ๊ธฐ์ค (PASS ์กฐ๊ฑด)\n",
"#### ๋ฏธ์
ํ์ \n",
"\n",
"**ํ๊ฐ ๊ตฌ๋ถ:** ํ์\n",
"\n",
"**ํต์ฌ ํ๊ฐ์์ญ:** ๋น์ ํ/๋ถ๊ท์น ์์์ ๋ํ ๋ชจ๋ธ์ ์ ์๋ ฅ ๋ฐ ์์ธก ์์ ์ฑ\n",
"\n",
"#### ๋ฏธ์
PASS ์กฐ๊ฑด\n",
"\n",
"- ์ ๋ ๊ธฐ์ค: ๊ธฐ์ค ๋ชจ๋ธ ๋๋น ์ค์ฐจ์จ(MAE/RMSE) 20% ์ด์ ๊ฐ์ , R2 Score 0.85 ์ด์\n",
"\n",
"\n",
"- ์งํ ๊ธฐ์ค: ๋ชจ๋ธ ํ์ดํผํ๋ผ๋ฏธํฐ ํ๋ ์๋ฃ ๋ฐ ์ต์ ๋ชจ๋ธ ๊ฐ์ค์น ์ ์ฅ\n",
"\n",
"\n",
"- ๊ฒ์ฆ ๋ฐฉ๋ฒ: Hold-out ํ
์คํธ ๋ฐ์ดํฐ์
๋์ ์ต์ข
์ฑ๋ฅ ํ๊ฐ\n",
"\n",
"\n",
"#### ์ฑ์ ์ ํ์ ๋ฉ๋ชจ\n",
"\n",
"> ๋จ์ ์ฝ๋ ์คํ ์ฌ๋ถ๋ฟ๋ง ์๋๋ผ ์ถ๋ ฅ๋ ์์น์ ๊ทผ๊ฑฐ ์๋ฃ๊ฐ ๊ตฐ ์ค๋ฌด ์๊ตฌ์ฌํญ์ ๋ถํฉํ๋์ง ๋ฉด๋ฐํ ํ์ธํ๋ค.\n",
"\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"\"\"\"์์ก์ ๋จ์ ๊ฒฝ๋ก ์ดํยท๋ถ์ ์ ์ฐจ๋ฅผ ์ง์ ์๊ฐ๊ณผ ์ดํ ๊ฑฐ๋ฆฌ๋ก ์ ๋ขฐ๋(0~1)๋ฅผ ๋งค๊ฒจ ๊ฒฝ๋ณด ๋ฑ๊ธ์ ๋ด๋ ๊ฐ์ง ์์ง์ ๋ง๋ค์ด ์ด๋ฒคํธ ๋จ์ ์ฌํ์จยท์ค๊ฒฝ๋ณด๋ฅผ ๊ฒ์ฆํ๊ณ , ๊ฐ์ง ์ ๋ขฐ๋๋ฅผ ๋ฐ์ํ ์์๋ธ ETA ๋ชจ๋ธ์ด ๊ธฐ์ค ๋ชจ๋ธ ๋๋น MAEยทRMSE๋ฅผ ์ผ๋ง๋ ์ค์ด๋์ง Hold-out์ผ๋ก ํ๊ฐํ๋ค.\"\"\"\n",
"import numpy as np\n",
"import pandas as pd\n",
"from sklearn.ensemble import HistGradientBoostingRegressor, RandomForestRegressor\n",
"from sklearn.metrics import r2_score\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# [๊ต์ก์ฉ๊ฐ์์๋ฃ] ๊ฐ์ ์์ก ๋
ธ์ ยท์์ก์ ๋จ ํ
๋ ๋ฉํธ๋ฆฌ ์์ฑ๊ธฐ (์ค์ ๋๋กยท์ง๋ช
ยท๋ถ๋ ์์)\n",
"# ๊ฐ์ ๋
ธ์ (์ถ๋ฐ ๊ฑฐ์ -> ๋์ฐฉ ๊ฑฐ์ , 5๊ฐ ๊ตฌ๊ฐ)์ 5๋ถ ๊ฐ๊ฒฉ์ผ๋ก ๋ณด๊ณ ํ๋ ์ ๋จ GPSยทํ๋ฌผ ์ผ์ยทํต์ ์ํ ์๋ฃ๋ฅผ ๋ง๋ ๋ค.\n",
"# ---------------------------------------------------------------------------\n",
"PTS = [(0, 0), (30, 12), (62, 14), (88, 38), (112, 42), (135, 66)] # ๋
ธ์ ๊ผญ์ง์ (km, ๊ฐ์ ์ขํ)\n",
"ROUTE = pd.DataFrame({\"seg\": [\"R1\", \"R2\", \"R3\", \"R4\", \"R5\"], \"road\": [\"paved\", \"unpaved\", \"paved\", \"paved\", \"unpaved\"],\n",
" \"limit\": [70, 50, 70, 50, 50], \"kind\": [\"์ผ๋ฐ\", \"์ผ๋ฐ\", \"์ผ๋ฐ\", \"๊ต๋ยทํฐ๋\", \"์ผ๋ฐ\"], \"threat\": [0, 0, 1, 0, 0]})\n",
"ROUTE[\"x0\"], ROUTE[\"y0\"] = [p[0] for p in PTS[:-1]], [p[1] for p in PTS[:-1]]\n",
"ROUTE[\"x1\"], ROUTE[\"y1\"] = [p[0] for p in PTS[1:]], [p[1] for p in PTS[1:]]\n",
"ROUTE[\"km\"] = np.hypot(ROUTE[\"x1\"] - ROUTE[\"x0\"], ROUTE[\"y1\"] - ROUTE[\"y0\"])\n",
"ROUTE[\"c0\"] = np.r_[0, np.cumsum(ROUTE[\"km\"])[:-1]]\n",
"ROUTE[\"c1\"] = np.cumsum(ROUTE[\"km\"])\n",
"LEN = float(ROUTE[\"km\"].sum())\n",
"ROAD_F = {\"paved\": 1.0, \"unpaved\": 0.75}\n",
"C1L, C0L, KML = ROUTE[\"c1\"].tolist(), ROUTE[\"c0\"].tolist(), ROUTE[\"km\"].tolist() # ๋ฐ๋ณต ๊ณ์ฐ์ฉ ๋ฆฌ์คํธ(์๋)\n",
"BASE_V = [l * ROAD_F[r] for l, r in zip(ROUTE[\"limit\"], ROUTE[\"road\"])]\n",
"THR, UNP, TUN = ROUTE[\"threat\"].tolist(), (ROUTE[\"road\"] == \"unpaved\").tolist(), (ROUTE[\"kind\"] == \"๊ต๋ยทํฐ๋\").tolist()\n",
"\n",
"\n",
"def seg_of(s):\n",
" # [TODO : ํ์ ์ค์ต 5] ์์ก์ ๋จ ๊ฒฝ๋ก ์ดํ ๋ฐ ๋ถ์ ์ ์ฐจ ๊ฐ์ง ์ ๋ขฐ๋ ๊ฒฝ๋ณด ์์ง ๊ฐ๋ฐ ๊ด๋ จ ํต์ฌ ๋ก์ง์ ๊ตฌํํ์ธ์.\n",
" # ํํธ: ์๋จ ์ฃผ์ ๋ฐ ๋ฐ์ดํฐ ์คํค๋ง/์ง์นจ์ ์ฐธ๊ณ ํ์ฌ ์์ฑํฉ๋๋ค.\n",
" # <<< ์ฌ๊ธฐ์ ์ฝ๋๋ฅผ ์์ฑํ์ธ์ >>>\n",
" pass # ์์ฑ ํ pass๋ฅผ ์ ๊ฑฐํ์ธ์.\n",
"def route_xy(s):\n",
" \"\"\"๋
ธ์ ์ ์งํ๊ฑฐ๋ฆฌ s(km)์ ์ขํ์ ์งํ ๋ฐฉํฅ(๋).\"\"\"\n",
" # [TODO : ํ์ ์ค์ต 5] ์์ก์ ๋จ ๊ฒฝ๋ก ์ดํ ๋ฐ ๋ถ์ ์ ์ฐจ ๊ฐ์ง ์ ๋ขฐ๋ ๊ฒฝ๋ณด ์์ง ๊ฐ๋ฐ ๊ด๋ จ ํต์ฌ ๋ก์ง์ ๊ตฌํํ์ธ์.\n",
" # ํํธ: ์๋จ ์ฃผ์ ๋ฐ ๋ฐ์ดํฐ ์คํค๋ง/์ง์นจ์ ์ฐธ๊ณ ํ์ฌ ์์ฑํฉ๋๋ค.\n",
" # <<< ์ฌ๊ธฐ์ ์ฝ๋๋ฅผ ์์ฑํ์ธ์ >>>\n",
" pass # ์์ฑ ํ pass๋ฅผ ์ ๊ฑฐํ์ธ์.\n",
"def project(x, y):\n",
" \"\"\"GPS ์ขํ๋ฅผ ๋
ธ์ ์ ํฌ์ํด (์งํ๊ฑฐ๋ฆฌ km, ๋
ธ์ ๊ณผ์ ์์ง ์ดํ km)๋ฅผ ๊ตฌํ๋ค.\"\"\"\n",
" x, y = np.atleast_1d(x).astype(float), np.atleast_1d(y).astype(float)\n",
" best_s, best_d = np.zeros(len(x)), np.full(len(x), np.inf)\n",
" for r in ROUTE.itertuples():\n",
" dx, dy = r.x1 - r.x0, r.y1 - r.y0\n",
" f = np.clip(((x - r.x0) * dx + (y - r.y0) * dy) / (dx * dx + dy * dy), 0, 1)\n",
" d = np.hypot(x - (r.x0 + f * dx), y - (r.y0 + f * dy))\n",
" better = d < best_d\n",
" best_s, best_d = np.where(better, r.c0 + f * r.km, best_s), np.where(better, d, best_d)\n",
" return best_s, best_d\n",
"\n",
"\n",
"def cong(clock_min):\n",
" h = (clock_min / 60) % 24\n",
" return 0.82 if (7 <= h < 9 or 17 <= h < 19) else 1.0 # ์ถํด๊ทผ ์๊ฐ๋ ์ ์ฒด ๊ณ์(๊ฐ์ )\n",
"\n",
"\n",
"def simulate_trip(rng, tid, start_min, rain, size, cargo):\n",
" \"\"\"1๋ถ ๋จ์๋ก ์ด๋์ ๋ชจ์ํ๊ณ 5๋ถ๋ง๋ค ํ
๋ ๋ฉํธ๋ฆฌ๋ฅผ ๋จ๊ธด๋ค. ์ดํยท๋ถ์ ์ ์ฐจ ์ด๋ฒคํธ๋ ์ ๋ต ๋ผ๋ฒจ๋ก ํจ๊ป ์ ์ฅํ๋ค.\"\"\"\n",
" s, t, stop_left, rows = 0.0, 0, 0, []\n",
" dev = (rng.random() < 0.12)\n",
" dev_t0 = int(rng.integers(30, 120)) if dev else -1\n",
" dev_len = int(rng.integers(12, 25)) if dev else 0\n",
" cause = \"\"\n",
" while s < LEN:\n",
" i = seg_of(s)\n",
" v = BASE_V[i] * (0.88 if rain else 1.0) * (0.85 if (rain and UNP[i]) else 1.0) * cong(start_min + t) \\\n",
" * (0.7 if THR[i] else 1.0) * (1 - 0.012 * size) * rng.normal(1, 0.06)\n",
" off = 0.0\n",
" if dev and dev_t0 <= t < dev_t0 + dev_len:\n",
" ph = (t - dev_t0) / dev_len\n",
" off, v = 4.0 * (1 - abs(2 * ph - 1)), v * 0.4 # ๋
ธ์ ์ดํ(์ผ๊ฐํ ํํ๋ก ์ต๋ 4km)\n",
" if stop_left > 0:\n",
" v, stop_left = 0.0, stop_left - 1\n",
" elif rng.random() < 0.0015:\n",
" stop_left, cause = int(rng.integers(8, 36)), (\"maint\" if rng.random() < 0.7 else \"urgent\")\n",
" s += v / 60\n",
" t += 1\n",
" if t % 5 == 0 or s >= LEN:\n",
" x, y, hd = route_xy(min(s, LEN))\n",
" nx, ny = np.cos(np.radians(hd)), -np.sin(np.radians(hd))\n",
" j = seg_of(min(s, LEN)); tun = TUN[j]\n",
" rows.append(dict(trip=tid, t=t, clock=(start_min + t) % 1440, x=x + off * nx + rng.normal(0, 0.03), y=y + off * ny + rng.normal(0, 0.03),\n",
" speed=max(v * rng.normal(1, 0.03), 0.0), heading=(hd + rng.normal(0, 3)) % 360, rssi=-62 - 14 * tun + rng.normal(0, 3),\n",
" cargo_temp=18 + 3 * np.sin(t / 40) + rng.normal(0, 0.3), shock_g=abs(rng.normal(0.25, 0.1)) * (2.0 if UNP[j] else 1.0),\n",
" s_true=min(s, LEN), off_true=off, stopped_true=int(v == 0.0), rain=int(rain), size=size, cargo=cargo, stop_cause=cause if v == 0.0 else \"\"))\n",
" arr = t\n",
" for r in rows:\n",
" r[\"eta_true\"] = arr - r[\"t\"]\n",
" return rows\n",
"\n",
"\n",
"def make_telemetry(seed=21, n_trips=200):\n",
" \"\"\"n_trips๊ฐ ์์ก์ ๋จ ์ด๋ ๊ธฐ๋ก(๋ ์ง ์). ์ ๋จ ๊ท๋ชจ 3~12๋, ํ๋ฌผ ๊ตฌ๋ถ ์ผ๋ฐ/๋์ฅ/๋ฏผ๊ฐ.\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" rows = []\n",
" for k in range(n_trips):\n",
" rows += simulate_trip(rng, k, int(rng.integers(5 * 60, 15 * 60)), bool(rng.random() < 0.2), int(rng.integers(3, 13)), str(rng.choice([\"์ผ๋ฐ\", \"๋์ฅ\", \"๋ฏผ๊ฐ\"], p=[.6, .25, .15])))\n",
" return pd.DataFrame(rows)\n",
"\n",
"\n",
"def mape(y, p):\n",
" y, p = np.asarray(y, float), np.asarray(p, float)\n",
" return float(np.mean(np.abs(y - p) / np.abs(y)) * 100)\n",
"\n",
"\n",
"def mae(y, p):\n",
" return float(np.mean(np.abs(np.asarray(y, float) - np.asarray(p, float))))\n",
"# ---------------------------------------------------------------------------\n",
"# ---------------------------------------------------------------------------\n",
"# ETA ์์ธก ํผ์ฒ: ๊ด์ธก ์์ ๊น์ง์ ํ
๋ ๋ฉํธ๋ฆฌ์ ๋จ์ ๋
ธ์ ๊ตฌ์ฑ(ํฌ์ฅยท์ํยท๊ต๋ํฐ๋ ๊ตฌ๊ฐ ๊ฑฐ๋ฆฌ)๋ง ์ฌ์ฉ\n",
"# ---------------------------------------------------------------------------\n",
"FEATS = [\"remaining_km\", \"progress\", \"speed\", \"speed_ma3\", \"speed_std3\", \"stop_n\", \"stop_min\", \"since_stop\", \"hour_sin\", \"hour_cos\", \"cong\", \"rain\", \"size\",\n",
" \"k_๋์ฅ\", \"k_๋ฏผ๊ฐ\", \"rem_unpaved_km\", \"rem_threat_km\", \"rem_bridge_km\", \"off_km\", \"rssi\", \"base_eta\"]\n",
"\n",
"\n",
"def rem_composition(s):\n",
" \"\"\"์งํ๊ฑฐ๋ฆฌ s ์ดํ ๋จ์ ๊ตฌ๊ฐ ์ค ๋นํฌ์ฅยท์ํยท๊ต๋ํฐ๋ ๊ฑฐ๋ฆฌ(km).\"\"\"\n",
" left = np.clip(ROUTE[\"c1\"].values[None, :] - np.maximum(np.atleast_1d(s)[:, None], ROUTE[\"c0\"].values[None, :]), 0, None)\n",
" return left @ np.array(UNP, float), left @ np.array(THR, float), left @ np.array(TUN, float)\n",
"\n",
"\n",
"def add_features(df):\n",
" d = df.sort_values([\"trip\", \"t\"]).copy()\n",
" d[\"s_obs\"], d[\"off_km\"] = project(d[\"x\"].values, d[\"y\"].values)\n",
" d[\"remaining_km\"] = LEN - d[\"s_obs\"]\n",
" d[\"progress\"] = d[\"s_obs\"] / LEN\n",
" g = d.groupby(\"trip\")[\"speed\"]\n",
" d[\"speed_ma3\"] = g.rolling(3, min_periods=1).mean().reset_index(level=0, drop=True)\n",
" d[\"speed_std3\"] = g.rolling(3, min_periods=1).std().reset_index(level=0, drop=True).fillna(0)\n",
" d[\"is_stop\"] = (d[\"speed\"] < 3).astype(int)\n",
" new_stop = (d[\"is_stop\"] == 1) & (d.groupby(\"trip\")[\"is_stop\"].shift(1).fillna(0) == 0)\n",
" d[\"stop_n\"] = new_stop.astype(int).groupby(d[\"trip\"]).cumsum()\n",
" d[\"stop_min\"] = d.groupby(\"trip\")[\"is_stop\"].cumsum() * 5\n",
" blk = (d[\"is_stop\"] == 1).astype(int).groupby(d[\"trip\"]).cumsum() # ์ ์ฐจ ๋์ ํ์๋ก ๊ตฌ๊ฐ์ ๋๋์ด ๋ง์ง๋ง ์ ์ฐจ ์ดํ ๊ฒฝ๊ณผ ๊ณ์ฐ\n",
" d[\"since_stop\"] = d.groupby([\"trip\", blk]).cumcount() * 5\n",
" d[\"hour_sin\"], d[\"hour_cos\"] = np.sin(2 * np.pi * d[\"clock\"] / 1440), np.cos(2 * np.pi * d[\"clock\"] / 1440)\n",
" d[\"cong\"] = d[\"clock\"].map(cong)\n",
" d[\"k_๋์ฅ\"], d[\"k_๋ฏผ๊ฐ\"] = (d[\"cargo\"] == \"๋์ฅ\").astype(int), (d[\"cargo\"] == \"๋ฏผ๊ฐ\").astype(int)\n",
" d[\"rem_unpaved_km\"], d[\"rem_threat_km\"], d[\"rem_bridge_km\"] = rem_composition(d[\"s_obs\"].values)\n",
" d[\"base_eta\"] = np.minimum(d[\"remaining_km\"] / np.maximum(d[\"speed\"], 5.0) * 60, 600) # ํ์ฌ ์๋ ๊ธฐ๋ฐ ๋จ์ ETA(๋ถ, ์ ์ฐจ ์ ํญ์ฃผ ๋ฐฉ์ง๋ก 5km/h ํํยท600๋ถ ์ํ)\n",
" return d.reset_index(drop=True)\n",
"# ---------------------------------------------------------------------------\n",
"\n",
"df = make_telemetry(); d = add_features(df)\n",
"\n",
"\n",
"# ---------------- 1) ์ดํยท์ ์ฐจ ์ ๋ขฐ๋ ์ ์ ----------------\n",
"def run_len(flag, trip):\n",
" \"\"\"๊ฐ์ ์ด๋ ์์์ ์กฐ๊ฑด์ด ์ฐ์์ผ๋ก ์ฐธ์ธ ํ์(ํ์ฌ ํฌํจ).\"\"\"\n",
" out, cur, last = [], 0, None\n",
" for f, t in zip(flag, trip):\n",
" cur = cur + 1 if (f and t == last) else (1 if f else 0)\n",
" out.append(cur); last = t\n",
" return np.array(out)\n",
"\n",
"\n",
"def score(off, speed):\n",
" \"\"\"์ดํ๊ฑฐ๋ฆฌยท์๋ ์ด์์ ์ ๋ขฐ๋(0~1)๋ฅผ ๊ณ์ฐํ๋ค.\"\"\"\n",
" dev_run = run_len((off >= 1.0).values, d[\"trip\"].values)\n",
" stop_run = run_len((speed < 3).values, d[\"trip\"].values)\n",
" return np.clip(off / 2.5, 0, 1) * (1 - 0.5 ** dev_run), 1 - np.exp(-stop_run * 5 / 10.0), dev_run, stop_run\n",
"\n",
"\n",
"d[\"dev_conf\"], d[\"stop_conf\"], d[\"dev_run\"], d[\"stop_run\"] = score(d[\"off_km\"], d[\"speed\"])\n",
"d[\"dev_true\"] = (d[\"off_true\"] >= 1.0).astype(int)\n",
"d[\"stop_ev\"] = run_len((d[\"stopped_true\"] == 1).values, d[\"trip\"].values)\n",
"\n",
"\n",
"def event_table(dc, sc):\n",
" rows = []\n",
" for name, true_flag, conf in ((\"๊ฒฝ๋ก ์ดํ\", d[\"dev_true\"] == 1, dc), (\"๋ถ์ ์ ์ฐจ(10๋ถ ์ด์)\", d[\"stop_ev\"] >= 2, sc)):\n",
" start = true_flag & ~true_flag.groupby(d[\"trip\"]).shift(1, fill_value=False)\n",
" ev_ids = start.groupby(d[\"trip\"]).cumsum().where(true_flag)\n",
" n_ev, hit, delays = 0, 0, []\n",
" for (tid, eid), g in d[true_flag].assign(ev=ev_ids[true_flag]).groupby([\"trip\", \"ev\"]):\n",
" n_ev += 1\n",
" a = g[conf[g.index] >= 0.5]\n",
" if len(a):\n",
" hit += 1; delays.append(a[\"t\"].iloc[0] - g[\"t\"].iloc[0])\n",
" alarm = conf >= 0.5\n",
" rows.append((name, n_ev, hit, hit / max(n_ev, 1), float(np.mean(delays)) if delays else np.nan, int(alarm.sum()), int((alarm & ~true_flag).sum())))\n",
" return pd.DataFrame(rows, columns=[\"์ ํ\", \"์ค์ ์ด๋ฒคํธ\", \"ํ์ง\", \"์ฌํ์จ\", \"ํ๊ท ํ์ง ์ง์ฐ(๋ถ)\", \"๊ฒฝ๋ณด ๊ด์ธก\", \"์ค๊ฒฝ๋ณด ๊ด์ธก\"])\n",
"\n",
"\n",
"print(\"[1] ์ด๋ฒคํธ ๋จ์ ๊ฒ์ฆ(์ ๋ขฐ๋ 0.5 ์ด์์ ๊ฒฝ๋ณด๋ก ๊ฐ์ฃผ) - ๊นจ๋ํ ๊ด์ธก\")\n",
"print(event_table(d[\"dev_conf\"], d[\"stop_conf\"]).round(2).to_string(index=False))\n",
"rng = np.random.default_rng(5)\n",
"noisy_off = d[\"off_km\"] + np.abs(rng.normal(0, 0.9, len(d))) # ํฐ๋ ์์ยทGPS ์ค์ฐจ๋ฅผ ๊ฐ์ ํ ์ก์\n",
"noisy_spd = (d[\"speed\"] + rng.normal(0, 7, len(d))).clip(lower=0)\n",
"nd, ns, _, _ = score(noisy_off, noisy_spd)\n",
"print(\" ์ก์ ์กฐ๊ฑด(GPS ์ค์ฐจ +0.9km, ์๋ ์ค์ฐจ 7km/h):\")\n",
"print(event_table(pd.Series(nd, index=d.index), pd.Series(ns, index=d.index)).round(2).to_string(index=False))\n",
"for th in (0.3, 0.5, 0.8):\n",
" p = (d[\"stop_conf\"] >= th); y = (d[\"stop_ev\"] >= 2)\n",
" print(f\" ๋ถ์ ์ ์ฐจ ์๊ณ๊ฐ {th}(๊นจ๋ํ ๊ด์ธก): ์ ๋ฐ๋ {float((p & y).sum() / max(p.sum(), 1)):.0%}, ๊ด์ธก ์ฌํ์จ {float((p & y).sum() / max(y.sum(), 1)):.0%}\")\n",
"print(\" ์ก์์ด ์ปค์ง๋ฉด ์ค๊ฒฝ๋ณด๊ฐ ๋๊ณ ์ดํ ํ์ ์ด ํ๋ค๋ฆฐ๋ค. ์๊ณ๊ฐ์ ๊ฒฝ๋ณด ํผ๋ก์ ์กฐ๊ธฐ ๋์ ์ฌ์ด์์ ์
๋ฌด์ ์ผ๋ก ์ ํด์ผ ํ๋ค.\")\n",
"\n",
"# ---------------- 3) ๊ฐ์ง ์ ๋ขฐ๋๋ฅผ ๋ฐ์ํ ETA ๋ชจ๋ธ vs ๊ธฐ์ค ๋ชจ๋ธ ----------------\n",
"FE2 = FEATS + [\"dev_conf\", \"stop_conf\", \"dev_run\", \"stop_run\"]\n",
"tr, te = d[d.trip < 160], d[(d.trip >= 160) & (d.eta_true >= 15)]\n",
"h1 = HistGradientBoostingRegressor(max_iter=200, max_depth=4, learning_rate=0.06, random_state=0).fit(tr[FE2], tr[\"eta_true\"])\n",
"h0 = HistGradientBoostingRegressor(max_iter=150, max_depth=4, learning_rate=0.08, random_state=0).fit(tr[FEATS], tr[\"eta_true\"])\n",
"rf = RandomForestRegressor(n_estimators=60, min_samples_leaf=5, max_features=0.5, random_state=0, n_jobs=1).fit(tr[FE2], tr[\"eta_true\"])\n",
"P = {\"๊ธฐ์ค(ํ์ฌ ์๋ ETA)\": te[\"base_eta\"].values, \"M4 ๋ชจ๋ธ\": h0.predict(te[FEATS]), \"HGB+๊ฐ์ง ์ ๋ขฐ๋\": h1.predict(te[FE2]), \"RandomForest\": rf.predict(te[FE2])}\n",
"P[\"์์๋ธ(ํ๊ท )\"] = (P[\"HGB+๊ฐ์ง ์ ๋ขฐ๋\"] + P[\"RandomForest\"]) / 2\n",
"tab = pd.DataFrame({k: {\"MAE(๋ถ)\": mae(te.eta_true, v), \"RMSE(๋ถ)\": float(np.sqrt(np.mean((te.eta_true - v) ** 2))), \"MAPE(%)\": mape(te.eta_true, v), \"R2\": r2_score(te.eta_true, v)} for k, v in P.items()}).T.round(3)\n",
"print(f\"\\n[2] Hold-out(์ด๋ 160~199, {len(te)}ํ) ETA ์ฑ๋ฅ\")\n",
"print(tab.to_string())\n",
"b, e, m4 = tab.loc[\"๊ธฐ์ค(ํ์ฌ ์๋ ETA)\"], tab.loc[\"์์๋ธ(ํ๊ท )\"], tab.loc[\"M4 ๋ชจ๋ธ\"]\n",
"imp_ok = 1 - e[\"MAE(๋ถ)\"] / b[\"MAE(๋ถ)\"] >= 0.2 and 1 - e[\"RMSE(๋ถ)\"] / b[\"RMSE(๋ถ)\"] >= 0.2\n",
"print(f\" ๊ธฐ์ค ๋๋น MAE ๊ฐ์ {1 - e['MAE(๋ถ)'] / b['MAE(๋ถ)']:.1%}, RMSE ๊ฐ์ {1 - e['RMSE(๋ถ)'] / b['RMSE(๋ถ)']:.1%} (๋ชฉํ 20% ์ด์: {'๋ฌ์ฑ' if imp_ok else '๋ฏธ๋ฌ'}), R2 {e['R2']:.3f} (๋ชฉํ 0.85 ์ด์: {'๋ฌ์ฑ' if e['R2'] >= .85 else '๋ฏธ๋ฌ'})\")\n",
"print(f\" ์ฐธ๊ณ : M4 ๋ชจ๋ธ ๋๋น ์ถ๊ฐ ๊ฐ์ ์ MAE {1 - e['MAE(๋ถ)'] / m4['MAE(๋ถ)']:.1%}๋ก, ๊ฐ์ง ์ ๋ขฐ๋ ํผ์ฒ์ ์ถ๊ฐ ํจ๊ณผ๋ ์๋ค(์ด๋ฏธ ์ ์ฐจ ์ด๋ ฅ ํผ์ฒ๊ฐ ์๊ธฐ ๋๋ฌธ).\")\n",
"stress = {\"์ดํ ์ค\": te.off_km >= 1, \"์ ์ฐจ ์งํ(๊ฒฝ๊ณผ 10๋ถ ์ด๋ด)\": (te.since_stop <= 10) & (te.stop_n > 0), \"์ฐ์ฒ\": te.rain == 1, \"ํ์\": (te.off_km < 1) & (te.stop_n == 0) & (te.rain == 0)}\n",
"rr = [(k, int(m.sum()), mae(te.eta_true[m], P[\"๊ธฐ์ค(ํ์ฌ ์๋ ETA)\"][m.values]), mae(te.eta_true[m], P[\"์์๋ธ(ํ๊ท )\"][m.values])) for k, m in stress.items() if m.sum()]\n",
"print(\"[3] ๋ถ๊ท์น ์ํฉ๋ณ MAE(๋ถ): ๊ธฐ์ค vs ์์๋ธ\")\n",
"print(pd.DataFrame(rr, columns=[\"์กฐ๊ฑด\", \"๊ฑด์\", \"๊ธฐ์ค\", \"์์๋ธ\"]).round(1).to_string(index=False))\n",
"print(\"๊ฒฝ๋ณด๋ ํ๋จ ๋ณด์กฐ์ด๋ฉฐ, ํ์ฅ ํ์ธ๊ณผ ๋์ฒด ์์ก ์ง์๋ ์ฌ๋(๊ด์ ์ฌยท์์กํต์ ๊ด)์ด ๊ฒฐ์ ํ๋ค(M7).\")\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"๐ก [์ฐธ๊ณ ] ๋ฏธ์
05 ์์ ์คํ ๊ฒฐ๊ณผ (Output)
\n",
"\n",
"```text\n",
"[1] ์ด๋ฒคํธ ๋จ์ ๊ฒ์ฆ(์ ๋ขฐ๋ 0.5 ์ด์์ ๊ฒฝ๋ณด๋ก ๊ฐ์ฃผ) - ๊นจ๋ํ ๊ด์ธก\n",
" ์ ํ ์ค์ ์ด๋ฒคํธ ํ์ง ์ฌํ์จ ํ๊ท ํ์ง ์ง์ฐ(๋ถ) ๊ฒฝ๋ณด ๊ด์ธก ์ค๊ฒฝ๋ณด ๊ด์ธก\n",
" ๊ฒฝ๋ก ์ดํ 22 22 1.0 3.18 42 0\n",
"๋ถ์ ์ ์ฐจ(10๋ถ ์ด์) 88 88 1.0 0.00 281 0\n",
" ์ก์ ์กฐ๊ฑด(GPS ์ค์ฐจ +0.9km, ์๋ ์ค์ฐจ 7km/h):\n",
" ์ ํ ์ค์ ์ด๋ฒคํธ ํ์ง ์ฌํ์จ ํ๊ท ํ์ง ์ง์ฐ(๋ถ) ๊ฒฝ๋ณด ๊ด์ธก ์ค๊ฒฝ๋ณด ๊ด์ธก\n",
" ๊ฒฝ๋ก ์ดํ 22 22 1.00 1.59 338 283\n",
"๋ถ์ ์ ์ฐจ(10๋ถ ์ด์) 88 60 0.68 4.08 117 2\n",
" ๋ถ์ ์ ์ฐจ ์๊ณ๊ฐ 0.3(๊นจ๋ํ ๊ด์ธก): ์ ๋ฐ๋ 76%, ๊ด์ธก ์ฌํ์จ 100%\n",
" ๋ถ์ ์ ์ฐจ ์๊ณ๊ฐ 0.5(๊นจ๋ํ ๊ด์ธก): ์ ๋ฐ๋ 100%, ๊ด์ธก ์ฌํ์จ 100%\n",
" ๋ถ์ ์ ์ฐจ ์๊ณ๊ฐ 0.8(๊นจ๋ํ ๊ด์ธก): ์ ๋ฐ๋ 100%, ๊ด์ธก ์ฌํ์จ 42%\n",
" ์ก์์ด ์ปค์ง๋ฉด ์ค๊ฒฝ๋ณด๊ฐ ๋๊ณ ์ดํ ํ์ ์ด ํ๋ค๋ฆฐ๋ค. ์๊ณ๊ฐ์ ๊ฒฝ๋ณด ํผ๋ก์ ์กฐ๊ธฐ ๋์ ์ฌ์ด์์ ์
๋ฌด์ ์ผ๋ก ์ ํด์ผ ํ๋ค.\n",
"\n",
"[2] Hold-out(์ด๋ 160~199, 1885ํ) ETA ์ฑ๋ฅ\n",
" MAE(๋ถ) RMSE(๋ถ) MAPE(%) R2\n",
"๊ธฐ์ค(ํ์ฌ ์๋ ETA) 43.633 86.625 31.275 -0.461\n",
"M4 ๋ชจ๋ธ 8.677 12.747 7.235 0.968\n",
"HGB+๊ฐ์ง ์ ๋ขฐ๋ 8.699 12.755 7.285 0.968\n",
"RandomForest 8.520 12.319 6.959 0.970\n",
"์์๋ธ(ํ๊ท ) 8.485 12.344 6.964 0.970\n",
" ๊ธฐ์ค ๋๋น MAE ๊ฐ์ 80.6%, RMSE ๊ฐ์ 85.8% (๋ชฉํ 20% ์ด์: ๋ฌ์ฑ), R2 0.970 (๋ชฉํ 0.85 ์ด์: ๋ฌ์ฑ)\n",
" ์ฐธ๊ณ : M4 ๋ชจ๋ธ ๋๋น ์ถ๊ฐ ๊ฐ์ ์ MAE 2.2%๋ก, ๊ฐ์ง ์ ๋ขฐ๋ ํผ์ฒ์ ์ถ๊ฐ ํจ๊ณผ๋ ์๋ค(์ด๋ฏธ ์ ์ฐจ ์ด๋ ฅ ํผ์ฒ๊ฐ ์๊ธฐ ๋๋ฌธ).\n",
"[3] ๋ถ๊ท์น ์ํฉ๋ณ MAE(๋ถ): ๊ธฐ์ค vs ์์๋ธ\n",
" ์กฐ๊ฑด ๊ฑด์ ๊ธฐ์ค ์์๋ธ\n",
" ์ดํ ์ค 6 337.0 10.6\n",
"์ ์ฐจ ์งํ(๊ฒฝ๊ณผ 10๋ถ ์ด๋ด) 108 243.0 11.3\n",
" ์ฐ์ฒ 453 56.2 8.1\n",
" ํ์ 1162 28.4 8.0\n",
"๊ฒฝ๋ณด๋ ํ๋จ ๋ณด์กฐ์ด๋ฉฐ, ํ์ฅ ํ์ธ๊ณผ ๋์ฒด ์์ก ์ง์๋ ์ฌ๋(๊ด์ ์ฌยท์์กํต์ ๊ด)์ด ๊ฒฐ์ ํ๋ค(M7).\n",
"```\n",
" \n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## ๐ฏ ๋ฏธ์
06. ์ค์๊ฐ ์์ก์ ๋จ ์์น ์ถ์ ๋ฐ ์ค์๊ฐ ETA ๋ณ๋ ๋์๋ณด๋ UI ๊ตฌํ\n",
"**์์์๊ฐ**: ์ฝ 3.0์๊ฐ\n",
"\n",
"### ๐ ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ์ํ ์ง์นจ\n",
"#### ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ๋น์์ฑ\n",
"\n",
"์ค์๊ฐ ์์ก์ ๋จ ์์น ์ถ์ ๋ฐ ETA ๋ณ๋ ๋์๋ณด๋ UI๋ฅผ ๊ตฌํํ๋ค. ์ค์ ์ ์ ํ์ฅ์์๋ ๋จ์ ํ์ง์ ๊ทธ์น์ง ์๊ณ ์ ์ํ ์ํ ํ๋จ๊ณผ ์ ๋ขฐ๋ ๋์ ์์ฌ๊ฒฐ์ ์ด ํ์์ ์ด๋ฏ๋ก, **์ค์๊ฐ ์์ก์ ๋จ ์์น ์ถ์ ๊ณผ ETA ๋ณ๋์ ๋ณด์ฌ ์ฃผ๋ ๋์๋ณด๋๋ฅผ ๊ตฌํํ๊ณ , ์ง์ฐ ์์ธ๋ณ ๊ธฐ์ฌ๋๋ฅผ ๊ฑด๋ง๋ค ์๋ ์ฐ์ถํด ์ค๋ช
ํ๋ค.** ๊ณผ์
์ ์์ํ์ฌ ์คํ์ ๋ฐฉ์งํ๊ณ ์์ ๋์๋ ฅ์ ๊ทน๋ํํด์ผ ํฉ๋๋ค.\n",
"\n",
"์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ณธ ๋ฏธ์
์์๋ **์ถ๋ฐ ํ 120๋ถ ์์ ์ ์ ๋จ ์์น์ ์ํ(์ ์ยท์ฃผ์ยท์ง์ฐยท์ ์ฐจยท์ดํ)๋ฅผ ํ์ ๋
ธ์ ๋๋ก ๋ณด์ฌ ์ค๋ค ๊ณผ์ ์ ์งํํ๋** ์ค์ต์ ์ฒด๊ณ์ ์ผ๋ก ์ํํฉ๋๋ค.\n",
"\n",
"ํ์ต์๋ ์ค์ต๊ฐ์ด๋์ ์ ์๋ ์ด 4๊ฐ ์ธ๋ถ ํ๋(๊ธฐ๋ณธ ๊ธฐ๋ฅ ๊ตฌํ โ ๊ฒฐ๊ณผ ์งํ ๊ฒ์ฆ โ ํ๋ผ๋ฏธํฐ ํ๋ ๋ฐ ๋์ ๊ณผ์ )์ ์์ฐจ์ ์ผ๋ก ์งํํ๋ฉฐ ํต์ฌ ํ์ ๊ธฐ์ค์ ๋ฌ์ฑํฉ๋๋ค.\n",
"\n",
"**์ด๋ฒ ๊ฒฐ๊ณผ๋ฌผ:** `src/explainability/mov06_xai_explainer.py ๋ฐ XAI ๋ฆฌํฌํธ` ํํ์ ๊ฒฐ๊ณผ๋ฌผ์ ์์ฑํ๋ค.\n",
"\n",
"### ๐ ํ๊ฐ ๋ฐ ๊ฒ์ฆ ๊ธฐ์ค (PASS ์กฐ๊ฑด)\n",
"#### ๋ฏธ์
ํ์ \n",
"\n",
"**ํ๊ฐ ๊ตฌ๋ถ:** ํ์\n",
"\n",
"**ํต์ฌ ํ๊ฐ์์ญ:** ๊ตฐ์ ์ฅ๊ต/๊ตฐ๋ฌด์์ด ์ ๋ขฐํ๊ณ ์กฐ์นํ ์ ์๋ ์ค๋ช
์ ๋ช
ํ์ฑ\n",
"\n",
"#### ๋ฏธ์
PASS ์กฐ๊ฑด\n",
"\n",
"- ์ ๋ ๊ธฐ์ค: ์ฃผ์ ์์ธก ๊ฑด๋น ๊ธฐ์ฌ๋ ์์ 3๊ฐ ์ธ์ ์๋ ์ฐ์ถ๋ฅ 100%\n",
"\n",
"\n",
"- ์งํ ๊ธฐ์ค: SHAP summary plot ๋ฐ waterfall plot ์์ฑ ํ์ดํ๋ผ์ธ ์์ฑ\n",
"\n",
"\n",
"- ๊ฒ์ฆ ๋ฐฉ๋ฒ: ์ค๋ฌด์ ๋์ ์ค๋ช
๊ทผ๊ฑฐ ์ ์ฉ์ฑ ํ๊ฐ ๊ฒ์ฆ\n",
"\n",
"\n",
"#### ์ฑ์ ์ ํ์ ๋ฉ๋ชจ\n",
"\n",
"> ๋จ์ ์ฝ๋ ์คํ ์ฌ๋ถ๋ฟ๋ง ์๋๋ผ ์ถ๋ ฅ๋ ์์น์ ๊ทผ๊ฑฐ ์๋ฃ๊ฐ ๊ตฐ ์ค๋ฌด ์๊ตฌ์ฌํญ์ ๋ถํฉํ๋์ง ๋ฉด๋ฐํ ํ์ธํ๋ค.\n",
"\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"\"\"\"์ค์๊ฐ ์์ก์ ๋จ ์์น ์ถ์ ๊ณผ ETA ๋ณ๋ ๋์๋ณด๋๋ฅผ ๊ตฌํํ๊ณ , ETA ์ง์ฐ๋ถ์ ๋ํ ์์ธ๊ตฐ๋ณ ๊ธฐ์ฌ๋ ์์ 3๊ฐ๋ฅผ ๊ฑด๋ง๋ค ์๋ ์ฐ์ถํด ์์ฐ์ด ์ค๋ช
์ผ๋ก ๋ณด์ฌ ์ค๋ค.\"\"\"\n",
"import numpy as np\n",
"import pandas as pd\n",
"import matplotlib.pyplot as plt\n",
"from sklearn.ensemble import HistGradientBoostingRegressor\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# [๊ต์ก์ฉ๊ฐ์์๋ฃ] ๊ฐ์ ์์ก ๋
ธ์ ยท์์ก์ ๋จ ํ
๋ ๋ฉํธ๋ฆฌ ์์ฑ๊ธฐ (์ค์ ๋๋กยท์ง๋ช
ยท๋ถ๋ ์์)\n",
"# ๊ฐ์ ๋
ธ์ (์ถ๋ฐ ๊ฑฐ์ -> ๋์ฐฉ ๊ฑฐ์ , 5๊ฐ ๊ตฌ๊ฐ)์ 5๋ถ ๊ฐ๊ฒฉ์ผ๋ก ๋ณด๊ณ ํ๋ ์ ๋จ GPSยทํ๋ฌผ ์ผ์ยทํต์ ์ํ ์๋ฃ๋ฅผ ๋ง๋ ๋ค.\n",
"# ---------------------------------------------------------------------------\n",
"PTS = [(0, 0), (30, 12), (62, 14), (88, 38), (112, 42), (135, 66)] # ๋
ธ์ ๊ผญ์ง์ (km, ๊ฐ์ ์ขํ)\n",
"ROUTE = pd.DataFrame({\"seg\": [\"R1\", \"R2\", \"R3\", \"R4\", \"R5\"], \"road\": [\"paved\", \"unpaved\", \"paved\", \"paved\", \"unpaved\"],\n",
" \"limit\": [70, 50, 70, 50, 50], \"kind\": [\"์ผ๋ฐ\", \"์ผ๋ฐ\", \"์ผ๋ฐ\", \"๊ต๋ยทํฐ๋\", \"์ผ๋ฐ\"], \"threat\": [0, 0, 1, 0, 0]})\n",
"ROUTE[\"x0\"], ROUTE[\"y0\"] = [p[0] for p in PTS[:-1]], [p[1] for p in PTS[:-1]]\n",
"ROUTE[\"x1\"], ROUTE[\"y1\"] = [p[0] for p in PTS[1:]], [p[1] for p in PTS[1:]]\n",
"ROUTE[\"km\"] = np.hypot(ROUTE[\"x1\"] - ROUTE[\"x0\"], ROUTE[\"y1\"] - ROUTE[\"y0\"])\n",
"ROUTE[\"c0\"] = np.r_[0, np.cumsum(ROUTE[\"km\"])[:-1]]\n",
"ROUTE[\"c1\"] = np.cumsum(ROUTE[\"km\"])\n",
"LEN = float(ROUTE[\"km\"].sum())\n",
"ROAD_F = {\"paved\": 1.0, \"unpaved\": 0.75}\n",
"C1L, C0L, KML = ROUTE[\"c1\"].tolist(), ROUTE[\"c0\"].tolist(), ROUTE[\"km\"].tolist() # ๋ฐ๋ณต ๊ณ์ฐ์ฉ ๋ฆฌ์คํธ(์๋)\n",
"BASE_V = [l * ROAD_F[r] for l, r in zip(ROUTE[\"limit\"], ROUTE[\"road\"])]\n",
"THR, UNP, TUN = ROUTE[\"threat\"].tolist(), (ROUTE[\"road\"] == \"unpaved\").tolist(), (ROUTE[\"kind\"] == \"๊ต๋ยทํฐ๋\").tolist()\n",
"\n",
"\n",
"def seg_of(s):\n",
" # [TODO : ํ์ ์ค์ต 6] ์ค์๊ฐ ์์ก์ ๋จ ์์น ์ถ์ ๋ฐ ์ค์๊ฐ ETA ๋ณ๋ ๋์๋ณด๋ UI ๊ตฌํ ๊ด๋ จ ํต์ฌ ๋ก์ง์ ๊ตฌํํ์ธ์.\n",
" # ํํธ: ์๋จ ์ฃผ์ ๋ฐ ๋ฐ์ดํฐ ์คํค๋ง/์ง์นจ์ ์ฐธ๊ณ ํ์ฌ ์์ฑํฉ๋๋ค.\n",
" # <<< ์ฌ๊ธฐ์ ์ฝ๋๋ฅผ ์์ฑํ์ธ์ >>>\n",
" pass # ์์ฑ ํ pass๋ฅผ ์ ๊ฑฐํ์ธ์.\n",
"def route_xy(s):\n",
" \"\"\"๋
ธ์ ์ ์งํ๊ฑฐ๋ฆฌ s(km)์ ์ขํ์ ์งํ ๋ฐฉํฅ(๋).\"\"\"\n",
" # [TODO : ํ์ ์ค์ต 6] ์ค์๊ฐ ์์ก์ ๋จ ์์น ์ถ์ ๋ฐ ์ค์๊ฐ ETA ๋ณ๋ ๋์๋ณด๋ UI ๊ตฌํ ๊ด๋ จ ํต์ฌ ๋ก์ง์ ๊ตฌํํ์ธ์.\n",
" # ํํธ: ์๋จ ์ฃผ์ ๋ฐ ๋ฐ์ดํฐ ์คํค๋ง/์ง์นจ์ ์ฐธ๊ณ ํ์ฌ ์์ฑํฉ๋๋ค.\n",
" # <<< ์ฌ๊ธฐ์ ์ฝ๋๋ฅผ ์์ฑํ์ธ์ >>>\n",
" pass # ์์ฑ ํ pass๋ฅผ ์ ๊ฑฐํ์ธ์.\n",
"def project(x, y):\n",
" \"\"\"GPS ์ขํ๋ฅผ ๋
ธ์ ์ ํฌ์ํด (์งํ๊ฑฐ๋ฆฌ km, ๋
ธ์ ๊ณผ์ ์์ง ์ดํ km)๋ฅผ ๊ตฌํ๋ค.\"\"\"\n",
" x, y = np.atleast_1d(x).astype(float), np.atleast_1d(y).astype(float)\n",
" best_s, best_d = np.zeros(len(x)), np.full(len(x), np.inf)\n",
" for r in ROUTE.itertuples():\n",
" dx, dy = r.x1 - r.x0, r.y1 - r.y0\n",
" f = np.clip(((x - r.x0) * dx + (y - r.y0) * dy) / (dx * dx + dy * dy), 0, 1)\n",
" d = np.hypot(x - (r.x0 + f * dx), y - (r.y0 + f * dy))\n",
" better = d < best_d\n",
" best_s, best_d = np.where(better, r.c0 + f * r.km, best_s), np.where(better, d, best_d)\n",
" return best_s, best_d\n",
"\n",
"\n",
"def cong(clock_min):\n",
" h = (clock_min / 60) % 24\n",
" return 0.82 if (7 <= h < 9 or 17 <= h < 19) else 1.0 # ์ถํด๊ทผ ์๊ฐ๋ ์ ์ฒด ๊ณ์(๊ฐ์ )\n",
"\n",
"\n",
"def simulate_trip(rng, tid, start_min, rain, size, cargo):\n",
" \"\"\"1๋ถ ๋จ์๋ก ์ด๋์ ๋ชจ์ํ๊ณ 5๋ถ๋ง๋ค ํ
๋ ๋ฉํธ๋ฆฌ๋ฅผ ๋จ๊ธด๋ค. ์ดํยท๋ถ์ ์ ์ฐจ ์ด๋ฒคํธ๋ ์ ๋ต ๋ผ๋ฒจ๋ก ํจ๊ป ์ ์ฅํ๋ค.\"\"\"\n",
" s, t, stop_left, rows = 0.0, 0, 0, []\n",
" dev = (rng.random() < 0.12)\n",
" dev_t0 = int(rng.integers(30, 120)) if dev else -1\n",
" dev_len = int(rng.integers(12, 25)) if dev else 0\n",
" cause = \"\"\n",
" while s < LEN:\n",
" i = seg_of(s)\n",
" v = BASE_V[i] * (0.88 if rain else 1.0) * (0.85 if (rain and UNP[i]) else 1.0) * cong(start_min + t) \\\n",
" * (0.7 if THR[i] else 1.0) * (1 - 0.012 * size) * rng.normal(1, 0.06)\n",
" off = 0.0\n",
" if dev and dev_t0 <= t < dev_t0 + dev_len:\n",
" ph = (t - dev_t0) / dev_len\n",
" off, v = 4.0 * (1 - abs(2 * ph - 1)), v * 0.4 # ๋
ธ์ ์ดํ(์ผ๊ฐํ ํํ๋ก ์ต๋ 4km)\n",
" if stop_left > 0:\n",
" v, stop_left = 0.0, stop_left - 1\n",
" elif rng.random() < 0.0015:\n",
" stop_left, cause = int(rng.integers(8, 36)), (\"maint\" if rng.random() < 0.7 else \"urgent\")\n",
" s += v / 60\n",
" t += 1\n",
" if t % 5 == 0 or s >= LEN:\n",
" x, y, hd = route_xy(min(s, LEN))\n",
" nx, ny = np.cos(np.radians(hd)), -np.sin(np.radians(hd))\n",
" j = seg_of(min(s, LEN)); tun = TUN[j]\n",
" rows.append(dict(trip=tid, t=t, clock=(start_min + t) % 1440, x=x + off * nx + rng.normal(0, 0.03), y=y + off * ny + rng.normal(0, 0.03),\n",
" speed=max(v * rng.normal(1, 0.03), 0.0), heading=(hd + rng.normal(0, 3)) % 360, rssi=-62 - 14 * tun + rng.normal(0, 3),\n",
" cargo_temp=18 + 3 * np.sin(t / 40) + rng.normal(0, 0.3), shock_g=abs(rng.normal(0.25, 0.1)) * (2.0 if UNP[j] else 1.0),\n",
" s_true=min(s, LEN), off_true=off, stopped_true=int(v == 0.0), rain=int(rain), size=size, cargo=cargo, stop_cause=cause if v == 0.0 else \"\"))\n",
" arr = t\n",
" for r in rows:\n",
" r[\"eta_true\"] = arr - r[\"t\"]\n",
" return rows\n",
"\n",
"\n",
"def make_telemetry(seed=21, n_trips=200):\n",
" \"\"\"n_trips๊ฐ ์์ก์ ๋จ ์ด๋ ๊ธฐ๋ก(๋ ์ง ์). ์ ๋จ ๊ท๋ชจ 3~12๋, ํ๋ฌผ ๊ตฌ๋ถ ์ผ๋ฐ/๋์ฅ/๋ฏผ๊ฐ.\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" rows = []\n",
" for k in range(n_trips):\n",
" rows += simulate_trip(rng, k, int(rng.integers(5 * 60, 15 * 60)), bool(rng.random() < 0.2), int(rng.integers(3, 13)), str(rng.choice([\"์ผ๋ฐ\", \"๋์ฅ\", \"๋ฏผ๊ฐ\"], p=[.6, .25, .15])))\n",
" return pd.DataFrame(rows)\n",
"\n",
"\n",
"def mape(y, p):\n",
" y, p = np.asarray(y, float), np.asarray(p, float)\n",
" return float(np.mean(np.abs(y - p) / np.abs(y)) * 100)\n",
"\n",
"\n",
"def mae(y, p):\n",
" return float(np.mean(np.abs(np.asarray(y, float) - np.asarray(p, float))))\n",
"# ---------------------------------------------------------------------------\n",
"# ---------------------------------------------------------------------------\n",
"# ETA ์์ธก ํผ์ฒ: ๊ด์ธก ์์ ๊น์ง์ ํ
๋ ๋ฉํธ๋ฆฌ์ ๋จ์ ๋
ธ์ ๊ตฌ์ฑ(ํฌ์ฅยท์ํยท๊ต๋ํฐ๋ ๊ตฌ๊ฐ ๊ฑฐ๋ฆฌ)๋ง ์ฌ์ฉ\n",
"# ---------------------------------------------------------------------------\n",
"FEATS = [\"remaining_km\", \"progress\", \"speed\", \"speed_ma3\", \"speed_std3\", \"stop_n\", \"stop_min\", \"since_stop\", \"hour_sin\", \"hour_cos\", \"cong\", \"rain\", \"size\",\n",
" \"k_๋์ฅ\", \"k_๋ฏผ๊ฐ\", \"rem_unpaved_km\", \"rem_threat_km\", \"rem_bridge_km\", \"off_km\", \"rssi\", \"base_eta\"]\n",
"\n",
"\n",
"def rem_composition(s):\n",
" \"\"\"์งํ๊ฑฐ๋ฆฌ s ์ดํ ๋จ์ ๊ตฌ๊ฐ ์ค ๋นํฌ์ฅยท์ํยท๊ต๋ํฐ๋ ๊ฑฐ๋ฆฌ(km).\"\"\"\n",
" left = np.clip(ROUTE[\"c1\"].values[None, :] - np.maximum(np.atleast_1d(s)[:, None], ROUTE[\"c0\"].values[None, :]), 0, None)\n",
" return left @ np.array(UNP, float), left @ np.array(THR, float), left @ np.array(TUN, float)\n",
"\n",
"\n",
"def add_features(df):\n",
" d = df.sort_values([\"trip\", \"t\"]).copy()\n",
" d[\"s_obs\"], d[\"off_km\"] = project(d[\"x\"].values, d[\"y\"].values)\n",
" d[\"remaining_km\"] = LEN - d[\"s_obs\"]\n",
" d[\"progress\"] = d[\"s_obs\"] / LEN\n",
" g = d.groupby(\"trip\")[\"speed\"]\n",
" d[\"speed_ma3\"] = g.rolling(3, min_periods=1).mean().reset_index(level=0, drop=True)\n",
" d[\"speed_std3\"] = g.rolling(3, min_periods=1).std().reset_index(level=0, drop=True).fillna(0)\n",
" d[\"is_stop\"] = (d[\"speed\"] < 3).astype(int)\n",
" new_stop = (d[\"is_stop\"] == 1) & (d.groupby(\"trip\")[\"is_stop\"].shift(1).fillna(0) == 0)\n",
" d[\"stop_n\"] = new_stop.astype(int).groupby(d[\"trip\"]).cumsum()\n",
" d[\"stop_min\"] = d.groupby(\"trip\")[\"is_stop\"].cumsum() * 5\n",
" blk = (d[\"is_stop\"] == 1).astype(int).groupby(d[\"trip\"]).cumsum() # ์ ์ฐจ ๋์ ํ์๋ก ๊ตฌ๊ฐ์ ๋๋์ด ๋ง์ง๋ง ์ ์ฐจ ์ดํ ๊ฒฝ๊ณผ ๊ณ์ฐ\n",
" d[\"since_stop\"] = d.groupby([\"trip\", blk]).cumcount() * 5\n",
" d[\"hour_sin\"], d[\"hour_cos\"] = np.sin(2 * np.pi * d[\"clock\"] / 1440), np.cos(2 * np.pi * d[\"clock\"] / 1440)\n",
" d[\"cong\"] = d[\"clock\"].map(cong)\n",
" d[\"k_๋์ฅ\"], d[\"k_๋ฏผ๊ฐ\"] = (d[\"cargo\"] == \"๋์ฅ\").astype(int), (d[\"cargo\"] == \"๋ฏผ๊ฐ\").astype(int)\n",
" d[\"rem_unpaved_km\"], d[\"rem_threat_km\"], d[\"rem_bridge_km\"] = rem_composition(d[\"s_obs\"].values)\n",
" d[\"base_eta\"] = np.minimum(d[\"remaining_km\"] / np.maximum(d[\"speed\"], 5.0) * 60, 600) # ํ์ฌ ์๋ ๊ธฐ๋ฐ ๋จ์ ETA(๋ถ, ์ ์ฐจ ์ ํญ์ฃผ ๋ฐฉ์ง๋ก 5km/h ํํยท600๋ถ ์ํ)\n",
" return d.reset_index(drop=True)\n",
"# ---------------------------------------------------------------------------\n",
"\n",
"df = make_telemetry(); d = add_features(df)\n",
"tr = d[d.trip < 160]\n",
"model = HistGradientBoostingRegressor(max_iter=150, max_depth=4, learning_rate=0.08, random_state=0).fit(tr[FEATS], tr[\"eta_true\"])\n",
"d[\"eta_pred\"] = model.predict(d[FEATS]).clip(min=0)\n",
"PLAN = 255 # ์ดํ๊ณํ ์์์๊ฐ(๋ถ, ๊ฐ์ )\n",
"\n",
"# ---------------- 1) ์์ธ๊ตฐ ๊ธฐ์ฌ๋(ํ๊ท ๋์ฒด ๋ฐฉ์): ๊ฑฐ๋ฆฌ ์์ธ์ '์์ ์ฃผํ'์ผ๋ก ๋ณด๊ณ ์ง์ฐ ์์ธ๋ง ์ค๋ช
----------------\n",
"GROUPS = {\"ํ์ฌ ์๋\": [\"speed\", \"speed_ma3\", \"speed_std3\"], \"์ ์ฐจ ์ด๋ ฅ\": [\"stop_n\", \"stop_min\", \"since_stop\"], \"์ฐ์ฒ\": [\"rain\"], \"์๊ฐ๋ ์ ์ฒด\": [\"hour_sin\", \"hour_cos\", \"cong\"],\n",
" \"๋
ธ์ ๊ตฌ์ฑ(๋นํฌ์ฅยท์ํยท๊ต๋ํฐ๋)\": [\"rem_unpaved_km\", \"rem_threat_km\", \"rem_bridge_km\"], \"์ดํยทํต์ \": [\"off_km\", \"rssi\"], \"์ ๋จยทํ๋ฌผ\": [\"size\", \"k_๋์ฅ\", \"k_๋ฏผ๊ฐ\"]}\n",
"mean_row = tr[FEATS].mean()\n",
"te = d[d.trip >= 160].copy()\n",
"contrib = pd.DataFrame(index=te.index)\n",
"for g, cols in GROUPS.items():\n",
" x = te[FEATS].copy(); x[cols] = mean_row[cols].values\n",
" contrib[g] = te[\"eta_pred\"] - model.predict(x).clip(min=0) # ์์ = ์ด ์์ธ์ด ๋์ฐฉ์ ๋ฆ์ถค\n",
"top3 = contrib.abs().apply(lambda r: r.nlargest(3).index.tolist(), axis=1)\n",
"ok = int(sum(len(t) == 3 and len(set(t)) == 3 and np.isfinite(contrib.loc[i, t].values).all() for i, t in top3.items()))\n",
"weak = int((contrib.abs().apply(lambda r: r.nlargest(3).iloc[2], axis=1) < 1.0).sum())\n",
"print(f\"[1] ETA ์์ธก {len(te)}๊ฑด ์ค ๊ธฐ์ฌ๋ ์์ 3๊ฐ ์์ธ ์๋ ์ฐ์ถ {ok}๊ฑด ({ok / len(te):.0%}, ๋ชฉํ 100%), 3๋ฒ์งธ ์์ธ ๊ธฐ์ฌ 1๋ถ ๋ฏธ๋ง {weak}๊ฑด\")\n",
"print(\" ์์ธ๊ตฐ๋ณ ํ๊ท ๊ธฐ์ฌ ํฌ๊ธฐ(๋ถ):\", \", \".join(f\"{k} {v:.1f}\" for k, v in contrib.abs().mean().sort_values(ascending=False).items()))\n",
"\n",
"\n",
"def explain(i):\n",
" r, c = te.loc[i], contrib.loc[i]\n",
" main = top3[i][0]\n",
" why = {\"ํ์ฌ ์๋\": \"ํ์ฌ ์๋๊ฐ ๋ฎ์\", \"์ ์ฐจ ์ด๋ ฅ\": \"์ ์ฐจ ์ด๋ ฅ์ผ๋ก\", \"์ฐ์ฒ\": \"์ฐ์ฒ์ผ๋ก\", \"์๊ฐ๋ ์ ์ฒด\": \"์๊ฐ๋ ์ ์ฒด๋ก\", \"๋
ธ์ ๊ตฌ์ฑ(๋นํฌ์ฅยท์ํยท๊ต๋ํฐ๋)\": \"๋จ์ ๊ตฌ๊ฐ(๋นํฌ์ฅยท์ํ ๊ตฌ์ญ)์ด ๋๋ ค\",\n",
" \"์ดํยทํต์ \": \"๋
ธ์ ์ดํยทํต์ ์ํ๋ก\", \"์ ๋จยทํ๋ฌผ\": \"์ ๋จ ๊ท๋ชจยทํ๋ฌผ ํน์ฑ์ผ๋ก\"}[main]\n",
" return f\"trip {int(r['trip'])} t={int(r['t'])}๋ถ: {why} ๋์ฐฉ๊น์ง {r['eta_pred']:.0f}๋ถ ์์. ์ฃผ์ ์์ธ \" + \", \".join(f\"{g}({c[g]:+.1f}๋ถ)\" for g in top3[i])\n",
"\n",
"\n",
"# ---------------- 2) ์์ ์ค๋
์ท: ์ ๋จ 12๊ฐ์ ์ค์๊ฐ ์ํํ ----------------\n",
"snap = te[(te[\"t\"] == 120) & (te[\"trip\"] < 172)].copy()\n",
"snap[\"๋์ฐฉ ์์ (๋ถ)\"] = snap[\"t\"] + snap[\"eta_pred\"]\n",
"snap[\"๊ณํ ๋๋น(๋ถ)\"] = snap[\"๋์ฐฉ ์์ (๋ถ)\"] - PLAN\n",
"snap[\"์ํ\"] = np.select([snap[\"off_km\"] >= 1, snap[\"is_stop\"] == 1, snap[\"๊ณํ ๋๋น(๋ถ)\"] >= 20, snap[\"๊ณํ ๋๋น(๋ถ)\"] >= 10], [\"์ดํ\", \"์ ์ฐจ\", \"์ง์ฐ\", \"์ฃผ์\"], \"์ ์\")\n",
"print(f\"\\n[2] ์ ๋จ ์ํํ(์ถ๋ฐ ํ 120๋ถ, ์ ๋จ {len(snap)}๊ฐ, ์ดํ๊ณํ {PLAN}๋ถ ๊ธฐ์ค) \" + str(snap[\"์ํ\"].value_counts().to_dict()))\n",
"show = snap[[\"trip\", \"progress\", \"speed\", \"eta_pred\", \"๊ณํ ๋๋น(๋ถ)\", \"์ํ\"]].rename(columns={\"progress\": \"์งํ๋ฅ \", \"speed\": \"์๋\", \"eta_pred\": \"ETA(๋ถ)\"}).round(1).head(8)\n",
"print(show.to_string(index=False))\n",
"worst = snap.sort_values(\"๊ณํ ๋๋น(๋ถ)\", ascending=False).head(3).index\n",
"print(\"[3] ์ง์ฐ ์์ 3๊ฐ ์ ๋จ ์์ฐ์ด ์ค๋ช
\")\n",
"for i in worst:\n",
" print(\" -\", explain(i))\n",
"\n",
"# ---------------- 3) ETA ๋ณ๋ ์ถ์ (์ฌ์): ํ ์ ๋จ์ ETA๊ฐ ์๊ฐ์ ๋ฐ๋ผ ์ด๋ป๊ฒ ๋ฐ๋๋ ----------------\n",
"tid = int(te.loc[worst[0], \"trip\"])\n",
"q = te[te.trip == tid].reset_index(drop=True)\n",
"q[\"arrive_pred\"] = q[\"t\"] + q[\"eta_pred\"]\n",
"q[\"change\"] = q[\"arrive_pred\"].diff()\n",
"alerts = q[q[\"change\"].abs() >= 15]\n",
"arr_true = int(q[\"t\"].iloc[-1] + q[\"eta_true\"].iloc[-1])\n",
"print(f\"\\n[4] trip {tid} ETA ๋ณ๋ ์ถ์ : ์ค์ ๋์ฐฉ {arr_true}๋ถ, ์์ธก ๋์ฐฉ ๋ฒ์ {q['arrive_pred'].min():.0f}~{q['arrive_pred'].max():.0f}๋ถ, 15๋ถ ์ด์ ๋ณ๋ ์๋ฆผ {len(alerts)}ํ\")\n",
"for _, r in alerts.head(3).iterrows():\n",
" print(f\" t={int(r['t'])}๋ถ: ์์ธก ๋์ฐฉ {r['arrive_pred']:.0f}๋ถ({r['change']:+.0f}๋ถ ๋ณ๋), ์๋ {r['speed']:.0f}km/h, ์ ์ฐจ ๋์ {int(r['stop_min'])}๋ถ\")\n",
"print(\" ETA ๋ณ๋ ์๋ฆผ์ ํ๋จ ๋ณด์กฐ์ด๋ฉฐ, ํ์ฅ ํ์ธ๊ณผ ๋์ฒด ์์ก ์ฌ๋ถ๋ ์ฌ๋์ด ๊ฒฐ์ ํ๋ค(M7).\")\n",
"\n",
"fig, ax = plt.subplots(2, 2, figsize=(13, 8))\n",
"ax[0, 0].plot(PTS[0][0], PTS[0][1], \"ks\"); ax[0, 0].plot([p[0] for p in PTS], [p[1] for p in PTS], \"-\", c=\"lightgray\", lw=6)\n",
"col = {\"์ ์\": \"tab:green\", \"์ฃผ์\": \"tab:olive\", \"์ง์ฐ\": \"tab:orange\", \"์ ์ฐจ\": \"tab:red\", \"์ดํ\": \"tab:purple\"}\n",
"for r in snap.itertuples():\n",
" ax[0, 0].plot(r.x, r.y, \"o\", c=col[r.์ํ], ms=9)\n",
"ax[0, 0].set_title(\"1 Convoy positions at t=120 min (green ok / red stopped / purple off-route)\"); ax[0, 0].set_aspect(\"equal\")\n",
"ax[0, 1].plot(q[\"t\"], q[\"arrive_pred\"], \"o-\", label=\"predicted arrival\"); ax[0, 1].axhline(arr_true, c=\"k\", ls=\"--\", label=\"actual arrival\"); ax[0, 1].axhline(PLAN, c=\"g\", ls=\":\", label=\"plan\")\n",
"ax[0, 1].legend(); ax[0, 1].set_title(f\"2 ETA tracking, trip {tid} (minutes since departure)\")\n",
"snap[\"์ํ\"].value_counts().reindex(list(col)).fillna(0).plot.bar(ax=ax[1, 0], color=list(col.values())); ax[1, 0].set_title(\"3 Status count\"); ax[1, 0].set_xlabel(\"\"); ax[1, 0].set_xticklabels([\"ok\", \"caution\", \"late\", \"stopped\", \"off-route\"], rotation=0)\n",
"cw = contrib.loc[worst[0]]\n",
"ax[1, 1].barh([\"speed\", \"stops\", \"rain\", \"congestion\", \"route mix\", \"off-route/comm\", \"fleet/cargo\"][::-1], cw[list(GROUPS)].values[::-1]); ax[1, 1].set_title(\"4 Delay contribution of worst convoy (min)\")\n",
"plt.suptitle(\"MOV06 convoy tracking dashboard (synthetic)\"); plt.tight_layout(); plt.show()\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"๐ก [์ฐธ๊ณ ] ๋ฏธ์
06 ์์ ์คํ ๊ฒฐ๊ณผ (Output)
\n",
"\n",
"```text\n",
"[1] ETA ์์ธก 2034๊ฑด ์ค ๊ธฐ์ฌ๋ ์์ 3๊ฐ ์์ธ ์๋ ์ฐ์ถ 2034๊ฑด (100%, ๋ชฉํ 100%), 3๋ฒ์งธ ์์ธ ๊ธฐ์ฌ 1๋ถ ๋ฏธ๋ง 1081๊ฑด\n",
" ์์ธ๊ตฐ๋ณ ํ๊ท ๊ธฐ์ฌ ํฌ๊ธฐ(๋ถ): ์ฐ์ฒ 5.5, ์ ๋จยทํ๋ฌผ 4.4, ์๊ฐ๋ ์ ์ฒด 3.6, ํ์ฌ ์๋ 2.1, ์ ์ฐจ ์ด๋ ฅ 0.7, ์ดํยทํต์ 0.2, ๋
ธ์ ๊ตฌ์ฑ(๋นํฌ์ฅยท์ํยท๊ต๋ํฐ๋) 0.2\n",
"\n",
"[2] ์ ๋จ ์ํํ(์ถ๋ฐ ํ 120๋ถ, ์ ๋จ 12๊ฐ, ์ดํ๊ณํ 255๋ถ ๊ธฐ์ค) {'์ ์': 8, '์ง์ฐ': 2, '์ ์ฐจ': 2}\n",
" trip ์งํ๋ฅ ์๋ ETA(๋ถ) ๊ณํ ๋๋น(๋ถ) ์ํ\n",
" 160 0.5 43.5 119.0 -16.0 ์ ์\n",
" 161 0.6 46.1 98.6 -36.4 ์ ์\n",
" 162 0.4 36.2 181.1 46.1 ์ง์ฐ\n",
" 163 0.5 52.1 109.0 -26.0 ์ ์\n",
" 164 0.4 0.0 179.8 44.8 ์ ์ฐจ\n",
" 165 0.6 40.2 112.9 -22.1 ์ ์\n",
" 166 0.4 0.0 163.0 28.0 ์ ์ฐจ\n",
" 167 0.6 34.8 134.8 -0.2 ์ ์\n",
"[3] ์ง์ฐ ์์ 3๊ฐ ์ ๋จ ์์ฐ์ด ์ค๋ช
\n",
" - trip 170 t=120๋ถ: ์ฐ์ฒ์ผ๋ก ๋์ฐฉ๊น์ง 182๋ถ ์์. ์ฃผ์ ์์ธ ์ฐ์ฒ(+21.6๋ถ), ์ ๋จยทํ๋ฌผ(+12.9๋ถ), ํ์ฌ ์๋(+2.8๋ถ)\n",
" - trip 162 t=120๋ถ: ์ฐ์ฒ์ผ๋ก ๋์ฐฉ๊น์ง 181๋ถ ์์. ์ฃผ์ ์์ธ ์ฐ์ฒ(+21.6๋ถ), ์ ๋จยทํ๋ฌผ(+12.6๋ถ), ํ์ฌ ์๋(+1.9๋ถ)\n",
" - trip 164 t=120๋ถ: ์ฐ์ฒ์ผ๋ก ๋์ฐฉ๊น์ง 180๋ถ ์์. ์ฃผ์ ์์ธ ์ฐ์ฒ(+26.1๋ถ), ์ ๋จยทํ๋ฌผ(-9.0๋ถ), ํ์ฌ ์๋(+4.5๋ถ)\n",
"\n",
"[4] trip 170 ETA ๋ณ๋ ์ถ์ : ์ค์ ๋์ฐฉ 314๋ถ, ์์ธก ๋์ฐฉ ๋ฒ์ 297~317๋ถ, 15๋ถ ์ด์ ๋ณ๋ ์๋ฆผ 0ํ\n",
" ETA ๋ณ๋ ์๋ฆผ์ ํ๋จ ๋ณด์กฐ์ด๋ฉฐ, ํ์ฅ ํ์ธ๊ณผ ๋์ฒด ์์ก ์ฌ๋ถ๋ ์ฌ๋์ด ๊ฒฐ์ ํ๋ค(M7).\n",
"```\n",
" \n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## ๐ฏ ๋ฏธ์
07. ๋์ฐฉ ์ง์ฐ ๊ฒฝ๋ณด ๋ฐ์ ์ ํ์ฅ ํ์ธ ๋ฐ ๋์ฒด ์์ก ์ง์๋ฅผ ์ํ HITL ์ ์ฐจ ์ค๊ณ\n",
"**์์์๊ฐ**: ์ฝ 3.5์๊ฐ\n",
"\n",
"### ๐ ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ์ํ ์ง์นจ\n",
"#### ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ๋น์์ฑ\n",
"\n",
"๋์ฐฉ ์ง์ฐ ๊ฒฝ๋ณด ๋ฐ์ ์ ํ์ฅ ํ์ธ ๋ฐ ๋์ฒด ์ง์ HITL ์ ์ฐจ๋ฅผ ์ค๊ณํ๋ค. ์ค์ ์ ์ ํ์ฅ์์๋ ๋จ์ ํ์ง์ ๊ทธ์น์ง ์๊ณ ์ ์ํ ์ํ ํ๋จ๊ณผ ์ ๋ขฐ๋ ๋์ ์์ฌ๊ฒฐ์ ์ด ํ์์ ์ด๋ฏ๋ก, **๋์ฐฉ ์ง์ฐ ๊ฒฝ๋ณด๊ฐ ๋ฐ์ํ์ ๋ ํ์ฅ ํ์ธ๊ณผ ๋์ฒด ์์ก ์ง์๋ฅผ ๋น์ฉ ์ต์ ํ๋ก ์ถ์ฒํ๊ณ , ์ฌ๋์ด ์น์ธยท๋ฐ๋ คํ๋ HITL ์ ์ฐจ๋ฅผ ์ค๊ณํ๋ค.** ๊ณผ์
์ ์์ํ์ฌ ์คํ์ ๋ฐฉ์งํ๊ณ ์์ ๋์๋ ฅ์ ๊ทน๋ํํด์ผ ํฉ๋๋ค.\n",
"\n",
"์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ณธ ๋ฏธ์
์์๋ **์ง์ฐ ์์ค ๋จ๊ฐ, ํ์ฅ ํ์ธ ํจ๊ณผ, ๋์ฒด ์์ก ๋น์ฉยท๋์ฐฉ ์๊ฐ, ์๋น ์ฐจ๋ ์ ์ ์ฝ์ ๊ฐ์ ๊ฐ์ผ๋ก ๋๊ณ ๊ธฐ๋๊ฐ ์ต์ ์กฐ์น๋ฅผ ์ถ์ฒํด ํ์ฌ ์๋ ๊ธฐ๋ฐ ๊ดํ ๊ท์น๊ณผ ๋น์ฉ์ ๋น๊ตํ๋ฉฐ ์๋น ์ฐจ๋ ์ยท์ฐ์ฒยท๋ฏผ๊ฐ ํ๋ฌผ ์๋๋ฆฌ์ค์ ๋จ๊ฐ ๋ฏผ๊ฐ๋๋ฅผ ๋ณธ๋ค ๊ณผ์ ์ ์งํํ๋** ์ค์ต์ ์ฒด๊ณ์ ์ผ๋ก ์ํํฉ๋๋ค.\n",
"\n",
"ํ์ต์๋ ์ค์ต๊ฐ์ด๋์ ์ ์๋ ์ด 4๊ฐ ์ธ๋ถ ํ๋(๊ธฐ๋ณธ ๊ธฐ๋ฅ ๊ตฌํ โ ๊ฒฐ๊ณผ ์งํ ๊ฒ์ฆ โ ํ๋ผ๋ฏธํฐ ํ๋ ๋ฐ ๋์ ๊ณผ์ )์ ์์ฐจ์ ์ผ๋ก ์งํํ๋ฉฐ ํต์ฌ ํ์ ๊ธฐ์ค์ ๋ฌ์ฑํฉ๋๋ค.\n",
"\n",
"**์ด๋ฒ ๊ฒฐ๊ณผ๋ฌผ:** `src/optimization/mov06_simulator.py` ํํ์ ๊ฒฐ๊ณผ๋ฌผ์ ์์ฑํ๋ค.\n",
"\n",
"### ๐ ํ๊ฐ ๋ฐ ๊ฒ์ฆ ๊ธฐ์ค (PASS ์กฐ๊ฑด)\n",
"#### ๋ฏธ์
ํ์ \n",
"\n",
"**ํ๊ฐ ๊ตฌ๋ถ:** ํ์\n",
"\n",
"**ํต์ฌ ํ๊ฐ์์ญ:** ํ์ค์ ๊ตฐ์ ์ ์ฝ์กฐ๊ฑด ๋ฐ์๋ ๋ฐ ์๋๋ฆฌ์ค๋ณ ๋์ ์ ์ ์ ์ ์ฑ\n",
"\n",
"#### ๋ฏธ์
PASS ์กฐ๊ฑด\n",
"\n",
"- ์ ๋ ๊ธฐ์ค: ์ต์ ํ ์ ์ฉ ์ ๋น์ฉ 15% ์ ๊ฐ ๋๋ ์ฌ๊ณ ํ์ ์จ 20% ํฅ์\n",
"\n",
"\n",
"- ์งํ ๊ธฐ์ค: ์ ํ/์ ์ ๊ณํ๋ฒ(LP/MIP) ์ต์ ํ ํด ๋์ถ ๋ฐ ์๋ฎฌ๋ ์ด์
์๋ฃ\n",
"\n",
"\n",
"- ๊ฒ์ฆ ๋ฐฉ๋ฒ: ๊ณผ๊ฑฐ ์ค์ ๋ฐ์ดํฐ ๋๋น ์ต์ ํ ๊ฒฐ๊ณผ ๋น๊ต ๋ฐฑํ
์คํ
\n",
"\n",
"\n",
"#### ์ฑ์ ์ ํ์ ๋ฉ๋ชจ\n",
"\n",
"> ๋จ์ ์ฝ๋ ์คํ ์ฌ๋ถ๋ฟ๋ง ์๋๋ผ ์ถ๋ ฅ๋ ์์น์ ๊ทผ๊ฑฐ ์๋ฃ๊ฐ ๊ตฐ ์ค๋ฌด ์๊ตฌ์ฌํญ์ ๋ถํฉํ๋์ง ๋ฉด๋ฐํ ํ์ธํ๋ค.\n",
"\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"\"\"\"๋์ฐฉ ์ง์ฐ ๊ฒฝ๋ณด ๋ฐ์ ์ ํ์ฅ ํ์ธยท๋์ฒด ์์ก ์ง์์์ ๋น์ฉ ์ต์ ํ๋ก ์ถ์ฒํ๊ณ (์๋น ๋์ฒด ์์ก ์ ์ ์ฝ ๋ฐ์), ๋ชจ๋ ์ง์๋ฅผ ๊ด์ ์ฌ ์์ ํ ์์กํต์ ๊ดยท๊ตฐ์์ฐธ๋ชจ๊ฐ ์ง์ ์น์ธยท๋ฐ๋ คํ๋ HITL ์ ์ฐจ(๊ถํยท2๋จ๊ณ ๊ฒฐ์ฌยทํด์ ์ฒด์ธ ๊ฐ์ฌ ๋ก๊ทธ)๋ฅผ ๊ตฌํํ๋ค.\"\"\"\n",
"import hashlib\n",
"import json\n",
"import numpy as np\n",
"import pandas as pd\n",
"import matplotlib.pyplot as plt\n",
"from scipy.stats import norm\n",
"from sklearn.ensemble import HistGradientBoostingRegressor\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# [๊ต์ก์ฉ๊ฐ์์๋ฃ] ๊ฐ์ ์์ก ๋
ธ์ ยท์์ก์ ๋จ ํ
๋ ๋ฉํธ๋ฆฌ ์์ฑ๊ธฐ (์ค์ ๋๋กยท์ง๋ช
ยท๋ถ๋ ์์)\n",
"# ๊ฐ์ ๋
ธ์ (์ถ๋ฐ ๊ฑฐ์ -> ๋์ฐฉ ๊ฑฐ์ , 5๊ฐ ๊ตฌ๊ฐ)์ 5๋ถ ๊ฐ๊ฒฉ์ผ๋ก ๋ณด๊ณ ํ๋ ์ ๋จ GPSยทํ๋ฌผ ์ผ์ยทํต์ ์ํ ์๋ฃ๋ฅผ ๋ง๋ ๋ค.\n",
"# ---------------------------------------------------------------------------\n",
"PTS = [(0, 0), (30, 12), (62, 14), (88, 38), (112, 42), (135, 66)] # ๋
ธ์ ๊ผญ์ง์ (km, ๊ฐ์ ์ขํ)\n",
"ROUTE = pd.DataFrame({\"seg\": [\"R1\", \"R2\", \"R3\", \"R4\", \"R5\"], \"road\": [\"paved\", \"unpaved\", \"paved\", \"paved\", \"unpaved\"],\n",
" \"limit\": [70, 50, 70, 50, 50], \"kind\": [\"์ผ๋ฐ\", \"์ผ๋ฐ\", \"์ผ๋ฐ\", \"๊ต๋ยทํฐ๋\", \"์ผ๋ฐ\"], \"threat\": [0, 0, 1, 0, 0]})\n",
"ROUTE[\"x0\"], ROUTE[\"y0\"] = [p[0] for p in PTS[:-1]], [p[1] for p in PTS[:-1]]\n",
"ROUTE[\"x1\"], ROUTE[\"y1\"] = [p[0] for p in PTS[1:]], [p[1] for p in PTS[1:]]\n",
"ROUTE[\"km\"] = np.hypot(ROUTE[\"x1\"] - ROUTE[\"x0\"], ROUTE[\"y1\"] - ROUTE[\"y0\"])\n",
"ROUTE[\"c0\"] = np.r_[0, np.cumsum(ROUTE[\"km\"])[:-1]]\n",
"ROUTE[\"c1\"] = np.cumsum(ROUTE[\"km\"])\n",
"LEN = float(ROUTE[\"km\"].sum())\n",
"ROAD_F = {\"paved\": 1.0, \"unpaved\": 0.75}\n",
"C1L, C0L, KML = ROUTE[\"c1\"].tolist(), ROUTE[\"c0\"].tolist(), ROUTE[\"km\"].tolist() # ๋ฐ๋ณต ๊ณ์ฐ์ฉ ๋ฆฌ์คํธ(์๋)\n",
"BASE_V = [l * ROAD_F[r] for l, r in zip(ROUTE[\"limit\"], ROUTE[\"road\"])]\n",
"THR, UNP, TUN = ROUTE[\"threat\"].tolist(), (ROUTE[\"road\"] == \"unpaved\").tolist(), (ROUTE[\"kind\"] == \"๊ต๋ยทํฐ๋\").tolist()\n",
"\n",
"\n",
"def seg_of(s):\n",
" # [TODO : ํ์ ์ค์ต 7] ๋์ฐฉ ์ง์ฐ ๊ฒฝ๋ณด ๋ฐ์ ์ ํ์ฅ ํ์ธ ๋ฐ ๋์ฒด ์์ก ์ง์๋ฅผ ์ํ HITL ์ ์ฐจ ์ค๊ณ ๊ด๋ จ ํต์ฌ ๋ก์ง์ ๊ตฌํํ์ธ์.\n",
" # ํํธ: ์๋จ ์ฃผ์ ๋ฐ ๋ฐ์ดํฐ ์คํค๋ง/์ง์นจ์ ์ฐธ๊ณ ํ์ฌ ์์ฑํฉ๋๋ค.\n",
" # <<< ์ฌ๊ธฐ์ ์ฝ๋๋ฅผ ์์ฑํ์ธ์ >>>\n",
" pass # ์์ฑ ํ pass๋ฅผ ์ ๊ฑฐํ์ธ์.\n",
"def route_xy(s):\n",
" \"\"\"๋
ธ์ ์ ์งํ๊ฑฐ๋ฆฌ s(km)์ ์ขํ์ ์งํ ๋ฐฉํฅ(๋).\"\"\"\n",
" # [TODO : ํ์ ์ค์ต 7] ๋์ฐฉ ์ง์ฐ ๊ฒฝ๋ณด ๋ฐ์ ์ ํ์ฅ ํ์ธ ๋ฐ ๋์ฒด ์์ก ์ง์๋ฅผ ์ํ HITL ์ ์ฐจ ์ค๊ณ ๊ด๋ จ ํต์ฌ ๋ก์ง์ ๊ตฌํํ์ธ์.\n",
" # ํํธ: ์๋จ ์ฃผ์ ๋ฐ ๋ฐ์ดํฐ ์คํค๋ง/์ง์นจ์ ์ฐธ๊ณ ํ์ฌ ์์ฑํฉ๋๋ค.\n",
" # <<< ์ฌ๊ธฐ์ ์ฝ๋๋ฅผ ์์ฑํ์ธ์ >>>\n",
" pass # ์์ฑ ํ pass๋ฅผ ์ ๊ฑฐํ์ธ์.\n",
"def project(x, y):\n",
" \"\"\"GPS ์ขํ๋ฅผ ๋
ธ์ ์ ํฌ์ํด (์งํ๊ฑฐ๋ฆฌ km, ๋
ธ์ ๊ณผ์ ์์ง ์ดํ km)๋ฅผ ๊ตฌํ๋ค.\"\"\"\n",
" x, y = np.atleast_1d(x).astype(float), np.atleast_1d(y).astype(float)\n",
" best_s, best_d = np.zeros(len(x)), np.full(len(x), np.inf)\n",
" for r in ROUTE.itertuples():\n",
" dx, dy = r.x1 - r.x0, r.y1 - r.y0\n",
" f = np.clip(((x - r.x0) * dx + (y - r.y0) * dy) / (dx * dx + dy * dy), 0, 1)\n",
" d = np.hypot(x - (r.x0 + f * dx), y - (r.y0 + f * dy))\n",
" better = d < best_d\n",
" best_s, best_d = np.where(better, r.c0 + f * r.km, best_s), np.where(better, d, best_d)\n",
" return best_s, best_d\n",
"\n",
"\n",
"def cong(clock_min):\n",
" h = (clock_min / 60) % 24\n",
" return 0.82 if (7 <= h < 9 or 17 <= h < 19) else 1.0 # ์ถํด๊ทผ ์๊ฐ๋ ์ ์ฒด ๊ณ์(๊ฐ์ )\n",
"\n",
"\n",
"def simulate_trip(rng, tid, start_min, rain, size, cargo):\n",
" \"\"\"1๋ถ ๋จ์๋ก ์ด๋์ ๋ชจ์ํ๊ณ 5๋ถ๋ง๋ค ํ
๋ ๋ฉํธ๋ฆฌ๋ฅผ ๋จ๊ธด๋ค. ์ดํยท๋ถ์ ์ ์ฐจ ์ด๋ฒคํธ๋ ์ ๋ต ๋ผ๋ฒจ๋ก ํจ๊ป ์ ์ฅํ๋ค.\"\"\"\n",
" s, t, stop_left, rows = 0.0, 0, 0, []\n",
" dev = (rng.random() < 0.12)\n",
" dev_t0 = int(rng.integers(30, 120)) if dev else -1\n",
" dev_len = int(rng.integers(12, 25)) if dev else 0\n",
" cause = \"\"\n",
" while s < LEN:\n",
" i = seg_of(s)\n",
" v = BASE_V[i] * (0.88 if rain else 1.0) * (0.85 if (rain and UNP[i]) else 1.0) * cong(start_min + t) \\\n",
" * (0.7 if THR[i] else 1.0) * (1 - 0.012 * size) * rng.normal(1, 0.06)\n",
" off = 0.0\n",
" if dev and dev_t0 <= t < dev_t0 + dev_len:\n",
" ph = (t - dev_t0) / dev_len\n",
" off, v = 4.0 * (1 - abs(2 * ph - 1)), v * 0.4 # ๋
ธ์ ์ดํ(์ผ๊ฐํ ํํ๋ก ์ต๋ 4km)\n",
" if stop_left > 0:\n",
" v, stop_left = 0.0, stop_left - 1\n",
" elif rng.random() < 0.0015:\n",
" stop_left, cause = int(rng.integers(8, 36)), (\"maint\" if rng.random() < 0.7 else \"urgent\")\n",
" s += v / 60\n",
" t += 1\n",
" if t % 5 == 0 or s >= LEN:\n",
" x, y, hd = route_xy(min(s, LEN))\n",
" nx, ny = np.cos(np.radians(hd)), -np.sin(np.radians(hd))\n",
" j = seg_of(min(s, LEN)); tun = TUN[j]\n",
" rows.append(dict(trip=tid, t=t, clock=(start_min + t) % 1440, x=x + off * nx + rng.normal(0, 0.03), y=y + off * ny + rng.normal(0, 0.03),\n",
" speed=max(v * rng.normal(1, 0.03), 0.0), heading=(hd + rng.normal(0, 3)) % 360, rssi=-62 - 14 * tun + rng.normal(0, 3),\n",
" cargo_temp=18 + 3 * np.sin(t / 40) + rng.normal(0, 0.3), shock_g=abs(rng.normal(0.25, 0.1)) * (2.0 if UNP[j] else 1.0),\n",
" s_true=min(s, LEN), off_true=off, stopped_true=int(v == 0.0), rain=int(rain), size=size, cargo=cargo, stop_cause=cause if v == 0.0 else \"\"))\n",
" arr = t\n",
" for r in rows:\n",
" r[\"eta_true\"] = arr - r[\"t\"]\n",
" return rows\n",
"\n",
"\n",
"def make_telemetry(seed=21, n_trips=200):\n",
" \"\"\"n_trips๊ฐ ์์ก์ ๋จ ์ด๋ ๊ธฐ๋ก(๋ ์ง ์). ์ ๋จ ๊ท๋ชจ 3~12๋, ํ๋ฌผ ๊ตฌ๋ถ ์ผ๋ฐ/๋์ฅ/๋ฏผ๊ฐ.\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" rows = []\n",
" for k in range(n_trips):\n",
" rows += simulate_trip(rng, k, int(rng.integers(5 * 60, 15 * 60)), bool(rng.random() < 0.2), int(rng.integers(3, 13)), str(rng.choice([\"์ผ๋ฐ\", \"๋์ฅ\", \"๋ฏผ๊ฐ\"], p=[.6, .25, .15])))\n",
" return pd.DataFrame(rows)\n",
"\n",
"\n",
"def mape(y, p):\n",
" y, p = np.asarray(y, float), np.asarray(p, float)\n",
" return float(np.mean(np.abs(y - p) / np.abs(y)) * 100)\n",
"\n",
"\n",
"def mae(y, p):\n",
" return float(np.mean(np.abs(np.asarray(y, float) - np.asarray(p, float))))\n",
"# ---------------------------------------------------------------------------\n",
"# ---------------------------------------------------------------------------\n",
"# ETA ์์ธก ํผ์ฒ: ๊ด์ธก ์์ ๊น์ง์ ํ
๋ ๋ฉํธ๋ฆฌ์ ๋จ์ ๋
ธ์ ๊ตฌ์ฑ(ํฌ์ฅยท์ํยท๊ต๋ํฐ๋ ๊ตฌ๊ฐ ๊ฑฐ๋ฆฌ)๋ง ์ฌ์ฉ\n",
"# ---------------------------------------------------------------------------\n",
"FEATS = [\"remaining_km\", \"progress\", \"speed\", \"speed_ma3\", \"speed_std3\", \"stop_n\", \"stop_min\", \"since_stop\", \"hour_sin\", \"hour_cos\", \"cong\", \"rain\", \"size\",\n",
" \"k_๋์ฅ\", \"k_๋ฏผ๊ฐ\", \"rem_unpaved_km\", \"rem_threat_km\", \"rem_bridge_km\", \"off_km\", \"rssi\", \"base_eta\"]\n",
"\n",
"\n",
"def rem_composition(s):\n",
" \"\"\"์งํ๊ฑฐ๋ฆฌ s ์ดํ ๋จ์ ๊ตฌ๊ฐ ์ค ๋นํฌ์ฅยท์ํยท๊ต๋ํฐ๋ ๊ฑฐ๋ฆฌ(km).\"\"\"\n",
" left = np.clip(ROUTE[\"c1\"].values[None, :] - np.maximum(np.atleast_1d(s)[:, None], ROUTE[\"c0\"].values[None, :]), 0, None)\n",
" return left @ np.array(UNP, float), left @ np.array(THR, float), left @ np.array(TUN, float)\n",
"\n",
"\n",
"def add_features(df):\n",
" d = df.sort_values([\"trip\", \"t\"]).copy()\n",
" d[\"s_obs\"], d[\"off_km\"] = project(d[\"x\"].values, d[\"y\"].values)\n",
" d[\"remaining_km\"] = LEN - d[\"s_obs\"]\n",
" d[\"progress\"] = d[\"s_obs\"] / LEN\n",
" g = d.groupby(\"trip\")[\"speed\"]\n",
" d[\"speed_ma3\"] = g.rolling(3, min_periods=1).mean().reset_index(level=0, drop=True)\n",
" d[\"speed_std3\"] = g.rolling(3, min_periods=1).std().reset_index(level=0, drop=True).fillna(0)\n",
" d[\"is_stop\"] = (d[\"speed\"] < 3).astype(int)\n",
" new_stop = (d[\"is_stop\"] == 1) & (d.groupby(\"trip\")[\"is_stop\"].shift(1).fillna(0) == 0)\n",
" d[\"stop_n\"] = new_stop.astype(int).groupby(d[\"trip\"]).cumsum()\n",
" d[\"stop_min\"] = d.groupby(\"trip\")[\"is_stop\"].cumsum() * 5\n",
" blk = (d[\"is_stop\"] == 1).astype(int).groupby(d[\"trip\"]).cumsum() # ์ ์ฐจ ๋์ ํ์๋ก ๊ตฌ๊ฐ์ ๋๋์ด ๋ง์ง๋ง ์ ์ฐจ ์ดํ ๊ฒฝ๊ณผ ๊ณ์ฐ\n",
" d[\"since_stop\"] = d.groupby([\"trip\", blk]).cumcount() * 5\n",
" d[\"hour_sin\"], d[\"hour_cos\"] = np.sin(2 * np.pi * d[\"clock\"] / 1440), np.cos(2 * np.pi * d[\"clock\"] / 1440)\n",
" d[\"cong\"] = d[\"clock\"].map(cong)\n",
" d[\"k_๋์ฅ\"], d[\"k_๋ฏผ๊ฐ\"] = (d[\"cargo\"] == \"๋์ฅ\").astype(int), (d[\"cargo\"] == \"๋ฏผ๊ฐ\").astype(int)\n",
" d[\"rem_unpaved_km\"], d[\"rem_threat_km\"], d[\"rem_bridge_km\"] = rem_composition(d[\"s_obs\"].values)\n",
" d[\"base_eta\"] = np.minimum(d[\"remaining_km\"] / np.maximum(d[\"speed\"], 5.0) * 60, 600) # ํ์ฌ ์๋ ๊ธฐ๋ฐ ๋จ์ ETA(๋ถ, ์ ์ฐจ ์ ํญ์ฃผ ๋ฐฉ์ง๋ก 5km/h ํํยท600๋ถ ์ํ)\n",
" return d.reset_index(drop=True)\n",
"# ---------------------------------------------------------------------------\n",
"\n",
"df = make_telemetry(); d = add_features(df)\n",
"tr = d[d.trip < 160]\n",
"model = HistGradientBoostingRegressor(max_iter=150, max_depth=4, learning_rate=0.08, random_state=0).fit(tr[FEATS], tr[\"eta_true\"])\n",
"te = d[d.trip >= 160].copy()\n",
"te[\"eta_pred\"] = model.predict(te[FEATS]).clip(min=0)\n",
"SIG = float(np.std(tr[\"eta_true\"] - model.predict(tr[FEATS]))) # ์์ธก ์ค์ฐจ ํ์คํธ์ฐจ(๋ถ)\n",
"\n",
"# ---------------- ๊ฐ์ ๊ฐ(๊ต์ก์ฉ): ์ดํ๊ณํ, ์ง์ฐ ์์ค ๋จ๊ฐ, ์กฐ์น๋ณ ๋น์ฉยทํจ๊ณผ ----------------\n",
"PLAN = 255 # ์ดํ๊ณํ ์์์๊ฐ(๋ถ)\n",
"W = {\"์ผ๋ฐ\": 0.5, \"๋์ฅ\": 1.5, \"๋ฏผ๊ฐ\": 3.0} # ์ง์ฐ 1๋ถ๋น ์์ค(๋ง์, ํ๋ฌผ ๊ตฌ๋ถ๋ณ ๊ฐ์ )\n",
"C_CHECK, K_CHECK = 2.0, 0.75 # ํ์ฅ ํ์ธ: ๋น์ฉ 2๋ง์, ์ง์ฐ 25% ๊ฐ์ ๊ฐ์ \n",
"C_REPL, T_REPL = 45.0, 90 # ๋์ฒด ์์ก: ๋น์ฉ 45๋ง์, ์ง์ ํ 90๋ถ ๋ค ๋์ฐฉ\n",
"OBS = [60, 100, 140, 180] # ํ๋จ ์์ (์ถ๋ฐ ํ ๋ถ)\n",
"\n",
"\n",
"def exp_late(mu, sig):\n",
" \"\"\"์ง์ฐ(๋ถ) ๊ธฐ๋๊ฐ E[max(L,0)], L ~ N(mu, sig).\"\"\"\n",
" z = mu / sig\n",
" return sig * norm.pdf(z) + mu * norm.cdf(z)\n",
"\n",
"\n",
"def outcome(r, act):\n",
" \"\"\"์ค์ ๊ฒฐ๊ณผ ๋น์ฉ(๋ง์): ์ค์ ๋์ฐฉ ์๊ฐ์ผ๋ก ํ๊ฐ.\"\"\"\n",
" L = r[\"t\"] + r[\"eta_true\"] - PLAN\n",
" w = W[r[\"cargo\"]]\n",
" if act == \"๋๊ธฐ\":\n",
" return w * max(L, 0)\n",
" if act == \"ํ์ฅ ํ์ธ\":\n",
" return C_CHECK + K_CHECK * w * max(L, 0)\n",
" return C_REPL + w * max(r[\"t\"] + T_REPL - PLAN, 0)\n",
"\n",
"\n",
"def decide(g, policy, reserve):\n",
" \"\"\"ํ ํ๋จ ์์ ์ ์ ๋จ ๋ชฉ๋ก์ ์กฐ์น๋ฅผ ๋ฐฐ์ ํ๋ค. policy: 'rule'(๋จ์ ETA 10๋ถ ์ด์ ์ง์ฐ์ด๋ฉด ๋์ฒด ์ถ๋) / 'opt'(๊ธฐ๋๊ฐ ์ต์).\"\"\"\n",
" plan = {}\n",
" if policy == \"rule\":\n",
" for i, r in g.iterrows():\n",
" plan[i] = (\"๋์ฒด ์์ก\", r[\"t\"] + r[\"base_eta\"] - PLAN) if r[\"t\"] + r[\"base_eta\"] - PLAN >= 10 else (\"๋๊ธฐ\", 0)\n",
" else:\n",
" for i, r in g.iterrows():\n",
" mu, w = r[\"t\"] + r[\"eta_pred\"] - PLAN, W[r[\"cargo\"]]\n",
" c = {\"๋๊ธฐ\": w * exp_late(mu, SIG), \"ํ์ฅ ํ์ธ\": C_CHECK + K_CHECK * w * exp_late(mu, SIG),\n",
" \"๋์ฒด ์์ก\": C_REPL + w * exp_late(r[\"t\"] + T_REPL - PLAN, SIG)}\n",
" best = min(c, key=c.get)\n",
" plan[i] = (best, c[\"๋๊ธฐ\"] - c[best])\n",
" cand = sorted([i for i, (a, _) in plan.items() if a == \"๋์ฒด ์์ก\"], key=lambda i: -plan[i][1])\n",
" for i in cand[reserve:]: # ์๋น ๋์ฒด ์์ก ์ ์ ์ฝ: ์ด๊ณผ๋ถ์ 'ํ์ฅ ํ์ธ'์ผ๋ก ๋ฎ์ถ๋ค\n",
" plan[i] = (\"ํ์ฅ ํ์ธ\" if policy == \"opt\" else \"๋๊ธฐ\", 0)\n",
" return {i: a for i, (a, _) in plan.items()}\n",
"\n",
"\n",
"def simulate(sub, policy, reserve):\n",
" cost, cnt = 0.0, {\"๋๊ธฐ\": 0, \"ํ์ฅ ํ์ธ\": 0, \"๋์ฒด ์์ก\": 0}\n",
" for t0 in OBS:\n",
" g = sub[(sub[\"t\"] == t0) & (sub[\"eta_true\"] > 0)]\n",
" for i, a in decide(g, policy, reserve).items():\n",
" cost += outcome(g.loc[i], a); cnt[a] += 1\n",
" return cost, cnt\n",
"\n",
"\n",
"# ---------------- 1) ์ต์ ํ ์๋ฎฌ๋ ์ดํฐ: ๊ธฐ์กด ๊ดํ vs ๊ธฐ๋๊ฐ ์ต์ ์ถ์ฒ ----------------\n",
"SC = [(\"ํ์(์๋น ๋์ฒด ์์ก 4๋)\", te, 4), (\"์๋น ๋์ฒด ์์ก 2๋\", te, 2), (\"์๋น ๋์ฒด ์์ก 0๋\", te, 0), (\"์ฐ์ฒ ์ด๋๋ง\", te[te.rain == 1], 4), (\"๋ฏผ๊ฐ ํ๋ฌผ๋ง\", te[te.cargo == \"๋ฏผ๊ฐ\"], 4)]\n",
"rows = []\n",
"for name, sub, R in SC:\n",
" cr, nr = simulate(sub, \"rule\", R)\n",
" co, no = simulate(sub, \"opt\", R)\n",
" rows.append((name, sum(nr.values()), cr, co, 1 - co / cr if cr > 0 else np.nan, nr[\"๋์ฒด ์์ก\"], no[\"๋์ฒด ์์ก\"], no[\"ํ์ฅ ํ์ธ\"]))\n",
"res = pd.DataFrame(rows, columns=[\"์๋๋ฆฌ์ค\", \"ํ๋จ ๊ฑด์\", \"๊ดํ ๋น์ฉ(๋ง์)\", \"์ถ์ฒ ๋น์ฉ(๋ง์)\", \"์ ๊ฐ๋ฅ \", \"๊ดํ ๋์ฒด์ถ๋\", \"์ถ์ฒ ๋์ฒด์ถ๋\", \"์ถ์ฒ ํ์ฅํ์ธ\"])\n",
"print(\"[1] ์ง์ฐ ๊ฒฝ๋ณด ์กฐ์น ๋น์ฉ ๋น๊ต(๋จ๊ฐยทํจ๊ณผ๋ ๊ต์ก์ฉ ๊ฐ์ ๊ฐ, ํ๋จ ์์ 4ํ x ์ ๋จ)\")\n",
"print(res.assign(์ ๊ฐ๋ฅ =res[\"์ ๊ฐ๋ฅ \"].map(\"{:.1%}\".format)).round(1).to_string(index=False))\n",
"base = res.iloc[0]\n",
"print(f\" ํ์ ์๋๋ฆฌ์ค ์ ๊ฐ {base['์ ๊ฐ๋ฅ ']:.1%} (๋ชฉํ 15% ์ด์: {'๋ฌ์ฑ' if base['์ ๊ฐ๋ฅ '] >= .15 else '๋ฏธ๋ฌ'}), ๊ดํ์ ๋์ฒด ์ถ๋ {int(base['๊ดํ ๋์ฒด์ถ๋'])}ํ๋ก ๋ถํ์ํ ์ถ๋์ด ๋ง๋ค\")\n",
"print(\" '๊ดํ'์ ํ์ฌ ์๋ ๋จ์ ETA๋ก 10๋ถ ์ด์ ๋ฆ์ผ๋ฉด ๋ฌด์กฐ๊ฑด ๋์ฒด ์ถ๋ํ๋ ๊ฐ์ ๊ท์น์ด๋ฉฐ ์ค์ ๋ถ๋ ๋ฐฉ์นจ์ด ์๋๋ค.\")\n",
"\n",
"# ---------------- 2) ๋จ๊ฐ ๋ฏผ๊ฐ๋: ๋์ฒด ์์ก ๋น์ฉ ----------------\n",
"sens = []\n",
"for c in (25.0, 45.0, 70.0, 100.0):\n",
" C_REPL = c\n",
" cr, _ = simulate(te, \"rule\", 4); co, no = simulate(te, \"opt\", 4)\n",
" sens.append((c, 1 - co / cr, no[\"๋์ฒด ์์ก\"]))\n",
"C_REPL = 45.0\n",
"print(\"\\n[2] ๋์ฒด ์์ก ๋น์ฉ ๋ฏผ๊ฐ๋:\", \"; \".join(f\"{c:.0f}๋ง์ -> ์ ๊ฐ {s:.0%}, ์ถ์ฒ ์ถ๋ {n}ํ\" for c, s, n in sens))\n",
"print(\" ๋น์ฉ์ด ๋์์ง์๋ก ์ถ์ฒ์ ํ์ฅ ํ์ธ ์ค์ฌ์ผ๋ก ์ด๋ํ๊ณ , ๊ดํ ๋๋น ์ ๊ฐํญ์ ๋ฒ์ด์ง๋ค.\")\n",
"\n",
"# ---------------- 3) HITL ๊ฒฐ์ฌ ์ํฌํ๋ก์ฐ ----------------\n",
"USERS = {\"ctl_a\": \"CTL\", \"ctl_b\": \"CTL\", \"tco_kim\": \"TCO\", \"tco_lee\": \"TCO\", \"log_park\": \"LOG\", \"aud_han\": \"AUD\"}\n",
"PERM = {\"CTL\": {\"submit\", \"redraft\"}, \"TCO\": {\"approve\", \"reject\"}, \"LOG\": {\"approve\", \"reject\"}, \"AUD\": {\"read\"}}\n",
"\n",
"\n",
"class Flow:\n",
" def __init__(self):\n",
" self.docs, self.log, self.blocked = {}, [], []\n",
"\n",
" def _audit(self, user, action, doc, detail):\n",
" prev = self.log[-1][\"hash\"] if self.log else \"GENESIS\"\n",
" rec = {\"seq\": len(self.log) + 1, \"user\": user, \"action\": action, \"doc\": doc, \"detail\": detail, \"prev\": prev}\n",
" rec[\"hash\"] = hashlib.sha256(json.dumps(rec, ensure_ascii=False, sort_keys=True).encode()).hexdigest()[:12]\n",
" self.log.append(rec)\n",
"\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 != \"hash\"}\n",
" if r[\"prev\"] != prev or hashlib.sha256(json.dumps(body, ensure_ascii=False, sort_keys=True).encode()).hexdigest()[:12] != r[\"hash\"]:\n",
" return False, r[\"seq\"]\n",
" prev = r[\"hash\"]\n",
" return True, None\n",
"\n",
" def deny(self, user, act, doc, why):\n",
" self.blocked.append((user, act, doc, why)); self._audit(user, \"DENIED:\" + act, doc, why); return False\n",
"\n",
" def submit(self, user, trip, action, detail, parent=None):\n",
" need = \"redraft\" if parent else \"submit\"\n",
" if need not in PERM.get(USERS.get(user), set()):\n",
" return self.deny(user, need, \"-\", \"์์ ๊ถํ ์์\")\n",
" did = f\"ALT{len(self.docs) + 1:02d}\"\n",
" self.docs[did] = {\"trip\": trip, \"action\": action, \"line\": [\"TCO\", \"LOG\"] if action == \"๋์ฒด ์์ก\" else [\"TCO\"], \"step\": 0, \"status\": \"๊ฒํ ๋๊ธฐ\",\n",
" \"drafter\": user, \"done\": [], \"parent\": parent}\n",
" self._audit(user, \"submit\", did, f\"trip {trip} {action} {detail}\")\n",
" return did\n",
"\n",
" def decide(self, user, did, act, reason=\"\"):\n",
" role, doc = USERS.get(user), self.docs.get(did)\n",
" if role is None: return self.deny(user, act, did, \"๋ฏธ๋ฑ๋ก ์ฌ์ฉ์\")\n",
" if doc is None or doc[\"status\"] != \"๊ฒํ ๋๊ธฐ\": return self.deny(user, act, did, \"๊ฒฐ์ฌ ๋์ ์๋(์ฒ๋ฆฌ ์๋ฃ)\")\n",
" if act not in PERM[role]: return self.deny(user, act, did, f\"{role} ์ญํ ์ {act} ๋ถ๊ฐ\")\n",
" if doc[\"line\"][doc[\"step\"]] != role: return self.deny(user, act, did, \"๊ฒฐ์ฌ ์์ ์๋ฐ\")\n",
" if user == doc[\"drafter\"] or user in doc[\"done\"]: return self.deny(user, act, did, \"๊ธฐ์์ยท๊ธฐ๊ฒฐ์ฌ์ ์ฌ๊ฒฐ์ฌ ๊ธ์ง\")\n",
" if len(reason.strip()) < 10: return self.deny(user, act, did, \"์ฌ์ 10์ ์ด์ ํ์\")\n",
" if act == \"reject\":\n",
" doc[\"status\"] = \"๋ฐ๋ ค\"\n",
" else:\n",
" doc[\"done\"].append(user); doc[\"step\"] += 1\n",
" doc[\"status\"] = \"์น์ธ\" if doc[\"step\"] == len(doc[\"line\"]) else \"๊ฒํ ๋๊ธฐ\"\n",
" self._audit(user, act, did, reason); return True\n",
"\n",
" def edit(self, user, did):\n",
" if self.docs[did][\"status\"] == \"์น์ธ\": return self.deny(user, \"edit\", did, \"์น์ธ ํ ์ง์ ๋ณ๊ฒฝ ๊ธ์ง(์ฌ๊ธฐ์)\")\n",
" return True\n",
"\n",
"\n",
"g = te[(te[\"t\"] == 140) & (te[\"eta_true\"] > 0)]\n",
"act = decide(g, \"opt\", 3)\n",
"prop = [(i, a) for i, a in act.items() if a != \"๋๊ธฐ\"]\n",
"prop = sorted(prop, key=lambda x: (x[1] != \"๋์ฒด ์์ก\", x[0]))[:6]\n",
"wf = Flow()\n",
"ids = [wf.submit(\"ctl_a\" if k % 2 == 0 else \"ctl_b\", int(g.loc[i, \"trip\"]), a, f\"์์ ์ง์ฐ {g.loc[i, 't'] + g.loc[i, 'eta_pred'] - PLAN:+.0f}๋ถ\") for k, (i, a) in enumerate(prop)]\n",
"two = [i for i in ids if len(wf.docs[i][\"line\"]) == 2]\n",
"print(f\"\\n[3] 140๋ถ ์์ AI ์ง์์ {len(ids)}๊ฑด ์ ๋ถ '๊ฒํ ๋๊ธฐ' ๋ฑ๋ก, ์๋ ์น์ธ {sum(wf.docs[i]['status'] == '์น์ธ' for i in ids)}๊ฑด (๋ชฉํ 0๊ฑด). 2๋จ๊ณ ๊ฒฐ์ฌ(๋์ฒด ์์ก) ๋์ {two}\")\n",
"first = two[0] if two else ids[0]\n",
"wf.decide(\"tco_kim\", first, \"reject\", \"์๋น ์ฐจ๋ ์์น ํ์ธ ํ ์ฌ๊ธฐ์ ์์ฒญ, ๊ทผ๊ฑฐ ๋ณด๊ฐ ํ์\")\n",
"new = wf.submit(\"ctl_a\", wf.docs[first][\"trip\"], wf.docs[first][\"action\"], \"์ฌ๊ธฐ์(์๋น ์ฐจ๋ ์ ๋ณด ๋ณด๊ฐ)\", parent=first)\n",
"wf.decide(\"tco_kim\", new, \"approve\", \"์๋น ์ฐจ๋ ๊ฐ์ฉ ํ์ธ, ๊ตฐ์์ฐธ๋ชจ ์์ ํจ\")\n",
"if wf.docs[new][\"line\"] == [\"TCO\", \"LOG\"]:\n",
" wf.decide(\"log_park\", new, \"approve\", \"๋์ฒด ์์ก ์ํ, ๋์ฐฉ ๋ณด๊ณ ์กฐ๊ฑด\")\n",
"for i in ids[1:3]:\n",
" wf.decide(\"tco_lee\", i, \"approve\", \"ํ์ฅ ํ์ธ ์ง์ ํ๋น, ๊ฒฐ๊ณผ ๋ณด๊ณ ์กฐ๊ฑด\")\n",
"tgt = ids[-1]\n",
"attacks = [(\"ctl_a\", tgt, \"approve\", \"๋ด๊ฐ ์ฌ๋ฆฐ ๊ฑด์ ์ง์ ์น์ธํ๋ค\"), (\"aud_han\", tgt, \"approve\", \"๊ฐ์ฌ ๊ณ์ ์ผ๋ก ์น์ธ ์๋ํจ\"), (\"ghost\", tgt, \"approve\", \"์ธ๋ถ ๊ณ์ ์ผ๋ก ์น์ธ ์๋ํจ\"),\n",
" (\"tco_kim\", tgt, \"approve\", \"\"), (\"tco_kim\", tgt, \"reject\", \"์งง์\"), (\"log_park\", tgt, \"approve\", \"์๊ธ์๊ฐ ์์๋ฅผ ๊ฑด๋๋ฐ์ด ์น์ธ\"),\n",
" (\"tco_lee\", ids[1], \"approve\", \"์ด๋ฏธ ์น์ธ๋ ๊ฑด์ ๋ค์ ์น์ธํจ\"), (\"log_park\", new, \"reject\", \"์น์ธ๋ ๊ฑด์ ๋ค์ง์ด ๋ฐ๋ คํจ\"), (\"ctl_b\", tgt, \"approve\", \"์์ ์ ๊ณ์ ์ผ๋ก ์น์ธ ์๋ํจ\")]\n",
"for u, doc, a, rs in attacks:\n",
" wf.decide(u, doc, a, rs)\n",
"wf.edit(\"ctl_a\", new)\n",
"print(f\" ๊ถํ ์ฐํ ์๋ 10๊ฑด ์ค ์ฐจ๋จ {len(wf.blocked)}๊ฑด (์ฐจ๋จ์จ {len(wf.blocked) / 10:.0%}, ๋ชฉํ 100%)\")\n",
"for u, a, doc, why in wf.blocked[:4]:\n",
" print(f\" ์ฐจ๋จ {u:<8} {a:<8} {doc}: {why}\")\n",
"ok, _ = wf.verify()\n",
"snap = json.loads(json.dumps(wf.log))\n",
"wf.log[3][\"detail\"] = \"์ฌํ์ ๊ณ ์ณ ์ด ์ฌ์ \"\n",
"ok2, at = wf.verify()\n",
"wf.log = snap\n",
"print(f\" ๊ฐ์ฌ ๋ก๊ทธ {len(wf.log)}๊ฑด ํด์ ์ฒด์ธ ๋ฌด๊ฒฐ์ฑ {'100%' if ok else 'ํผ์'}; 4๋ฒ ๊ธฐ๋ก ์ฌํ ์์ ์ {'ํต๊ณผ' if ok2 else f'์๋ณ์กฐ ๊ฒ์ถ({at}๋ฒ)'}\")\n",
"print(f\" {first}(๋ฐ๋ ค) -> {new}(์ฌ๊ธฐ์, parent ์ฐ๊ฒฐ) ์ด๋ ฅ:\", [(r['action'], r['user']) for r in wf.log if r['doc'] in (first, new)])\n",
"print(\" ๋ฌธ์ ์ํ:\", pd.Series({k: v[\"status\"] for k, v in wf.docs.items()}).value_counts().to_dict(), \"| ์ค์ ๋์ฒด ์์ก ์ง์๋ ์์กํต์ ๊ดยท๊ตฐ์์ฐธ๋ชจ ๊ฒฐ์ฌ๊ณผ ์งํ๊ณํต์ ๋ฐ๋ฅธ๋ค.\")\n",
"\n",
"fig, ax = plt.subplots(1, 2, figsize=(12, 4))\n",
"x = np.arange(len(res)); ax[0].bar(x - .2, res[\"๊ดํ ๋น์ฉ(๋ง์)\"], .4, label=\"rule (always replace)\"); ax[0].bar(x + .2, res[\"์ถ์ฒ ๋น์ฉ(๋ง์)\"], .4, label=\"expected-cost optimum\")\n",
"ax[0].set_xticks(x); ax[0].set_xticklabels([f\"S{i + 1}\" for i in x]); ax[0].legend(); ax[0].set_title(\"Total cost by scenario (10k KRW)\")\n",
"ax[1].plot([s[0] for s in sens], [s[1] * 100 for s in sens], \"o-\"); ax[1].set_title(\"Saving (%) vs replacement cost\"); ax[1].set_xlabel(\"replacement cost (10k KRW)\")\n",
"plt.tight_layout(); plt.show()\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"๐ก [์ฐธ๊ณ ] ๋ฏธ์
07 ์์ ์คํ ๊ฒฐ๊ณผ (Output)
\n",
"\n",
"```text\n",
"[1] ์ง์ฐ ๊ฒฝ๋ณด ์กฐ์น ๋น์ฉ ๋น๊ต(๋จ๊ฐยทํจ๊ณผ๋ ๊ต์ก์ฉ ๊ฐ์ ๊ฐ, ํ๋จ ์์ 4ํ x ์ ๋จ)\n",
" ์๋๋ฆฌ์ค ํ๋จ ๊ฑด์ ๊ดํ ๋น์ฉ(๋ง์) ์ถ์ฒ ๋น์ฉ(๋ง์) ์ ๊ฐ๋ฅ ๊ดํ ๋์ฒด์ถ๋ ์ถ์ฒ ๋์ฒด์ถ๋ ์ถ์ฒ ํ์ฅํ์ธ\n",
"ํ์(์๋น ๋์ฒด ์์ก 4๋) 160 1946.5 1190.8 38.8% 16 10 32\n",
" ์๋น ๋์ฒด ์์ก 2๋ 160 1992.5 1240.6 37.7% 8 7 35\n",
" ์๋น ๋์ฒด ์์ก 0๋ 160 1734.0 1421.1 18.0% 0 0 42\n",
" ์ฐ์ฒ ์ด๋๋ง 32 1363.0 956.0 29.9% 16 10 19\n",
" ๋ฏผ๊ฐ ํ๋ฌผ๋ง 12 117.0 35.5 69.7% 2 0 2\n",
" ํ์ ์๋๋ฆฌ์ค ์ ๊ฐ 38.8% (๋ชฉํ 15% ์ด์: ๋ฌ์ฑ), ๊ดํ์ ๋์ฒด ์ถ๋ 16ํ๋ก ๋ถํ์ํ ์ถ๋์ด ๋ง๋ค\n",
" '๊ดํ'์ ํ์ฌ ์๋ ๋จ์ ETA๋ก 10๋ถ ์ด์ ๋ฆ์ผ๋ฉด ๋ฌด์กฐ๊ฑด ๋์ฒด ์ถ๋ํ๋ ๊ฐ์ ๊ท์น์ด๋ฉฐ ์ค์ ๋ถ๋ ๋ฐฉ์นจ์ด ์๋๋ค.\n",
"\n",
"[2] ๋์ฒด ์์ก ๋น์ฉ ๋ฏผ๊ฐ๋: 25๋ง์ -> ์ ๊ฐ 40%, ์ถ์ฒ ์ถ๋ 14ํ; 45๋ง์ -> ์ ๊ฐ 39%, ์ถ์ฒ ์ถ๋ 10ํ; 70๋ง์ -> ์ ๊ฐ 41%, ์ถ์ฒ ์ถ๋ 3ํ; 100๋ง์ -> ์ ๊ฐ 50%, ์ถ์ฒ ์ถ๋ 0ํ\n",
" ๋น์ฉ์ด ๋์์ง์๋ก ์ถ์ฒ์ ํ์ฅ ํ์ธ ์ค์ฌ์ผ๋ก ์ด๋ํ๊ณ , ๊ดํ ๋๋น ์ ๊ฐํญ์ ๋ฒ์ด์ง๋ค.\n",
"\n",
"[3] 140๋ถ ์์ AI ์ง์์ 6๊ฑด ์ ๋ถ '๊ฒํ ๋๊ธฐ' ๋ฑ๋ก, ์๋ ์น์ธ 0๊ฑด (๋ชฉํ 0๊ฑด). 2๋จ๊ณ ๊ฒฐ์ฌ(๋์ฒด ์์ก) ๋์ ['ALT01', 'ALT02', 'ALT03']\n",
" ๊ถํ ์ฐํ ์๋ 10๊ฑด ์ค ์ฐจ๋จ 10๊ฑด (์ฐจ๋จ์จ 100%, ๋ชฉํ 100%)\n",
" ์ฐจ๋จ ctl_a approve ALT06: CTL ์ญํ ์ approve ๋ถ๊ฐ\n",
" ์ฐจ๋จ aud_han approve ALT06: AUD ์ญํ ์ approve ๋ถ๊ฐ\n",
" ์ฐจ๋จ ghost approve ALT06: ๋ฏธ๋ฑ๋ก ์ฌ์ฉ์\n",
" ์ฐจ๋จ tco_kim approve ALT06: ์ฌ์ 10์ ์ด์ ํ์\n",
" ๊ฐ์ฌ ๋ก๊ทธ 22๊ฑด ํด์ ์ฒด์ธ ๋ฌด๊ฒฐ์ฑ 100%; 4๋ฒ ๊ธฐ๋ก ์ฌํ ์์ ์ ์๋ณ์กฐ ๊ฒ์ถ(4๋ฒ)\n",
" ALT01(๋ฐ๋ ค) -> ALT07(์ฌ๊ธฐ์, parent ์ฐ๊ฒฐ) ์ด๋ ฅ: [('submit', 'ctl_a'), ('reject', 'tco_kim'), ('submit', 'ctl_a'), ('approve', 'tco_kim'), ('approve', 'log_park'), ('DENIED:reject', 'log_park'), ('DENIED:edit', 'ctl_a')]\n",
" ๋ฌธ์ ์ํ: {'๊ฒํ ๋๊ธฐ': 5, '๋ฐ๋ ค': 1, '์น์ธ': 1} | ์ค์ ๋์ฒด ์์ก ์ง์๋ ์์กํต์ ๊ดยท๊ตฐ์์ฐธ๋ชจ ๊ฒฐ์ฌ๊ณผ ์งํ๊ณํต์ ๋ฐ๋ฅธ๋ค.\n",
"```\n",
" \n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## ๐ฏ ๋ฏธ์
08. 100๊ฐ ์์ก์ ๋จ ๋์ ์ด๋ ๋ชจ์ ์ค์๊ฐ ์์น ๋ฐ์ดํฐ ์คํธ๋ฆผ ๋ถํ ํ
์คํธ\n",
"**์์์๊ฐ**: ์ฝ 3.0์๊ฐ\n",
"\n",
"### ๐ ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ์ํ ์ง์นจ\n",
"#### ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ๋น์์ฑ\n",
"\n",
"100๊ฐ ์์ก์ ๋จ ๋์ ์ด๋ ๋ชจ์ ์์น ๋ฐ์ดํฐ ์คํธ๋ฆผ ๋ถํ ํ
์คํธ๋ฅผ ์ํํ๋ค. ์ค์ ์ ์ ํ์ฅ์์๋ ๋จ์ ํ์ง์ ๊ทธ์น์ง ์๊ณ ์ ์ํ ์ํ ํ๋จ๊ณผ ์ ๋ขฐ๋ ๋์ ์์ฌ๊ฒฐ์ ์ด ํ์์ ์ด๋ฏ๋ก, **100๊ฐ ์์ก์ ๋จ์ด ๋์์ ์ด๋ํ๋ ๋ชจ์ ์์น ๋ฐ์ดํฐ ์คํธ๋ฆผ์ ๋ถํ ์ํ์ ์ํํด ์ฒ๋ฆฌ ์ฑ๋ฅ๊ณผ ๊ฒฝ๋ณด ์ ํ๋๋ฅผ ์ธก์ ํ๋ค.** ๊ณผ์
์ ์์ํ์ฌ ์คํ์ ๋ฐฉ์งํ๊ณ ์์ ๋์๋ ฅ์ ๊ทน๋ํํด์ผ ํฉ๋๋ค.\n",
"\n",
"์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ณธ ๋ฏธ์
์์๋ **100๊ฐ ์ ๋จ์ 5๋ถ ๊ฐ๊ฒฉ ํ
๋ ๋ฉํธ๋ฆฌ๋ฅผ ํฑ ๋จ์๋ก ์ผ๊ด ์ฒ๋ฆฌํ๋ ์คํธ๋ฆผ ์์ง์ ๋ง๋ค๊ณ ์ค๋ณต ์ ๊ฑฐ, ์์ค, ์ง์ฐ ๋์ฐฉ, ์ดํยท์ ์ฐจ ๊ฒฝ๋ณด์ ETA ๋ฐฐ์น ์์ธก์ ํฌํจ ์ํํ๊ณ ๊ฒ์ฆํ๋** ์ค์ต์ ์ฒด๊ณ์ ์ผ๋ก ์ํํฉ๋๋ค.\n",
"\n",
"ํ์ต์๋ ์ค์ต๊ฐ์ด๋์ ์ ์๋ ์ด 4๊ฐ ์ธ๋ถ ํ๋(๊ธฐ๋ณธ ๊ธฐ๋ฅ ๊ตฌํ โ ๊ฒฐ๊ณผ ์งํ ๊ฒ์ฆ โ ํ๋ผ๋ฏธํฐ ํ๋ ๋ฐ ๋์ ๊ณผ์ )์ ์์ฐจ์ ์ผ๋ก ์งํํ๋ฉฐ ํต์ฌ ํ์ ๊ธฐ์ค์ ๋ฌ์ฑํฉ๋๋ค.\n",
"\n",
"**์ด๋ฒ ๊ฒฐ๊ณผ๋ฌผ:** `src/dashboard/mov06_app.py (Streamlit/Dash ๋ฑ)` ํํ์ ๊ฒฐ๊ณผ๋ฌผ์ ์์ฑํ๋ค.\n",
"\n",
"### ๐ ํ๊ฐ ๋ฐ ๊ฒ์ฆ ๊ธฐ์ค (PASS ์กฐ๊ฑด)\n",
"#### ๋ฏธ์
ํ์ \n",
"\n",
"**ํ๊ฐ ๊ตฌ๋ถ:** ํ์\n",
"\n",
"**ํต์ฌ ํ๊ฐ์์ญ:** ์์ฌ๊ฒฐ์ ์ง์์ ์ต์ ํ๋ ์๊ฐ์ ์๊ณ ๋ฐ ์ฌ์ฉ์ ๊ฒฝํ(UX)\n",
"\n",
"#### ๋ฏธ์
PASS ์กฐ๊ฑด\n",
"\n",
"- ์ ๋ ๊ธฐ์ค: ๋์๋ณด๋ ๋ก๋ฉ ์๊ฐ 2์ด ์ด๋ด, ๊ฒฝ๋ณด ๋ฐ๋ น ์ ํ๋ 95% ์ด์\n",
"\n",
"\n",
"- ์งํ ๊ธฐ์ค: ์น ๊ธฐ๋ฐ ๋ถ์ ๋์๋ณด๋ ๊ตฌ๋ ๋ฐ ์ค์๊ฐ ํํฐ๋ง ๊ธฐ๋ฅ ๊ฒ์ฆ\n",
"\n",
"\n",
"- ๊ฒ์ฆ ๋ฐฉ๋ฒ: ์ค์ ๊ตฐ์ ๋ฐ์ดํฐ ์ฐ๋ ํ๋ฉด ํ
์คํธ ๋ฐ UI ๋์ ๊ฒ์ฆ\n",
"\n",
"\n",
"#### ์ฑ์ ์ ํ์ ๋ฉ๋ชจ\n",
"\n",
"> ๋จ์ ์ฝ๋ ์คํ ์ฌ๋ถ๋ฟ๋ง ์๋๋ผ ์ถ๋ ฅ๋ ์์น์ ๊ทผ๊ฑฐ ์๋ฃ๊ฐ ๊ตฐ ์ค๋ฌด ์๊ตฌ์ฌํญ์ ๋ถํฉํ๋์ง ๋ฉด๋ฐํ ํ์ธํ๋ค.\n",
"\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"\"\"\"100๊ฐ ์์ก์ ๋จ์ ์์น ํ
๋ ๋ฉํธ๋ฆฌ ์คํธ๋ฆผ์ ํฑ(5๋ถ) ๋จ์๋ก ๋ชจ์ ์ฒ๋ฆฌํ๋ ์์ง(์ค๋ณต ์ ๊ฑฐยท์์คยท์ง์ฐยทGPS ์ก์ ๋์, ์ดํยท๋ถ์ ์ ์ฐจ ๊ฒฝ๋ณด, ๋ฐฐ์น ETA ์์ธก)์ ๋์๋ณด๋ ๋ก๋ฉ ์๊ฐ๊ณผ ๊ฒฝ๋ณด ์ ํ๋๋ฅผ ๋ถํยท์ค๋ฅ ์ฃผ์
์ํ์ผ๋ก ์ธก์ ํ๊ณ ์ฒ๋ฆฌ ํ๊ณ ๊ท๋ชจ๋ฅผ ๋์ถํ๋ค.\"\"\"\n",
"import time\n",
"import numpy as np\n",
"import pandas as pd\n",
"import matplotlib.pyplot as plt\n",
"from sklearn.ensemble import HistGradientBoostingRegressor\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# [๊ต์ก์ฉ๊ฐ์์๋ฃ] ๊ฐ์ ์์ก ๋
ธ์ ยท์์ก์ ๋จ ํ
๋ ๋ฉํธ๋ฆฌ ์์ฑ๊ธฐ (์ค์ ๋๋กยท์ง๋ช
ยท๋ถ๋ ์์)\n",
"# ๊ฐ์ ๋
ธ์ (์ถ๋ฐ ๊ฑฐ์ -> ๋์ฐฉ ๊ฑฐ์ , 5๊ฐ ๊ตฌ๊ฐ)์ 5๋ถ ๊ฐ๊ฒฉ์ผ๋ก ๋ณด๊ณ ํ๋ ์ ๋จ GPSยทํ๋ฌผ ์ผ์ยทํต์ ์ํ ์๋ฃ๋ฅผ ๋ง๋ ๋ค.\n",
"# ---------------------------------------------------------------------------\n",
"PTS = [(0, 0), (30, 12), (62, 14), (88, 38), (112, 42), (135, 66)] # ๋
ธ์ ๊ผญ์ง์ (km, ๊ฐ์ ์ขํ)\n",
"ROUTE = pd.DataFrame({\"seg\": [\"R1\", \"R2\", \"R3\", \"R4\", \"R5\"], \"road\": [\"paved\", \"unpaved\", \"paved\", \"paved\", \"unpaved\"],\n",
" \"limit\": [70, 50, 70, 50, 50], \"kind\": [\"์ผ๋ฐ\", \"์ผ๋ฐ\", \"์ผ๋ฐ\", \"๊ต๋ยทํฐ๋\", \"์ผ๋ฐ\"], \"threat\": [0, 0, 1, 0, 0]})\n",
"ROUTE[\"x0\"], ROUTE[\"y0\"] = [p[0] for p in PTS[:-1]], [p[1] for p in PTS[:-1]]\n",
"ROUTE[\"x1\"], ROUTE[\"y1\"] = [p[0] for p in PTS[1:]], [p[1] for p in PTS[1:]]\n",
"ROUTE[\"km\"] = np.hypot(ROUTE[\"x1\"] - ROUTE[\"x0\"], ROUTE[\"y1\"] - ROUTE[\"y0\"])\n",
"ROUTE[\"c0\"] = np.r_[0, np.cumsum(ROUTE[\"km\"])[:-1]]\n",
"ROUTE[\"c1\"] = np.cumsum(ROUTE[\"km\"])\n",
"LEN = float(ROUTE[\"km\"].sum())\n",
"ROAD_F = {\"paved\": 1.0, \"unpaved\": 0.75}\n",
"C1L, C0L, KML = ROUTE[\"c1\"].tolist(), ROUTE[\"c0\"].tolist(), ROUTE[\"km\"].tolist() # ๋ฐ๋ณต ๊ณ์ฐ์ฉ ๋ฆฌ์คํธ(์๋)\n",
"BASE_V = [l * ROAD_F[r] for l, r in zip(ROUTE[\"limit\"], ROUTE[\"road\"])]\n",
"THR, UNP, TUN = ROUTE[\"threat\"].tolist(), (ROUTE[\"road\"] == \"unpaved\").tolist(), (ROUTE[\"kind\"] == \"๊ต๋ยทํฐ๋\").tolist()\n",
"\n",
"\n",
"def seg_of(s):\n",
" # [TODO : ํ์ ์ค์ต 8] 100๊ฐ ์์ก์ ๋จ ๋์ ์ด๋ ๋ชจ์ ์ค์๊ฐ ์์น ๋ฐ์ดํฐ ์คํธ๋ฆผ ๋ถํ ํ
์คํธ ๊ด๋ จ ํต์ฌ ๋ก์ง์ ๊ตฌํํ์ธ์.\n",
" # ํํธ: ์๋จ ์ฃผ์ ๋ฐ ๋ฐ์ดํฐ ์คํค๋ง/์ง์นจ์ ์ฐธ๊ณ ํ์ฌ ์์ฑํฉ๋๋ค.\n",
" # <<< ์ฌ๊ธฐ์ ์ฝ๋๋ฅผ ์์ฑํ์ธ์ >>>\n",
" pass # ์์ฑ ํ pass๋ฅผ ์ ๊ฑฐํ์ธ์.\n",
"def route_xy(s):\n",
" \"\"\"๋
ธ์ ์ ์งํ๊ฑฐ๋ฆฌ s(km)์ ์ขํ์ ์งํ ๋ฐฉํฅ(๋).\"\"\"\n",
" # [TODO : ํ์ ์ค์ต 8] 100๊ฐ ์์ก์ ๋จ ๋์ ์ด๋ ๋ชจ์ ์ค์๊ฐ ์์น ๋ฐ์ดํฐ ์คํธ๋ฆผ ๋ถํ ํ
์คํธ ๊ด๋ จ ํต์ฌ ๋ก์ง์ ๊ตฌํํ์ธ์.\n",
" # ํํธ: ์๋จ ์ฃผ์ ๋ฐ ๋ฐ์ดํฐ ์คํค๋ง/์ง์นจ์ ์ฐธ๊ณ ํ์ฌ ์์ฑํฉ๋๋ค.\n",
" # <<< ์ฌ๊ธฐ์ ์ฝ๋๋ฅผ ์์ฑํ์ธ์ >>>\n",
" pass # ์์ฑ ํ pass๋ฅผ ์ ๊ฑฐํ์ธ์.\n",
"def project(x, y):\n",
" \"\"\"GPS ์ขํ๋ฅผ ๋
ธ์ ์ ํฌ์ํด (์งํ๊ฑฐ๋ฆฌ km, ๋
ธ์ ๊ณผ์ ์์ง ์ดํ km)๋ฅผ ๊ตฌํ๋ค.\"\"\"\n",
" x, y = np.atleast_1d(x).astype(float), np.atleast_1d(y).astype(float)\n",
" best_s, best_d = np.zeros(len(x)), np.full(len(x), np.inf)\n",
" for r in ROUTE.itertuples():\n",
" dx, dy = r.x1 - r.x0, r.y1 - r.y0\n",
" f = np.clip(((x - r.x0) * dx + (y - r.y0) * dy) / (dx * dx + dy * dy), 0, 1)\n",
" d = np.hypot(x - (r.x0 + f * dx), y - (r.y0 + f * dy))\n",
" better = d < best_d\n",
" best_s, best_d = np.where(better, r.c0 + f * r.km, best_s), np.where(better, d, best_d)\n",
" return best_s, best_d\n",
"\n",
"\n",
"def cong(clock_min):\n",
" h = (clock_min / 60) % 24\n",
" return 0.82 if (7 <= h < 9 or 17 <= h < 19) else 1.0 # ์ถํด๊ทผ ์๊ฐ๋ ์ ์ฒด ๊ณ์(๊ฐ์ )\n",
"\n",
"\n",
"def simulate_trip(rng, tid, start_min, rain, size, cargo):\n",
" \"\"\"1๋ถ ๋จ์๋ก ์ด๋์ ๋ชจ์ํ๊ณ 5๋ถ๋ง๋ค ํ
๋ ๋ฉํธ๋ฆฌ๋ฅผ ๋จ๊ธด๋ค. ์ดํยท๋ถ์ ์ ์ฐจ ์ด๋ฒคํธ๋ ์ ๋ต ๋ผ๋ฒจ๋ก ํจ๊ป ์ ์ฅํ๋ค.\"\"\"\n",
" s, t, stop_left, rows = 0.0, 0, 0, []\n",
" dev = (rng.random() < 0.12)\n",
" dev_t0 = int(rng.integers(30, 120)) if dev else -1\n",
" dev_len = int(rng.integers(12, 25)) if dev else 0\n",
" cause = \"\"\n",
" while s < LEN:\n",
" i = seg_of(s)\n",
" v = BASE_V[i] * (0.88 if rain else 1.0) * (0.85 if (rain and UNP[i]) else 1.0) * cong(start_min + t) \\\n",
" * (0.7 if THR[i] else 1.0) * (1 - 0.012 * size) * rng.normal(1, 0.06)\n",
" off = 0.0\n",
" if dev and dev_t0 <= t < dev_t0 + dev_len:\n",
" ph = (t - dev_t0) / dev_len\n",
" off, v = 4.0 * (1 - abs(2 * ph - 1)), v * 0.4 # ๋
ธ์ ์ดํ(์ผ๊ฐํ ํํ๋ก ์ต๋ 4km)\n",
" if stop_left > 0:\n",
" v, stop_left = 0.0, stop_left - 1\n",
" elif rng.random() < 0.0015:\n",
" stop_left, cause = int(rng.integers(8, 36)), (\"maint\" if rng.random() < 0.7 else \"urgent\")\n",
" s += v / 60\n",
" t += 1\n",
" if t % 5 == 0 or s >= LEN:\n",
" x, y, hd = route_xy(min(s, LEN))\n",
" nx, ny = np.cos(np.radians(hd)), -np.sin(np.radians(hd))\n",
" j = seg_of(min(s, LEN)); tun = TUN[j]\n",
" rows.append(dict(trip=tid, t=t, clock=(start_min + t) % 1440, x=x + off * nx + rng.normal(0, 0.03), y=y + off * ny + rng.normal(0, 0.03),\n",
" speed=max(v * rng.normal(1, 0.03), 0.0), heading=(hd + rng.normal(0, 3)) % 360, rssi=-62 - 14 * tun + rng.normal(0, 3),\n",
" cargo_temp=18 + 3 * np.sin(t / 40) + rng.normal(0, 0.3), shock_g=abs(rng.normal(0.25, 0.1)) * (2.0 if UNP[j] else 1.0),\n",
" s_true=min(s, LEN), off_true=off, stopped_true=int(v == 0.0), rain=int(rain), size=size, cargo=cargo, stop_cause=cause if v == 0.0 else \"\"))\n",
" arr = t\n",
" for r in rows:\n",
" r[\"eta_true\"] = arr - r[\"t\"]\n",
" return rows\n",
"\n",
"\n",
"def make_telemetry(seed=21, n_trips=200):\n",
" \"\"\"n_trips๊ฐ ์์ก์ ๋จ ์ด๋ ๊ธฐ๋ก(๋ ์ง ์). ์ ๋จ ๊ท๋ชจ 3~12๋, ํ๋ฌผ ๊ตฌ๋ถ ์ผ๋ฐ/๋์ฅ/๋ฏผ๊ฐ.\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" rows = []\n",
" for k in range(n_trips):\n",
" rows += simulate_trip(rng, k, int(rng.integers(5 * 60, 15 * 60)), bool(rng.random() < 0.2), int(rng.integers(3, 13)), str(rng.choice([\"์ผ๋ฐ\", \"๋์ฅ\", \"๋ฏผ๊ฐ\"], p=[.6, .25, .15])))\n",
" return pd.DataFrame(rows)\n",
"\n",
"\n",
"def mape(y, p):\n",
" y, p = np.asarray(y, float), np.asarray(p, float)\n",
" return float(np.mean(np.abs(y - p) / np.abs(y)) * 100)\n",
"\n",
"\n",
"def mae(y, p):\n",
" return float(np.mean(np.abs(np.asarray(y, float) - np.asarray(p, float))))\n",
"# ---------------------------------------------------------------------------\n",
"# ---------------------------------------------------------------------------\n",
"# ETA ์์ธก ํผ์ฒ: ๊ด์ธก ์์ ๊น์ง์ ํ
๋ ๋ฉํธ๋ฆฌ์ ๋จ์ ๋
ธ์ ๊ตฌ์ฑ(ํฌ์ฅยท์ํยท๊ต๋ํฐ๋ ๊ตฌ๊ฐ ๊ฑฐ๋ฆฌ)๋ง ์ฌ์ฉ\n",
"# ---------------------------------------------------------------------------\n",
"FEATS = [\"remaining_km\", \"progress\", \"speed\", \"speed_ma3\", \"speed_std3\", \"stop_n\", \"stop_min\", \"since_stop\", \"hour_sin\", \"hour_cos\", \"cong\", \"rain\", \"size\",\n",
" \"k_๋์ฅ\", \"k_๋ฏผ๊ฐ\", \"rem_unpaved_km\", \"rem_threat_km\", \"rem_bridge_km\", \"off_km\", \"rssi\", \"base_eta\"]\n",
"\n",
"\n",
"def rem_composition(s):\n",
" \"\"\"์งํ๊ฑฐ๋ฆฌ s ์ดํ ๋จ์ ๊ตฌ๊ฐ ์ค ๋นํฌ์ฅยท์ํยท๊ต๋ํฐ๋ ๊ฑฐ๋ฆฌ(km).\"\"\"\n",
" left = np.clip(ROUTE[\"c1\"].values[None, :] - np.maximum(np.atleast_1d(s)[:, None], ROUTE[\"c0\"].values[None, :]), 0, None)\n",
" return left @ np.array(UNP, float), left @ np.array(THR, float), left @ np.array(TUN, float)\n",
"\n",
"\n",
"def add_features(df):\n",
" d = df.sort_values([\"trip\", \"t\"]).copy()\n",
" d[\"s_obs\"], d[\"off_km\"] = project(d[\"x\"].values, d[\"y\"].values)\n",
" d[\"remaining_km\"] = LEN - d[\"s_obs\"]\n",
" d[\"progress\"] = d[\"s_obs\"] / LEN\n",
" g = d.groupby(\"trip\")[\"speed\"]\n",
" d[\"speed_ma3\"] = g.rolling(3, min_periods=1).mean().reset_index(level=0, drop=True)\n",
" d[\"speed_std3\"] = g.rolling(3, min_periods=1).std().reset_index(level=0, drop=True).fillna(0)\n",
" d[\"is_stop\"] = (d[\"speed\"] < 3).astype(int)\n",
" new_stop = (d[\"is_stop\"] == 1) & (d.groupby(\"trip\")[\"is_stop\"].shift(1).fillna(0) == 0)\n",
" d[\"stop_n\"] = new_stop.astype(int).groupby(d[\"trip\"]).cumsum()\n",
" d[\"stop_min\"] = d.groupby(\"trip\")[\"is_stop\"].cumsum() * 5\n",
" blk = (d[\"is_stop\"] == 1).astype(int).groupby(d[\"trip\"]).cumsum() # ์ ์ฐจ ๋์ ํ์๋ก ๊ตฌ๊ฐ์ ๋๋์ด ๋ง์ง๋ง ์ ์ฐจ ์ดํ ๊ฒฝ๊ณผ ๊ณ์ฐ\n",
" d[\"since_stop\"] = d.groupby([\"trip\", blk]).cumcount() * 5\n",
" d[\"hour_sin\"], d[\"hour_cos\"] = np.sin(2 * np.pi * d[\"clock\"] / 1440), np.cos(2 * np.pi * d[\"clock\"] / 1440)\n",
" d[\"cong\"] = d[\"clock\"].map(cong)\n",
" d[\"k_๋์ฅ\"], d[\"k_๋ฏผ๊ฐ\"] = (d[\"cargo\"] == \"๋์ฅ\").astype(int), (d[\"cargo\"] == \"๋ฏผ๊ฐ\").astype(int)\n",
" d[\"rem_unpaved_km\"], d[\"rem_threat_km\"], d[\"rem_bridge_km\"] = rem_composition(d[\"s_obs\"].values)\n",
" d[\"base_eta\"] = np.minimum(d[\"remaining_km\"] / np.maximum(d[\"speed\"], 5.0) * 60, 600) # ํ์ฌ ์๋ ๊ธฐ๋ฐ ๋จ์ ETA(๋ถ, ์ ์ฐจ ์ ํญ์ฃผ ๋ฐฉ์ง๋ก 5km/h ํํยท600๋ถ ์ํ)\n",
" return d.reset_index(drop=True)\n",
"# ---------------------------------------------------------------------------\n",
"\n",
"df = make_telemetry(n_trips=100); d = add_features(df)\n",
"d[\"k\"] = d.groupby(\"trip\").cumcount()\n",
"K = int(d[\"k\"].max()) + 1\n",
"\n",
"\n",
"def grid(col, fill=0.0):\n",
" return d.pivot(index=\"k\", columns=\"trip\", values=col).reindex(range(K)).fillna(fill).values\n",
"\n",
"\n",
"OFF, SPD, VALID = grid(\"off_km\"), grid(\"speed\"), grid(\"t\", -1) >= 0\n",
"OFFT, STOPT = grid(\"off_true\"), grid(\"stopped_true\")\n",
"F = np.stack([grid(c) for c in FEATS], axis=-1).astype(np.float32) # [ํฑ, ์ ๋จ, ํผ์ฒ]\n",
"model = HistGradientBoostingRegressor(max_iter=100, max_depth=4, learning_rate=0.1, random_state=0).fit(d[FEATS][d.trip < 70].values, d[\"eta_true\"][d.trip < 70].values)\n",
"\n",
"\n",
"def run_len2(flag):\n",
" \"\"\"์ด(์ ๋จ)๋ณ๋ก ์กฐ๊ฑด์ด ์ฐ์ ์ฐธ์ธ ํ์(ํ์ฌ ํฌํจ)๋ฅผ ํฑ ๋ฐฉํฅ์ผ๋ก ์ผ๋ค.\"\"\"\n",
" out, cur = np.zeros(flag.shape, int), np.zeros(flag.shape[1], int)\n",
" for k in range(flag.shape[0]):\n",
" cur = np.where(flag[k], cur + 1, 0); out[k] = cur\n",
" return out\n",
"\n",
"\n",
"TRUTH = (run_len2(OFFT >= 1.0) >= 2) | (run_len2(STOPT == 1) >= 2) # ์ ๋ต: ์ดํ ๋๋ ์ ์ฐจ๊ฐ 2ํ ์ฐ์ ๋ณด๊ณ ๋ ์ํ\n",
"print(f\"[0] ์คํธ๋ฆผ ์์ฒ: ์ ๋จ 100๊ฐ, ์ต๋ {K}ํฑ(5๋ถ ๊ฐ๊ฒฉ), ์ ํจ ๋ณด๊ณ {int(VALID.sum())}๊ฑด, ๊ฒฝ๋ณด ์ํ ์ ๋ต {int((TRUTH & VALID).sum())}๊ฑด({(TRUTH & VALID).sum() / VALID.sum():.1%})\")\n",
"\n",
"\n",
"def run_stream(N, loss=0.0, dup=0.0, stale=0.0, noise=False, dedupe=True, seed=0, predict=True):\n",
" \"\"\"N๊ฐ ์ ๋จ์ ์คํธ๋ฆผ์ ํฑ๋ง๋ค ์ผ๊ด ์ฒ๋ฆฌํ๋ค. ๋ฐํ: ํฑ ์ฒ๋ฆฌ ์ง์ฐ(์ด) ๋ฆฌ์คํธ, ์ ํ๋ ์งํ, ๋ง์ง๋ง ์ํ.\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" idx = np.arange(N) % 100\n",
" dev_run, stop_run = np.zeros(N, int), np.zeros(N, int)\n",
" prev_o, prev_s, eta = np.zeros(N), np.full(N, 50.0), np.zeros(N)\n",
" lat, tp, fp, fn, tn, n_dup_ignored = [], 0, 0, 0, 0, 0\n",
" for k in range(K):\n",
" t0 = time.perf_counter()\n",
" valid = VALID[k, idx]\n",
" recv = valid & (rng.random(N) >= loss) # ํจํท ์์ค์ด๋ฉด ๊ฐฑ์ ์์(์ํ ์ ์ง)\n",
" dups = recv & (rng.random(N) < dup) # ๊ฐ์ ๋ณด๊ณ ๊ฐ ๋ ๋ฒ ๋์ฐฉ\n",
" o, s = OFF[k, idx].copy(), SPD[k, idx].copy()\n",
" if stale > 0:\n",
" old = rng.random(N) < stale\n",
" o, s = np.where(old, prev_o, o), np.where(old, prev_s, s) # ์ง์ฐ ๋์ฐฉ: ์ด์ ํฑ ๊ฐ์ด ๋์ฐฉ\n",
" if noise:\n",
" o = o + np.abs(rng.normal(0, 0.9, N)); s = np.clip(s + rng.normal(0, 7, N), 0, None)\n",
" prev_o, prev_s = OFF[k, idx], SPD[k, idx]\n",
" times = recv.astype(int) if dedupe else recv.astype(int) + dups.astype(int)\n",
" n_dup_ignored += int(dups.sum()) if dedupe else 0\n",
" dev_run = np.where(recv, np.where(o >= 1.0, dev_run + times, 0), dev_run)\n",
" stop_run = np.where(recv, np.where(s < 3, stop_run + times, 0), stop_run)\n",
" alarm = (dev_run >= 2) | (stop_run >= 2)\n",
" if predict and recv.any():\n",
" eta[recv] = model.predict(F[k, idx[recv]])\n",
" lat.append(time.perf_counter() - t0)\n",
" tr_, v = TRUTH[k, idx], valid\n",
" tp += int((alarm & tr_ & v).sum()); fp += int((alarm & ~tr_ & v).sum()); fn += int((~alarm & tr_ & v).sum()); tn += int((~alarm & ~tr_ & v).sum())\n",
" tot = tp + fp + fn + tn\n",
" return lat, {\"์ ํ๋\": (tp + tn) / tot, \"์ฌํ์จ\": tp / max(tp + fn, 1), \"์ค๊ฒฝ๋ณด\": fp, \"๋ฏธํ\": fn, \"์ค๋ณต๋ฌด์\": n_dup_ignored}, (alarm, eta, idx)\n",
"\n",
"\n",
"def dashboard(alarm, eta, idx):\n",
" \"\"\"๋์๋ณด๋ ์ฒซ ํ๋ฉด: ์ํ ์ง๊ณ + ์ง์ฐ ์์ 10๊ฐ + ์ฐจํธ ๋ ๋๋ง(Agg)๊น์ง ๊ฑธ๋ฆฐ ์๊ฐ์ ์ฐ๋ค.\"\"\"\n",
" t0 = time.perf_counter()\n",
" tb = pd.DataFrame({\"convoy\": np.arange(len(idx)), \"eta\": eta, \"alarm\": alarm})\n",
" summ = tb[\"alarm\"].value_counts().to_dict()\n",
" top = tb.sort_values(\"eta\", ascending=False).head(10)\n",
" fg, ax = plt.subplots(1, 2, figsize=(9, 4)) # ์์น ์ฐ์ ๋์ ETA ๋ง๋๋ฅผ ์ค์ ๋ก ๊ทธ๋ ค ๋ ๋๋ง ์๊ฐ์ ํฌํจ\n",
" ax[0].scatter(np.arange(len(tb)) % 100, eta, c=np.where(alarm, \"r\", \"g\"), s=4); ax[1].barh(top[\"convoy\"].astype(str), top[\"eta\"])\n",
" fg.canvas.draw(); plt.close(fg)\n",
" return time.perf_counter() - t0, summ, top\n",
"\n",
"\n",
"# ---------------- 1) ๊ท๋ชจ๋ณ ๋ถํ ์ํ ----------------\n",
"rows = []\n",
"for N in (100, 300, 1000, 3000, 10000):\n",
" lat, acc, st = run_stream(N, loss=0.05, dup=0.05)\n",
" dl, _, _ = dashboard(*st)\n",
" lat = np.array(lat) * 1000\n",
" rows.append((N, np.median(lat), np.percentile(lat, 95), lat.max(), N / (np.median(lat) / 1000), dl, acc[\"์ ํ๋\"]))\n",
"ld = pd.DataFrame(rows, columns=[\"์ ๋จ ์\", \"ํฑ ์ฒ๋ฆฌ ์ค์๊ฐ(ms)\", \"p95(ms)\", \"์ต๋(ms)\", \"์ฒ๋ฆฌ๋(๊ฑด/์ด)\", \"๋์๋ณด๋ ๋ก๋ฉ(์ด)\", \"๊ฒฝ๋ณด ์ ํ๋\"])\n",
"print(\"\\n[1] ๊ท๋ชจ๋ณ ๋ถํ ์ํ(์์ค 5%, ์ค๋ณต 5% ํฌํจ, ํฑ๋ง๋ค ETA ๋ฐฐ์น ์์ธก ํฌํจ)\")\n",
"print(ld.round(3).to_string(index=False))\n",
"r100 = ld.iloc[0]\n",
"print(f\" 100๊ฐ ์ ๋จ: ๋์๋ณด๋ ๋ก๋ฉ {r100['๋์๋ณด๋ ๋ก๋ฉ(์ด)']:.3f}์ด(๋ชฉํ 2์ด ์ด๋ด: {'๋ฌ์ฑ' if r100['๋์๋ณด๋ ๋ก๋ฉ(์ด)'] <= 2 else '๋ฏธ๋ฌ'}), ํฑ p95 {r100['p95(ms)']:.0f}ms (5๋ถ ๋ณด๊ณ ์ฃผ๊ธฐ ๋๋น ์ถฉ๋ถ)\")\n",
"lim = ld[(ld[\"p95(ms)\"] <= 2000) & (ld[\"๋์๋ณด๋ ๋ก๋ฉ(์ด)\"] <= 2)][\"์ ๋จ ์\"].max()\n",
"print(f\" ํฑ p95 2์ดยท๋์๋ณด๋ 2์ด ์ด๋ด๋ฅผ ๋ชจ๋ ์งํจ ์ต๋ ์ํ ๊ท๋ชจ: {int(lim)}๊ฐ ์ ๋จ (์ด ํ๊ฒฝ ๊ธฐ์ค, ์ค์ ์๋ฒ๋ ๋ณ๋ ์ํ ํ์)\")\n",
"\n",
"# ---------------- 2) ์ค๋ฅ ์ฃผ์
: ๊ฒฝ๋ณด ์ ํ๋ ----------------\n",
"CASES = [(\"์ ์\", {}), (\"ํจํท ์์ค 15%\", dict(loss=.15)), (\"์ค๋ณต 20%(์ค๋ณต ์ ๊ฑฐ ์ผฌ)\", dict(dup=.2)), (\"์ค๋ณต 20%(์ค๋ณต ์ ๊ฑฐ ๋)\", dict(dup=.2, dedupe=False)),\n",
" (\"์ง์ฐ ๋์ฐฉ 10%\", dict(stale=.1)), (\"GPSยท์๋ ์ก์\", dict(noise=True)), (\"์์ค+์ค๋ณต+์ง์ฐ ๋ณตํฉ\", dict(loss=.1, dup=.1, stale=.1))]\n",
"res = []\n",
"for name, kw in CASES:\n",
" accs = [run_stream(100, seed=s, predict=False, **kw)[1] for s in range(5)]\n",
" res.append((name, np.mean([a[\"์ ํ๋\"] for a in accs]), np.mean([a[\"์ฌํ์จ\"] for a in accs]), np.mean([a[\"์ค๊ฒฝ๋ณด\"] for a in accs]), np.mean([a[\"๋ฏธํ\"] for a in accs])))\n",
"er = pd.DataFrame(res, columns=[\"์กฐ๊ฑด\", \"์ ํ๋\", \"์ฌํ์จ\", \"์ค๊ฒฝ๋ณด(ํ๊ท )\", \"๋ฏธํ(ํ๊ท )\"])\n",
"print(\"\\n[2] ์ค๋ฅ ์ฃผ์
์ ๊ฒฝ๋ณด ์ ํ๋(100๊ฐ ์ ๋จ, ์๋ 5ํ ํ๊ท , ์ ํ๋=ํฑ ๋จ์ ๊ฒฝ๋ณด ์ํ ์ผ์น์จ)\")\n",
"print(er.assign(์ ํ๋=er[\"์ ํ๋\"].map(\"{:.1%}\".format), ์ฌํ์จ=er[\"์ฌํ์จ\"].map(\"{:.1%}\".format)).round(1).to_string(index=False))\n",
"ok_set = er[er[\"์กฐ๊ฑด\"].isin([\"์ ์\", \"ํจํท ์์ค 15%\", \"์ค๋ณต 20%(์ค๋ณต ์ ๊ฑฐ ์ผฌ)\", \"์ง์ฐ ๋์ฐฉ 10%\"])][\"์ ํ๋\"].min()\n",
"print(f\" ํต์ ์ค๋ฅ 4๊ฐ ์กฐ๊ฑด ์ต์ ์ ํ๋ {ok_set:.1%} (๋ชฉํ 95% ์ด์: {'๋ฌ์ฑ' if ok_set >= .95 else '๋ฏธ๋ฌ'}). ์ค๋ณต ์ ๊ฑฐ๋ฅผ ๋๋ฉด ์ค๊ฒฝ๋ณด๊ฐ {er.iloc[3]['์ค๊ฒฝ๋ณด(ํ๊ท )']:.0f}๊ฑด์ผ๋ก ๋๊ณ , ์ก์ ์กฐ๊ฑด์ ์ ํ๋ {er.iloc[5]['์ ํ๋']:.1%}ยท์ฌํ์จ {er.iloc[5]['์ฌํ์จ']:.0%}๋ก ๋จ์ด์ง๋ค.\")\n",
"print(\" ํจํท ์์ค ์กฐ๊ฑด์ ์ ํ๋๋ ๋์๋ ์ฌํ์จ์ด ๋ฎ๋ค(๊ฒฝ๋ณด ์ํ๊ฐ ํฌ์ํด ์ ํ๋๊ฐ ๋ถํ๋ ค์ง๋ฏ๋ก ์ฌํ์จยท์ค๊ฒฝ๋ณด๋ฅผ ํจ๊ป ๋ณธ๋ค).\")\n",
"print(\" ์ก์ ์กฐ๊ฑด์ GPS ์ค์ฐจ๋ฅผ ๊ฐ์ ํ ์ต์
์ํ์ด๋ฉฐ, ์ค๊ฒฝ๋ณด๊ฐ ๋์ด ์๊ณ๊ฐยท์ฐ์ ํ์ ์กฐ์ ๋๋ ๋ณด์กฐ ์ผ์ ํ์ธ์ด ํ์ํ๋ค.\")\n",
"\n",
"# ---------------- 3) ํ๊ณ์น์ ๋์ ----------------\n",
"lat, acc, st = run_stream(10000, loss=0.2, dup=0.2, stale=0.1)\n",
"dl, summ, top = dashboard(*st)\n",
"print(f\"\\n[3] ํ๊ณ ์ํ(์ ๋จ 10000๊ฐ, ์์ค 20%+์ค๋ณต 20%+์ง์ฐ 10%): ํฑ p95 {np.percentile(lat, 95) * 1000:.0f}ms, ๋์๋ณด๋ {dl:.3f}์ด, ๊ฒฝ๋ณด ์ํ {summ}\")\n",
"print(\" ๋์๋ณด๋๋ ์ ์ฒด ํ ๋์ ์์ฝยท์ง์ฐ ์์ 10๊ฐยท๊ฒฝ๋ณด ๋ชฉ๋ก๋ง ๋จผ์ ๋ณด์ฌ ์ฃผ์ด ๋ก๋ฉ์ ์ค์ธ๋ค. ๊ท๋ชจ๊ฐ ์ปค์ง๋ฉด ํ๋ฉด ๊ฐฑ์ ์ ํฑ ๋จ์ ์ฆ๋ถ ๊ฐฑ์ ์ผ๋ก ๋ฐ๊พผ๋ค.\")\n",
"print(\" ๊ฒฝ๋ณด๋ ํ๋จ ๋ณด์กฐ์ด๋ฉฐ ํ์ฅ ํ์ธ๊ณผ ๋์ฒด ์์ก์ ์ฌ๋์ด ๊ฒฐ์ ํ๋ค(M7). ๋ชจ๋ ์ ๋จยท์ขํ๋ ๊ต์ก์ฉ ๊ฐ์ ์๋ฃ์ด๋ค.\")\n",
"\n",
"fig, ax = plt.subplots(1, 3, figsize=(16, 4.2))\n",
"ax[0].loglog(ld[\"์ ๋จ ์\"], ld[\"p95(ms)\"], \"o-\", label=\"tick p95\"); ax[0].axhline(2000, c=\"r\", ls=\"--\", label=\"2 s limit\"); ax[0].legend(); ax[0].set_title(\"Tick latency vs convoys (ms)\")\n",
"ax[1].barh([f\"C{i + 1}\" for i in range(len(er))][::-1], er[\"์ ํ๋\"].values[::-1] * 100); ax[1].axvline(95, c=\"r\", ls=\"--\"); ax[1].set_xlim(80, 100.5); ax[1].set_title(\"Alarm accuracy by fault case (%)\")\n",
"ax[2].plot(ld[\"์ ๋จ ์\"], ld[\"๋์๋ณด๋ ๋ก๋ฉ(์ด)\"], \"s-\"); ax[2].set_xscale(\"log\"); ax[2].set_title(\"Dashboard load time (s)\")\n",
"plt.tight_layout(); plt.show()\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"๐ก [์ฐธ๊ณ ] ๋ฏธ์
08 ์์ ์คํ ๊ฒฐ๊ณผ (Output)
\n",
"\n",
"```text\n",
"[0] ์คํธ๋ฆผ ์์ฒ: ์ ๋จ 100๊ฐ, ์ต๋ 67ํฑ(5๋ถ ๊ฐ๊ฒฉ), ์ ํจ ๋ณด๊ณ 5247๊ฑด, ๊ฒฝ๋ณด ์ํ ์ ๋ต 162๊ฑด(3.1%)\n",
"\n",
"[1] ๊ท๋ชจ๋ณ ๋ถํ ์ํ(์์ค 5%, ์ค๋ณต 5% ํฌํจ, ํฑ๋ง๋ค ETA ๋ฐฐ์น ์์ธก ํฌํจ)\n",
" ์ ๋จ ์ ํฑ ์ฒ๋ฆฌ ์ค์๊ฐ(ms) p95(ms) ์ต๋(ms) ์ฒ๋ฆฌ๋(๊ฑด/์ด) ๋์๋ณด๋ ๋ก๋ฉ(์ด) ๊ฒฝ๋ณด ์ ํ๋\n",
" 100 1.649 5.862 12.300 60653.223 0.250 0.998\n",
" 300 4.228 10.542 18.831 70955.568 0.266 0.998\n",
" 1000 5.457 8.010 11.268 183260.643 0.175 0.998\n",
" 3000 13.141 17.598 24.502 228285.936 0.142 0.998\n",
"10000 41.636 59.255 63.660 240177.214 0.223 0.998\n",
" 100๊ฐ ์ ๋จ: ๋์๋ณด๋ ๋ก๋ฉ 0.250์ด(๋ชฉํ 2์ด ์ด๋ด: ๋ฌ์ฑ), ํฑ p95 6ms (5๋ถ ๋ณด๊ณ ์ฃผ๊ธฐ ๋๋น ์ถฉ๋ถ)\n",
" ํฑ p95 2์ดยท๋์๋ณด๋ 2์ด ์ด๋ด๋ฅผ ๋ชจ๋ ์งํจ ์ต๋ ์ํ ๊ท๋ชจ: 10000๊ฐ ์ ๋จ (์ด ํ๊ฒฝ ๊ธฐ์ค, ์ค์ ์๋ฒ๋ ๋ณ๋ ์ํ ํ์)\n",
"\n",
"[2] ์ค๋ฅ ์ฃผ์
์ ๊ฒฝ๋ณด ์ ํ๋(100๊ฐ ์ ๋จ, ์๋ 5ํ ํ๊ท , ์ ํ๋=ํฑ ๋จ์ ๊ฒฝ๋ณด ์ํ ์ผ์น์จ)\n",
" ์กฐ๊ฑด ์ ํ๋ ์ฌํ์จ ์ค๊ฒฝ๋ณด(ํ๊ท ) ๋ฏธํ(ํ๊ท )\n",
" ์ ์ 100.0% 100.0% 0.0 0.0\n",
" ํจํท ์์ค 15% 99.5% 89.0% 7.8 17.8\n",
"์ค๋ณต 20%(์ค๋ณต ์ ๊ฑฐ ์ผฌ) 100.0% 100.0% 0.0 0.0\n",
"์ค๋ณต 20%(์ค๋ณต ์ ๊ฑฐ ๋) 99.8% 100.0% 10.2 0.0\n",
" ์ง์ฐ ๋์ฐฉ 10% 99.8% 96.4% 6.8 5.8\n",
" GPSยท์๋ ์ก์ 91.1% 56.5% 398.8 70.4\n",
" ์์ค+์ค๋ณต+์ง์ฐ ๋ณตํฉ 99.4% 90.1% 13.0 16.0\n",
" ํต์ ์ค๋ฅ 4๊ฐ ์กฐ๊ฑด ์ต์ ์ ํ๋ 99.5% (๋ชฉํ 95% ์ด์: ๋ฌ์ฑ). ์ค๋ณต ์ ๊ฑฐ๋ฅผ ๋๋ฉด ์ค๊ฒฝ๋ณด๊ฐ 10๊ฑด์ผ๋ก ๋๊ณ , ์ก์ ์กฐ๊ฑด์ ์ ํ๋ 91.1%ยท์ฌํ์จ 57%๋ก ๋จ์ด์ง๋ค.\n",
" ํจํท ์์ค ์กฐ๊ฑด์ ์ ํ๋๋ ๋์๋ ์ฌํ์จ์ด ๋ฎ๋ค(๊ฒฝ๋ณด ์ํ๊ฐ ํฌ์ํด ์ ํ๋๊ฐ ๋ถํ๋ ค์ง๋ฏ๋ก ์ฌํ์จยท์ค๊ฒฝ๋ณด๋ฅผ ํจ๊ป ๋ณธ๋ค).\n",
" ์ก์ ์กฐ๊ฑด์ GPS ์ค์ฐจ๋ฅผ ๊ฐ์ ํ ์ต์
์ํ์ด๋ฉฐ, ์ค๊ฒฝ๋ณด๊ฐ ๋์ด ์๊ณ๊ฐยท์ฐ์ ํ์ ์กฐ์ ๋๋ ๋ณด์กฐ ์ผ์ ํ์ธ์ด ํ์ํ๋ค.\n",
"\n",
"[3] ํ๊ณ ์ํ(์ ๋จ 10000๊ฐ, ์์ค 20%+์ค๋ณต 20%+์ง์ฐ 10%): ํฑ p95 55ms, ๋์๋ณด๋ 0.258์ด, ๊ฒฝ๋ณด ์ํ {False: 9974, True: 26}\n",
" ๋์๋ณด๋๋ ์ ์ฒด ํ ๋์ ์์ฝยท์ง์ฐ ์์ 10๊ฐยท๊ฒฝ๋ณด ๋ชฉ๋ก๋ง ๋จผ์ ๋ณด์ฌ ์ฃผ์ด ๋ก๋ฉ์ ์ค์ธ๋ค. ๊ท๋ชจ๊ฐ ์ปค์ง๋ฉด ํ๋ฉด ๊ฐฑ์ ์ ํฑ ๋จ์ ์ฆ๋ถ ๊ฐฑ์ ์ผ๋ก ๋ฐ๊พผ๋ค.\n",
" ๊ฒฝ๋ณด๋ ํ๋จ ๋ณด์กฐ์ด๋ฉฐ ํ์ฅ ํ์ธ๊ณผ ๋์ฒด ์์ก์ ์ฌ๋์ด ๊ฒฐ์ ํ๋ค(M7). ๋ชจ๋ ์ ๋จยท์ขํ๋ ๊ต์ก์ฉ ๊ฐ์ ์๋ฃ์ด๋ค.\n",
"```\n",
" \n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## ๐ฏ ๋ฏธ์
09. AAR ๊ธฐ๋ฐ ETA ์์ธก ์ค์ฐจ ๋ถ์ ๋ฐ ์ง์ฐ ์์ธ ๊ฐ์ค์น ๋งคํธ๋ฆญ์ค ์ฌํ๋\n",
"**์์์๊ฐ**: ์ฝ 3.0์๊ฐ\n",
"\n",
"### ๐ ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ์ํ ์ง์นจ\n",
"#### ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ๋น์์ฑ\n",
"\n",
"AAR ๊ธฐ๋ฐ ETA ์์ธก ์ค์ฐจ๋ฅผ ๋ถ์ํ๊ณ ๊ฐ์ค์น๋ฅผ ์ฌํ๋ํ๋ค. ์ค์ ์ ์ ํ์ฅ์์๋ ๋จ์ ํ์ง์ ๊ทธ์น์ง ์๊ณ ์ ์ํ ์ํ ํ๋จ๊ณผ ์ ๋ขฐ๋ ๋์ ์์ฌ๊ฒฐ์ ์ด ํ์์ ์ด๋ฏ๋ก, **AAR๋ก ETA ์์ธก ์ค์ฐจ๋ฅผ ๋ถ์ํ๊ณ ์ง์ฐ ์์ธ ๊ฐ์ค์น ๋งคํธ๋ฆญ์ค๋ฅผ ์ฌํ๋ํ๋ฉฐ ๋์
ํจ๊ณผ๋ฅผ ์ ๋ํํ๋ค.** ๊ณผ์
์ ์์ํ์ฌ ์คํ์ ๋ฐฉ์งํ๊ณ ์์ ๋์๋ ฅ์ ๊ทน๋ํํด์ผ ํฉ๋๋ค.\n",
"\n",
"์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ณธ ๋ฏธ์
์์๋ **์ด์ ๊ตฌ๊ฐ ๊ธฐ๋ก์์ ํํ ๊ฐ์ค์น ETA์ ์ค์ฐจ๋ฅผ ์กฐ๊ฑด๋ณ๋ก ๋๋๊ณ ์ค์ฐจ ํฐ ์ฌ๋ก๋ฅผ ๋ถ์ ์ ์ฐจยท์ฐ์ฒยท์ ์ฒดยท๋นํฌ์ฅ ์์ธ์ผ๋ก ๋ถ๋ฅ ์ํํ๊ณ ๊ฒ์ฆํ๋** ์ค์ต์ ์ฒด๊ณ์ ์ผ๋ก ์ํํฉ๋๋ค.\n",
"\n",
"ํ์ต์๋ ์ค์ต๊ฐ์ด๋์ ์ ์๋ ์ด 4๊ฐ ์ธ๋ถ ํ๋(๊ธฐ๋ณธ ๊ธฐ๋ฅ ๊ตฌํ โ ๊ฒฐ๊ณผ ์งํ ๊ฒ์ฆ โ ํ๋ผ๋ฏธํฐ ํ๋ ๋ฐ ๋์ ๊ณผ์ )์ ์์ฐจ์ ์ผ๋ก ์งํํ๋ฉฐ ํต์ฌ ํ์ ๊ธฐ์ค์ ๋ฌ์ฑํฉ๋๋ค.\n",
"\n",
"**์ด๋ฒ ๊ฒฐ๊ณผ๋ฌผ:** `reports/mov06_roi_analysis.md` ํํ์ ๊ฒฐ๊ณผ๋ฌผ์ ์์ฑํ๋ค.\n",
"\n",
"### ๐ ํ๊ฐ ๋ฐ ๊ฒ์ฆ ๊ธฐ์ค (PASS ์กฐ๊ฑด)\n",
"#### ๋ฏธ์
ํ์ \n",
"\n",
"**ํ๊ฐ ๊ตฌ๋ถ:** ํ์\n",
"\n",
"**ํต์ฌ ํ๊ฐ์์ญ:** ์ถ์ ์ฐ์์ ๊ฐ๊ด์ฑ ๋ฐ ๊ตฐ ์ฌ์ /๊ตฐ์ ๊ธฐํ ๋ถ์ ์ค๋๋ ฅ\n",
"\n",
"#### ๋ฏธ์
PASS ์กฐ๊ฑด\n",
"\n",
"- ์ ๋ ๊ธฐ์ค: ๋น์ฉ ์ ๊ฐ์ก, ์๊ฐ ๋จ์ถ๋ฅ ๋ฑ 3๋ ์ ๋ ํจ๊ณผ ์งํ ์ฐ์ถ\n",
"\n",
"\n",
"- ์งํ ๊ธฐ์ค: ROI ๋ถ์ ๋ณด๊ณ ์ ์์ฑ ๋ฐ ์ฃผ์ ์งํ ๋ฏผ๊ฐ๋ ๋ถ์ ์๋ก\n",
"\n",
"\n",
"- ๊ฒ์ฆ ๋ฐฉ๋ฒ: ํ๊ณ/๊ธฐํ ๊ธฐ์ค ๋ชจ๋ธ๊ณผ์ ๋น๊ต ๊ฒ์ฆ\n",
"\n",
"\n",
"#### ์ฑ์ ์ ํ์ ๋ฉ๋ชจ\n",
"\n",
"> ๋จ์ ์ฝ๋ ์คํ ์ฌ๋ถ๋ฟ๋ง ์๋๋ผ ์ถ๋ ฅ๋ ์์น์ ๊ทผ๊ฑฐ ์๋ฃ๊ฐ ๊ตฐ ์ค๋ฌด ์๊ตฌ์ฌํญ์ ๋ถํฉํ๋์ง ๋ฉด๋ฐํ ํ์ธํ๋ค.\n",
"\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"\"\"\"AAR๋ก ETA ์์ธก ์ค์ฐจ๋ฅผ ์กฐ๊ฑด๋ณยท์ฌ๋ก๋ณ๋ก ๋ถ์ํด ์์ธ์ ๋ถ๋ฅํ๊ณ , ์ง์ฐ ์์ธ ๊ฐ์ค์น ๋งคํธ๋ฆญ์ค(๋ถ/km ๋จ์)๋ฅผ ๋น์์ ์ต์์ ๊ณฑ์ผ๋ก ์ฌํ๋ํด ๋ณด๋ฅ ๊ธฐ๊ฐ์์ ๊ฐ์ ์ ๊ฒ์ฆํ ๋ค, ๋น์ฉ ์ ๊ฐ์กยท์๊ฐ ๋จ์ถ๋ฅ ยทํฌ์ ํ์ ๊ธฐ๊ฐ 3๋ ํจ๊ณผ ์งํ๋ฅผ ์ฐ์๊ณผ ๋ฏผ๊ฐ๋๋ก ์ฐ์ถํ๋ค.\"\"\"\n",
"import numpy as np\n",
"import pandas as pd\n",
"import matplotlib.pyplot as plt\n",
"from scipy.optimize import nnls\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# [๊ต์ก์ฉ๊ฐ์์๋ฃ] ๊ฐ์ ์์ก ๋
ธ์ ยท์์ก์ ๋จ ํ
๋ ๋ฉํธ๋ฆฌ ์์ฑ๊ธฐ (์ค์ ๋๋กยท์ง๋ช
ยท๋ถ๋ ์์)\n",
"# ๊ฐ์ ๋
ธ์ (์ถ๋ฐ ๊ฑฐ์ -> ๋์ฐฉ ๊ฑฐ์ , 5๊ฐ ๊ตฌ๊ฐ)์ 5๋ถ ๊ฐ๊ฒฉ์ผ๋ก ๋ณด๊ณ ํ๋ ์ ๋จ GPSยทํ๋ฌผ ์ผ์ยทํต์ ์ํ ์๋ฃ๋ฅผ ๋ง๋ ๋ค.\n",
"# ---------------------------------------------------------------------------\n",
"PTS = [(0, 0), (30, 12), (62, 14), (88, 38), (112, 42), (135, 66)] # ๋
ธ์ ๊ผญ์ง์ (km, ๊ฐ์ ์ขํ)\n",
"ROUTE = pd.DataFrame({\"seg\": [\"R1\", \"R2\", \"R3\", \"R4\", \"R5\"], \"road\": [\"paved\", \"unpaved\", \"paved\", \"paved\", \"unpaved\"],\n",
" \"limit\": [70, 50, 70, 50, 50], \"kind\": [\"์ผ๋ฐ\", \"์ผ๋ฐ\", \"์ผ๋ฐ\", \"๊ต๋ยทํฐ๋\", \"์ผ๋ฐ\"], \"threat\": [0, 0, 1, 0, 0]})\n",
"ROUTE[\"x0\"], ROUTE[\"y0\"] = [p[0] for p in PTS[:-1]], [p[1] for p in PTS[:-1]]\n",
"ROUTE[\"x1\"], ROUTE[\"y1\"] = [p[0] for p in PTS[1:]], [p[1] for p in PTS[1:]]\n",
"ROUTE[\"km\"] = np.hypot(ROUTE[\"x1\"] - ROUTE[\"x0\"], ROUTE[\"y1\"] - ROUTE[\"y0\"])\n",
"ROUTE[\"c0\"] = np.r_[0, np.cumsum(ROUTE[\"km\"])[:-1]]\n",
"ROUTE[\"c1\"] = np.cumsum(ROUTE[\"km\"])\n",
"LEN = float(ROUTE[\"km\"].sum())\n",
"ROAD_F = {\"paved\": 1.0, \"unpaved\": 0.75}\n",
"C1L, C0L, KML = ROUTE[\"c1\"].tolist(), ROUTE[\"c0\"].tolist(), ROUTE[\"km\"].tolist() # ๋ฐ๋ณต ๊ณ์ฐ์ฉ ๋ฆฌ์คํธ(์๋)\n",
"BASE_V = [l * ROAD_F[r] for l, r in zip(ROUTE[\"limit\"], ROUTE[\"road\"])]\n",
"THR, UNP, TUN = ROUTE[\"threat\"].tolist(), (ROUTE[\"road\"] == \"unpaved\").tolist(), (ROUTE[\"kind\"] == \"๊ต๋ยทํฐ๋\").tolist()\n",
"\n",
"\n",
"def seg_of(s):\n",
" # [TODO : ํ์ ์ค์ต 9] AAR ๊ธฐ๋ฐ ETA ์์ธก ์ค์ฐจ ๋ถ์ ๋ฐ ์ง์ฐ ์์ธ ๊ฐ์ค์น ๋งคํธ๋ฆญ์ค ์ฌํ๋ ๊ด๋ จ ํต์ฌ ๋ก์ง์ ๊ตฌํํ์ธ์.\n",
" # ํํธ: ์๋จ ์ฃผ์ ๋ฐ ๋ฐ์ดํฐ ์คํค๋ง/์ง์นจ์ ์ฐธ๊ณ ํ์ฌ ์์ฑํฉ๋๋ค.\n",
" # <<< ์ฌ๊ธฐ์ ์ฝ๋๋ฅผ ์์ฑํ์ธ์ >>>\n",
" pass # ์์ฑ ํ pass๋ฅผ ์ ๊ฑฐํ์ธ์.\n",
"def route_xy(s):\n",
" \"\"\"๋
ธ์ ์ ์งํ๊ฑฐ๋ฆฌ s(km)์ ์ขํ์ ์งํ ๋ฐฉํฅ(๋).\"\"\"\n",
" # [TODO : ํ์ ์ค์ต 9] AAR ๊ธฐ๋ฐ ETA ์์ธก ์ค์ฐจ ๋ถ์ ๋ฐ ์ง์ฐ ์์ธ ๊ฐ์ค์น ๋งคํธ๋ฆญ์ค ์ฌํ๋ ๊ด๋ จ ํต์ฌ ๋ก์ง์ ๊ตฌํํ์ธ์.\n",
" # ํํธ: ์๋จ ์ฃผ์ ๋ฐ ๋ฐ์ดํฐ ์คํค๋ง/์ง์นจ์ ์ฐธ๊ณ ํ์ฌ ์์ฑํฉ๋๋ค.\n",
" # <<< ์ฌ๊ธฐ์ ์ฝ๋๋ฅผ ์์ฑํ์ธ์ >>>\n",
" pass # ์์ฑ ํ pass๋ฅผ ์ ๊ฑฐํ์ธ์.\n",
"def project(x, y):\n",
" \"\"\"GPS ์ขํ๋ฅผ ๋
ธ์ ์ ํฌ์ํด (์งํ๊ฑฐ๋ฆฌ km, ๋
ธ์ ๊ณผ์ ์์ง ์ดํ km)๋ฅผ ๊ตฌํ๋ค.\"\"\"\n",
" x, y = np.atleast_1d(x).astype(float), np.atleast_1d(y).astype(float)\n",
" best_s, best_d = np.zeros(len(x)), np.full(len(x), np.inf)\n",
" for r in ROUTE.itertuples():\n",
" dx, dy = r.x1 - r.x0, r.y1 - r.y0\n",
" f = np.clip(((x - r.x0) * dx + (y - r.y0) * dy) / (dx * dx + dy * dy), 0, 1)\n",
" d = np.hypot(x - (r.x0 + f * dx), y - (r.y0 + f * dy))\n",
" better = d < best_d\n",
" best_s, best_d = np.where(better, r.c0 + f * r.km, best_s), np.where(better, d, best_d)\n",
" return best_s, best_d\n",
"\n",
"\n",
"def cong(clock_min):\n",
" h = (clock_min / 60) % 24\n",
" return 0.82 if (7 <= h < 9 or 17 <= h < 19) else 1.0 # ์ถํด๊ทผ ์๊ฐ๋ ์ ์ฒด ๊ณ์(๊ฐ์ )\n",
"\n",
"\n",
"def simulate_trip(rng, tid, start_min, rain, size, cargo):\n",
" \"\"\"1๋ถ ๋จ์๋ก ์ด๋์ ๋ชจ์ํ๊ณ 5๋ถ๋ง๋ค ํ
๋ ๋ฉํธ๋ฆฌ๋ฅผ ๋จ๊ธด๋ค. ์ดํยท๋ถ์ ์ ์ฐจ ์ด๋ฒคํธ๋ ์ ๋ต ๋ผ๋ฒจ๋ก ํจ๊ป ์ ์ฅํ๋ค.\"\"\"\n",
" s, t, stop_left, rows = 0.0, 0, 0, []\n",
" dev = (rng.random() < 0.12)\n",
" dev_t0 = int(rng.integers(30, 120)) if dev else -1\n",
" dev_len = int(rng.integers(12, 25)) if dev else 0\n",
" cause = \"\"\n",
" while s < LEN:\n",
" i = seg_of(s)\n",
" v = BASE_V[i] * (0.88 if rain else 1.0) * (0.85 if (rain and UNP[i]) else 1.0) * cong(start_min + t) \\\n",
" * (0.7 if THR[i] else 1.0) * (1 - 0.012 * size) * rng.normal(1, 0.06)\n",
" off = 0.0\n",
" if dev and dev_t0 <= t < dev_t0 + dev_len:\n",
" ph = (t - dev_t0) / dev_len\n",
" off, v = 4.0 * (1 - abs(2 * ph - 1)), v * 0.4 # ๋
ธ์ ์ดํ(์ผ๊ฐํ ํํ๋ก ์ต๋ 4km)\n",
" if stop_left > 0:\n",
" v, stop_left = 0.0, stop_left - 1\n",
" elif rng.random() < 0.0015:\n",
" stop_left, cause = int(rng.integers(8, 36)), (\"maint\" if rng.random() < 0.7 else \"urgent\")\n",
" s += v / 60\n",
" t += 1\n",
" if t % 5 == 0 or s >= LEN:\n",
" x, y, hd = route_xy(min(s, LEN))\n",
" nx, ny = np.cos(np.radians(hd)), -np.sin(np.radians(hd))\n",
" j = seg_of(min(s, LEN)); tun = TUN[j]\n",
" rows.append(dict(trip=tid, t=t, clock=(start_min + t) % 1440, x=x + off * nx + rng.normal(0, 0.03), y=y + off * ny + rng.normal(0, 0.03),\n",
" speed=max(v * rng.normal(1, 0.03), 0.0), heading=(hd + rng.normal(0, 3)) % 360, rssi=-62 - 14 * tun + rng.normal(0, 3),\n",
" cargo_temp=18 + 3 * np.sin(t / 40) + rng.normal(0, 0.3), shock_g=abs(rng.normal(0.25, 0.1)) * (2.0 if UNP[j] else 1.0),\n",
" s_true=min(s, LEN), off_true=off, stopped_true=int(v == 0.0), rain=int(rain), size=size, cargo=cargo, stop_cause=cause if v == 0.0 else \"\"))\n",
" arr = t\n",
" for r in rows:\n",
" r[\"eta_true\"] = arr - r[\"t\"]\n",
" return rows\n",
"\n",
"\n",
"def make_telemetry(seed=21, n_trips=200):\n",
" \"\"\"n_trips๊ฐ ์์ก์ ๋จ ์ด๋ ๊ธฐ๋ก(๋ ์ง ์). ์ ๋จ ๊ท๋ชจ 3~12๋, ํ๋ฌผ ๊ตฌ๋ถ ์ผ๋ฐ/๋์ฅ/๋ฏผ๊ฐ.\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" rows = []\n",
" for k in range(n_trips):\n",
" rows += simulate_trip(rng, k, int(rng.integers(5 * 60, 15 * 60)), bool(rng.random() < 0.2), int(rng.integers(3, 13)), str(rng.choice([\"์ผ๋ฐ\", \"๋์ฅ\", \"๋ฏผ๊ฐ\"], p=[.6, .25, .15])))\n",
" return pd.DataFrame(rows)\n",
"\n",
"\n",
"def mape(y, p):\n",
" y, p = np.asarray(y, float), np.asarray(p, float)\n",
" return float(np.mean(np.abs(y - p) / np.abs(y)) * 100)\n",
"\n",
"\n",
"def mae(y, p):\n",
" return float(np.mean(np.abs(np.asarray(y, float) - np.asarray(p, float))))\n",
"# ---------------------------------------------------------------------------\n",
"# ---------------------------------------------------------------------------\n",
"# ETA ์์ธก ํผ์ฒ: ๊ด์ธก ์์ ๊น์ง์ ํ
๋ ๋ฉํธ๋ฆฌ์ ๋จ์ ๋
ธ์ ๊ตฌ์ฑ(ํฌ์ฅยท์ํยท๊ต๋ํฐ๋ ๊ตฌ๊ฐ ๊ฑฐ๋ฆฌ)๋ง ์ฌ์ฉ\n",
"# ---------------------------------------------------------------------------\n",
"FEATS = [\"remaining_km\", \"progress\", \"speed\", \"speed_ma3\", \"speed_std3\", \"stop_n\", \"stop_min\", \"since_stop\", \"hour_sin\", \"hour_cos\", \"cong\", \"rain\", \"size\",\n",
" \"k_๋์ฅ\", \"k_๋ฏผ๊ฐ\", \"rem_unpaved_km\", \"rem_threat_km\", \"rem_bridge_km\", \"off_km\", \"rssi\", \"base_eta\"]\n",
"\n",
"\n",
"def rem_composition(s):\n",
" \"\"\"์งํ๊ฑฐ๋ฆฌ s ์ดํ ๋จ์ ๊ตฌ๊ฐ ์ค ๋นํฌ์ฅยท์ํยท๊ต๋ํฐ๋ ๊ฑฐ๋ฆฌ(km).\"\"\"\n",
" left = np.clip(ROUTE[\"c1\"].values[None, :] - np.maximum(np.atleast_1d(s)[:, None], ROUTE[\"c0\"].values[None, :]), 0, None)\n",
" return left @ np.array(UNP, float), left @ np.array(THR, float), left @ np.array(TUN, float)\n",
"\n",
"\n",
"def add_features(df):\n",
" d = df.sort_values([\"trip\", \"t\"]).copy()\n",
" d[\"s_obs\"], d[\"off_km\"] = project(d[\"x\"].values, d[\"y\"].values)\n",
" d[\"remaining_km\"] = LEN - d[\"s_obs\"]\n",
" d[\"progress\"] = d[\"s_obs\"] / LEN\n",
" g = d.groupby(\"trip\")[\"speed\"]\n",
" d[\"speed_ma3\"] = g.rolling(3, min_periods=1).mean().reset_index(level=0, drop=True)\n",
" d[\"speed_std3\"] = g.rolling(3, min_periods=1).std().reset_index(level=0, drop=True).fillna(0)\n",
" d[\"is_stop\"] = (d[\"speed\"] < 3).astype(int)\n",
" new_stop = (d[\"is_stop\"] == 1) & (d.groupby(\"trip\")[\"is_stop\"].shift(1).fillna(0) == 0)\n",
" d[\"stop_n\"] = new_stop.astype(int).groupby(d[\"trip\"]).cumsum()\n",
" d[\"stop_min\"] = d.groupby(\"trip\")[\"is_stop\"].cumsum() * 5\n",
" blk = (d[\"is_stop\"] == 1).astype(int).groupby(d[\"trip\"]).cumsum() # ์ ์ฐจ ๋์ ํ์๋ก ๊ตฌ๊ฐ์ ๋๋์ด ๋ง์ง๋ง ์ ์ฐจ ์ดํ ๊ฒฝ๊ณผ ๊ณ์ฐ\n",
" d[\"since_stop\"] = d.groupby([\"trip\", blk]).cumcount() * 5\n",
" d[\"hour_sin\"], d[\"hour_cos\"] = np.sin(2 * np.pi * d[\"clock\"] / 1440), np.cos(2 * np.pi * d[\"clock\"] / 1440)\n",
" d[\"cong\"] = d[\"clock\"].map(cong)\n",
" d[\"k_๋์ฅ\"], d[\"k_๋ฏผ๊ฐ\"] = (d[\"cargo\"] == \"๋์ฅ\").astype(int), (d[\"cargo\"] == \"๋ฏผ๊ฐ\").astype(int)\n",
" d[\"rem_unpaved_km\"], d[\"rem_threat_km\"], d[\"rem_bridge_km\"] = rem_composition(d[\"s_obs\"].values)\n",
" d[\"base_eta\"] = np.minimum(d[\"remaining_km\"] / np.maximum(d[\"speed\"], 5.0) * 60, 600) # ํ์ฌ ์๋ ๊ธฐ๋ฐ ๋จ์ ETA(๋ถ, ์ ์ฐจ ์ ํญ์ฃผ ๋ฐฉ์ง๋ก 5km/h ํํยท600๋ถ ์ํ)\n",
" return d.reset_index(drop=True)\n",
"# ---------------------------------------------------------------------------\n",
"\n",
"df = make_telemetry(); d = add_features(df)\n",
"d = d[d.eta_true >= 15].copy()\n",
"d[\"unp_share\"] = d[\"rem_unpaved_km\"] / d[\"remaining_km\"].clip(lower=1)\n",
"d[\"thr_share\"] = d[\"rem_threat_km\"] / d[\"remaining_km\"].clip(lower=1)\n",
"d[\"brg_share\"] = d[\"rem_bridge_km\"] / d[\"remaining_km\"].clip(lower=1)\n",
"d[\"peak\"] = 1 - d[\"cong\"]\n",
"# ๊ฐ์ค์น ๋งคํธ๋ฆญ์ค: ๋จ์ 1km๋น ์์(๋ถ) = ๊ธฐ๋ณธ + ์์ธ๋ณ ๊ฐ์ฐ. ์์ธ ๊ฐ์ ๊ด์ธก ์์ ์ ์ํฉ.\n",
"COLS = {\"๊ธฐ๋ณธ(๋ถ/km)\": None, \"๋นํฌ์ฅ ๋น์จ\": \"unp_share\", \"์ํ ๊ตฌ์ญ ๋น์จ\": \"thr_share\", \"๊ต๋ยทํฐ๋ ๋น์จ\": \"brg_share\", \"์ฐ์ฒ\": \"rain\", \"์ฐ์ฒ x ๋นํฌ์ฅ\": None, \"์๊ฐ๋ ์ ์ฒด\": \"peak\", \"์ ๋จ ๊ท๋ชจ(๋)\": \"size\"}\n",
"NAMES = list(COLS)\n",
"\n",
"\n",
"def design(x):\n",
" km = x[\"remaining_km\"].values[:, None]\n",
" z = np.column_stack([np.ones(len(x)), x[\"unp_share\"], x[\"thr_share\"], x[\"brg_share\"], x[\"rain\"], x[\"rain\"] * x[\"unp_share\"], x[\"peak\"], x[\"size\"] / 10])\n",
" return km * z\n",
"\n",
"\n",
"def eta_rule(x, w):\n",
" return design(x) @ w\n",
"\n",
"\n",
"W0 = np.array([1.00, 0.50, 0.30, 0.10, 0.10, 0.10, 0.40, 0.05]) # ๋์
์์ (๊ณผ๊ฑฐ ๊ธฐ๋ก ๋ถ์กฑ) ์ ๋ฌธ๊ฐ ์ถ์ ๊ฐ์ค์น\n",
"past, ops, hold = d[d.trip < 100], d[(d.trip >= 100) & (d.trip < 150)], d[d.trip >= 150]\n",
"\n",
"# ---------------- 1) AAR ์ค์ฐจ ๋ถ์: ์ด์ ๊ตฌ๊ฐ(์ด๋ 100~149) ----------------\n",
"ops = ops.assign(pred=eta_rule(ops, W0), err=lambda x: x[\"pred\"] - x[\"eta_true\"])\n",
"print(f\"[1] AAR ๋์: ์ด๋ 100~149({ops.trip.nunique()}๊ฐ, {len(ops)}ํ) ํํ ๊ฐ์ค์น ETA MAPE {mape(ops.eta_true, ops.pred):.1f}%, MAE {mae(ops.eta_true, ops.pred):.1f}๋ถ, ํ๊ท ํธํฅ {ops.err.mean():+.1f}๋ถ(์์=๊ณผ์ ์์ธก)\")\n",
"conds = {\"์ฐ์ฒ\": ops.rain == 1, \"์๊ฐ๋ ์ ์ฒด\": ops.peak > 0, \"์ ์ฐจ ์ค/์ ์ฐจ ์งํ\": (ops.since_stop <= 10) & (ops.stop_n > 0), \"๋นํฌ์ฅ ๊ตฌ๊ฐ ์์ฃผ\": ops.unp_share > 0.5, \"๋์ฅ ํ๋ฌผ\": ops.cargo == \"๋์ฅ\", \"๋ฏผ๊ฐ ํ๋ฌผ\": ops.cargo == \"๋ฏผ๊ฐ\", \"ํ์\": (ops.rain == 0) & (ops.peak == 0) & (ops.stop_n == 0)}\n",
"ct = pd.DataFrame([(k, int(m.sum()), mape(ops.eta_true[m], ops.pred[m]), ops.err[m].mean()) for k, m in conds.items() if m.sum()], columns=[\"์กฐ๊ฑด\", \"๊ฑด์\", \"MAPE(%)\", \"ํ๊ท ํธํฅ(๋ถ)\"])\n",
"print(ct.round(1).to_string(index=False))\n",
"worst = ops.reindex(ops.err.abs().sort_values(ascending=False).index).head(40)\n",
"\n",
"\n",
"def cause(r):\n",
" if r[\"stop_n\"] > 0 and r[\"since_stop\"] <= 10: return \"๋ถ์ ์ ์ฐจ ์งํ\"\n",
" if r[\"rain\"] == 1: return \"์ฐ์ฒ ๊ณผ์ ๋ฐ์\"\n",
" if r[\"peak\"] > 0: return \"์๊ฐ๋ ์ ์ฒด ๊ณผ์ ๋ฐ์\"\n",
" if r[\"unp_share\"] > 0.5: return \"๋นํฌ์ฅ ๊ตฌ๊ฐ ๊ณผ์ ๋ฐ์\"\n",
" return \"๊ธฐํ(์๋ ๋ณ๋)\"\n",
"\n",
"\n",
"cs = worst.apply(cause, axis=1).value_counts()\n",
"print(\" ์ค์ฐจ ํฐ ์์ 40๊ฑด์ ์์ธ ๋ถ๋ฅ:\", \", \".join(f\"{k} {v}๊ฑด\" for k, v in cs.items()))\n",
"\n",
"# ---------------- 2) ๊ฐ์ค์น ์ฌํ๋: ๋น์์ ์ต์์ ๊ณฑ(์ด์ ๊ตฌ๊ฐ ํ์ต -> ๋ณด๋ฅ ๊ตฌ๊ฐ ๊ฒ์ฆ) ----------------\n",
"w1, _ = nnls(design(ops), ops[\"eta_true\"].values)\n",
"ho = hold.assign(p0=eta_rule(hold, W0), p1=eta_rule(hold, w1))\n",
"b = (mape(ho.eta_true, ho.p0), mae(ho.eta_true, ho.p0), np.sqrt(np.mean((ho.eta_true - ho.p0) ** 2)))\n",
"a = (mape(ho.eta_true, ho.p1), mae(ho.eta_true, ho.p1), np.sqrt(np.mean((ho.eta_true - ho.p1) ** 2)))\n",
"mt = pd.DataFrame({\"์ฌํ๋ ์ \": W0, \"์ฌํ๋ ํ\": w1}, index=NAMES)\n",
"mt[\"๋ณํ์จ\"] = np.where(mt[\"์ฌํ๋ ์ \"] > 0, mt[\"์ฌํ๋ ํ\"] / mt[\"์ฌํ๋ ์ \"] - 1, np.nan)\n",
"print(\"\\n[2] ์ง์ฐ ์์ธ ๊ฐ์ค์น ๋งคํธ๋ฆญ์ค(๋จ์ 1km๋น ๋ถ)\")\n",
"print(mt.round(3).to_string())\n",
"print(f\" ๋ณด๋ฅ ๊ตฌ๊ฐ(์ด๋ 150~199, {len(ho)}ํ): MAPE {b[0]:.1f}% -> {a[0]:.1f}%, MAE {b[1]:.1f} -> {a[1]:.1f}๋ถ, RMSE {b[2]:.1f} -> {a[2]:.1f}๋ถ\")\n",
"chg = mt[(mt[\"๋ณํ์จ\"].abs() > 0.5)].index.tolist()\n",
"print(f\" ๋ณํ์จ 50% ์ด๊ณผ ์์ธ {chg or '์์'}๋ ์ฌ๋์ด ๊ฒํ ยท์น์ธํ ๋ค ์ ์ฉํ๋ค(์๋ ๋ฐ์ ๊ธ์ง). ํ์ต ๊ธฐ๊ฐ(์ด๋ 0~99) ์ฌํ: ์ฌํ๋ ํ MAPE {mape(past.eta_true, eta_rule(past, w1)):.1f}%\")\n",
"print(\" ์ฐ์ฒ๊ณผ ๋นํฌ์ฅ ๊ฐ์ค์น๊ฐ ํฌ๊ฒ ๋ฐ๋๋ฉด ํ์ฅ ๊ด์ธก ๊ทผ๊ฑฐ๋ฅผ ํจ๊ป ํ์ธํด์ผ ํ๋ฉฐ, ๋
ธ์ ์ด ํ๋๋ฟ์ธ ๊ฐ์ ์๋ฃ๋ผ ์ค์ ๋
ธ์ ์๋ ๋ค์ ํ์ตํด์ผ ํ๋ค.\")\n",
"\n",
"# ---------------- 3) ROI 3๋ ์งํ(๊ต์ก์ฉ ๊ฐ์ ๊ฐ) ----------------\n",
"TRIPS_YR, STAFF, WAGE, MARGIN = 1200, 6, 1.2, 15 # ์ฐ ์์ก ํ์, ์๋ น ๋๊ธฐ ์ธ์, ์๊ฐ๋น ์ธ๊ฑด๋น(๋ง์), ์์ ์ฌ์ ์๊ฐ(๋ถ)\n",
"INV, OPEX = 3000.0, 120.0 # ๋์
๋น์ฉ(๋ง์), ์ฐ ์ด์๋น(๋ง์)\n",
"\n",
"\n",
"def roi(mae_b, mae_a, trips=TRIPS_YR, inv=INV):\n",
" wait_b, wait_a = MARGIN + mae_b, MARGIN + mae_a # ์๋ น ์ธก์ ETA ์ค์ฐจ๋งํผ ์ถ๊ฐ๋ก ๋๊ธฐยท์ค๋นํ๋ค๊ณ ๊ฐ์ \n",
" save = (wait_b - wait_a) / 60 * STAFF * WAGE * trips - OPEX\n",
" return save, 1 - wait_a / wait_b, inv / max(save / 12, 1e-9)\n",
"\n",
"\n",
"sv, rate, pb = roi(b[1], a[1])\n",
"print(f\"\\n[3] ROI 3๋ ์งํ [๊ต์ก์ฉ ๊ฐ์ : ์ฐ {TRIPS_YR}ํ, ๋๊ธฐ ์ธ์ {STAFF}๋ช
, ์ธ๊ฑด๋น {WAGE}๋ง์/์, ๋์
{INV:.0f}๋ง์, ์ด์ {OPEX:.0f}๋ง์/๋
]\")\n",
"print(f\" (1) ์ฐ๊ฐ ๋น์ฉ ์ ๊ฐ์ก {sv:,.0f}๋ง์ (2) ์๋ น ๋๊ธฐยท์ค๋น ์๊ฐ ๋จ์ถ๋ฅ {rate:.1%} (3) ํฌ์ ํ์ ๊ธฐ๊ฐ {pb:.1f}๊ฐ์\")\n",
"print(\" ์ฐ์: ์ ๊ฐ์ก = (์ฌํ๋ ์ ๋๊ธฐ - ํ ๋๊ธฐ)/60 x ์ธ์ x ์ธ๊ฑด๋น x ํ์ - ์ด์๋น, ๋๊ธฐ = ์์ ์ฌ์ 15๋ถ + ETA ํ๊ท ์ค์ฐจ\")\n",
"sens = [(t, roi(b[1], a[1], trips=t)) for t in (300, 600, 1200, 2400)]\n",
"print(\" ํ์ ๋ฏผ๊ฐ๋:\", \"; \".join(f\"{t}ํ -> ์ ๊ฐ {s[0]:,.0f}๋ง์, ํ์ {s[2]:.0f}๊ฐ์\" for t, s in sens))\n",
"print(\" ๋น๊ด ์๋๋ฆฌ์ค(์ค์ฐจ ๊ฐ์ ์ ๋ฐ๋ง ์คํ, 400ํ):\", f\"์ ๊ฐ {roi(b[1], (a[1] + b[1]) / 2, trips=400)[0]:,.0f}๋ง์, ํ์ {roi(b[1], (a[1] + b[1]) / 2, trips=400)[2]:.0f}๊ฐ์\")\n",
"print(\" ํจ๊ณผ 3์งํ ์ฐ์ถ ์๋ฃ(๋น์ฉ ์ ๊ฐ์กยท์๊ฐ ๋จ์ถ๋ฅ ยทํ์ ๊ธฐ๊ฐ). ๊ฐ์ ๊ฐ์ด๋ฏ๋ก ์ฌ์ ยท๊ธฐํ ๋ถ์์ ์ค์ ๋จ๊ฐ๋ก ๋ค์ ์ฐ์ ํด์ผ ํ๋ค.\")\n",
"\n",
"fig, ax = plt.subplots(1, 3, figsize=(16, 4.3))\n",
"ax[0].bar(ct[\"์กฐ๊ฑด\"].index, ct[\"MAPE(%)\"]); ax[0].set_xticks(range(len(ct))); ax[0].set_xticklabels([f\"C{i + 1}\" for i in range(len(ct))]); ax[0].set_title(\"AAR: MAPE (%) by condition\")\n",
"x = np.arange(len(mt)); ax[1].bar(x - .2, mt[\"์ฌํ๋ ์ \"], .4, label=\"before\"); ax[1].bar(x + .2, mt[\"์ฌํ๋ ํ\"], .4, label=\"after\"); ax[1].set_xticks(x); ax[1].set_xticklabels([f\"w{i}\" for i in x]); ax[1].legend(); ax[1].set_title(\"Delay weight matrix (min/km)\")\n",
"ax[2].scatter(ho.eta_true, ho.p0, s=6, alpha=.4, label=\"before\"); ax[2].scatter(ho.eta_true, ho.p1, s=6, alpha=.4, label=\"after\"); ax[2].plot([0, 250], [0, 250], \"k--\"); ax[2].legend(); ax[2].set_title(\"Hold-out ETA: actual vs predicted (min)\")\n",
"plt.tight_layout(); plt.show()\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"๐ก [์ฐธ๊ณ ] ๋ฏธ์
09 ์์ ์คํ ๊ฒฐ๊ณผ (Output)
\n",
"\n",
"```text\n",
"[1] AAR ๋์: ์ด๋ 100~149(50๊ฐ, 2408ํ) ํํ ๊ฐ์ค์น ETA MAPE 21.2%, MAE 28.4๋ถ, ํ๊ท ํธํฅ -28.4๋ถ(์์=๊ณผ์ ์์ธก)\n",
" ์กฐ๊ฑด ๊ฑด์ MAPE(%) ํ๊ท ํธํฅ(๋ถ)\n",
" ์ฐ์ฒ 571 28.6 -43.1\n",
" ์๊ฐ๋ ์ ์ฒด 485 22.2 -27.2\n",
"์ ์ฐจ ์ค/์ ์ฐจ ์งํ 154 27.4 -30.0\n",
" ๋นํฌ์ฅ ๊ตฌ๊ฐ ์์ฃผ 1216 23.7 -20.9\n",
" ๋์ฅ ํ๋ฌผ 520 20.3 -27.2\n",
" ๋ฏผ๊ฐ ํ๋ฌผ 490 23.6 -31.8\n",
" ํ์ 1128 18.6 -25.1\n",
" ์ค์ฐจ ํฐ ์์ 40๊ฑด์ ์์ธ ๋ถ๋ฅ: ์ฐ์ฒ ๊ณผ์ ๋ฐ์ 30๊ฑด, ๋ถ์ ์ ์ฐจ ์งํ 10๊ฑด\n",
"\n",
"[2] ์ง์ฐ ์์ธ ๊ฐ์ค์น ๋งคํธ๋ฆญ์ค(๋จ์ 1km๋น ๋ถ)\n",
" ์ฌํ๋ ์ ์ฌํ๋ ํ ๋ณํ์จ\n",
"๊ธฐ๋ณธ(๋ถ/km) 1.00 0.860 -0.140\n",
"๋นํฌ์ฅ ๋น์จ 0.50 0.962 0.924\n",
"์ํ ๊ตฌ์ญ ๋น์จ 0.30 0.377 0.255\n",
"๊ต๋ยทํฐ๋ ๋น์จ 0.10 0.520 4.205\n",
"์ฐ์ฒ 0.10 0.161 0.614\n",
"์ฐ์ฒ x ๋นํฌ์ฅ 0.10 0.484 3.838\n",
"์๊ฐ๋ ์ ์ฒด 0.40 0.622 0.554\n",
"์ ๋จ ๊ท๋ชจ(๋) 0.05 0.172 2.445\n",
" ๋ณด๋ฅ ๊ตฌ๊ฐ(์ด๋ 150~199, 2342ํ): MAPE 18.8% -> 8.1%, MAE 24.7 -> 9.9๋ถ, RMSE 30.9 -> 12.6๋ถ\n",
" ๋ณํ์จ 50% ์ด๊ณผ ์์ธ ['๋นํฌ์ฅ ๋น์จ', '๊ต๋ยทํฐ๋ ๋น์จ', '์ฐ์ฒ', '์ฐ์ฒ x ๋นํฌ์ฅ', '์๊ฐ๋ ์ ์ฒด', '์ ๋จ ๊ท๋ชจ(๋)']๋ ์ฌ๋์ด ๊ฒํ ยท์น์ธํ ๋ค ์ ์ฉํ๋ค(์๋ ๋ฐ์ ๊ธ์ง). ํ์ต ๊ธฐ๊ฐ(์ด๋ 0~99) ์ฌํ: ์ฌํ๋ ํ MAPE 8.5%\n",
" ์ฐ์ฒ๊ณผ ๋นํฌ์ฅ ๊ฐ์ค์น๊ฐ ํฌ๊ฒ ๋ฐ๋๋ฉด ํ์ฅ ๊ด์ธก ๊ทผ๊ฑฐ๋ฅผ ํจ๊ป ํ์ธํด์ผ ํ๋ฉฐ, ๋
ธ์ ์ด ํ๋๋ฟ์ธ ๊ฐ์ ์๋ฃ๋ผ ์ค์ ๋
ธ์ ์๋ ๋ค์ ํ์ตํด์ผ ํ๋ค.\n",
"\n",
"[3] ROI 3๋ ์งํ [๊ต์ก์ฉ ๊ฐ์ : ์ฐ 1200ํ, ๋๊ธฐ ์ธ์ 6๋ช
, ์ธ๊ฑด๋น 1.2๋ง์/์, ๋์
3000๋ง์, ์ด์ 120๋ง์/๋
]\n",
" (1) ์ฐ๊ฐ ๋น์ฉ ์ ๊ฐ์ก 2,016๋ง์ (2) ์๋ น ๋๊ธฐยท์ค๋น ์๊ฐ ๋จ์ถ๋ฅ 37.3% (3) ํฌ์ ํ์ ๊ธฐ๊ฐ 17.9๊ฐ์\n",
" ์ฐ์: ์ ๊ฐ์ก = (์ฌํ๋ ์ ๋๊ธฐ - ํ ๋๊ธฐ)/60 x ์ธ์ x ์ธ๊ฑด๋น x ํ์ - ์ด์๋น, ๋๊ธฐ = ์์ ์ฌ์ 15๋ถ + ETA ํ๊ท ์ค์ฐจ\n",
" ํ์ ๋ฏผ๊ฐ๋: 300ํ -> ์ ๊ฐ 414๋ง์, ํ์ 87๊ฐ์; 600ํ -> ์ ๊ฐ 948๋ง์, ํ์ 38๊ฐ์; 1200ํ -> ์ ๊ฐ 2,016๋ง์, ํ์ 18๊ฐ์; 2400ํ -> ์ ๊ฐ 4,152๋ง์, ํ์ 9๊ฐ์\n",
" ๋น๊ด ์๋๋ฆฌ์ค(์ค์ฐจ ๊ฐ์ ์ ๋ฐ๋ง ์คํ, 400ํ): ์ ๊ฐ 236๋ง์, ํ์ 153๊ฐ์\n",
" ํจ๊ณผ 3์งํ ์ฐ์ถ ์๋ฃ(๋น์ฉ ์ ๊ฐ์กยท์๊ฐ ๋จ์ถ๋ฅ ยทํ์ ๊ธฐ๊ฐ). ๊ฐ์ ๊ฐ์ด๋ฏ๋ก ์ฌ์ ยท๊ธฐํ ๋ถ์์ ์ค์ ๋จ๊ฐ๋ก ๋ค์ ์ฐ์ ํด์ผ ํ๋ค.\n",
"```\n",
" \n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## ๐ฏ ๋ฏธ์
10. ๊ตฐ์ํ ์ด๋ ์ํ ์ถ์ ๋ฐ ๋์ฐฉ ์์ ์๊ฐ(ETA) ์์ธก ์์คํ
์ต์ข
์์ฑ\n",
"**์์์๊ฐ**: ์ฝ 3.5์๊ฐ\n",
"\n",
"### ๐ ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ์ํ ์ง์นจ\n",
"#### ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ๋น์์ฑ\n",
"\n",
"๊ตฐ์ํ ์ด๋ ์ํ ์ถ์ ๋ฐ ๋์ฐฉ ์์ ์๊ฐ(ETA) ์์ธก ์์คํ
์ ์์ฑํ๋ค. ์ค์ ์ ์ ํ์ฅ์์๋ ๋จ์ ํ์ง์ ๊ทธ์น์ง ์๊ณ ์ ์ํ ์ํ ํ๋จ๊ณผ ์ ๋ขฐ๋ ๋์ ์์ฌ๊ฒฐ์ ์ด ํ์์ ์ด๋ฏ๋ก, **๊ตฐ์ํ ์ด๋ ์ํ ์ถ์ ๊ณผ ETA ์์ธก์ ํ๋๋ก ๋ฌถ์ ํตํฉ ํ์ดํ๋ผ์ธ์ ์์ฑํ๊ณ ์๋ ์ฌํ์ตยท์ด์ ๋ฌธ์ยท์์ฒด ์ ๊ฒ์ ๊ฐ์ถ๋ค.** ๊ณผ์
์ ์์ํ์ฌ ์คํ์ ๋ฐฉ์งํ๊ณ ์์ ๋์๋ ฅ์ ๊ทน๋ํํด์ผ ํฉ๋๋ค.\n",
"\n",
"์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ณธ ๋ฏธ์
์์๋ **์ ๊ท ์ด๋ 40๊ฐ๋ฅผ ์ถ๊ฐํด ์ ํฉ์ฑ์ ๊ฒ์ฆํ๊ณ ๊ธฐ์กด ๋ชจ๋ธ์ ์ค์ฐจ๋ก ๋๋ฆฌํํธ๋ฅผ ํ๋จํ ๋ค ์ฑ๋ฆฐ์ ๋ชจ๋ธ์ ํ์ตํด ๊ฐ์ ์ด 3% ์ด์์ผ ๋๋ง ์น๊ฒฉ ์ํํ๊ณ ๊ฒ์ฆํ๋** ์ค์ต์ ์ฒด๊ณ์ ์ผ๋ก ์ํํฉ๋๋ค.\n",
"\n",
"ํ์ต์๋ ์ค์ต๊ฐ์ด๋์ ์ ์๋ ์ด 4๊ฐ ์ธ๋ถ ํ๋(๊ธฐ๋ณธ ๊ธฐ๋ฅ ๊ตฌํ โ ๊ฒฐ๊ณผ ์งํ ๊ฒ์ฆ โ ํ๋ผ๋ฏธํฐ ํ๋ ๋ฐ ๋์ ๊ณผ์ )์ ์์ฐจ์ ์ผ๋ก ์งํํ๋ฉฐ ํต์ฌ ํ์ ๊ธฐ์ค์ ๋ฌ์ฑํฉ๋๋ค.\n",
"\n",
"**์ด๋ฒ ๊ฒฐ๊ณผ๋ฌผ:** `docs/mov06_operations_guide.md ๋ฐ pipeline/mov06_retrain.py` ํํ์ ๊ฒฐ๊ณผ๋ฌผ์ ์์ฑํ๋ค.\n",
"\n",
"### ๐ ํ๊ฐ ๋ฐ ๊ฒ์ฆ ๊ธฐ์ค (PASS ์กฐ๊ฑด)\n",
"#### ๋ฏธ์
ํ์ \n",
"\n",
"**ํ๊ฐ ๊ตฌ๋ถ:** ํ์\n",
"\n",
"**ํต์ฌ ํ๊ฐ์์ญ:** ์ง์ ๊ฐ๋ฅํ ์ด์ ๊ฐ๋ฅ์ฑ ๋ฐ ๊ตญ๋ฐฉ ๊ตฐ์ ์ ๋ณด์ฒด๊ณ์์ ์ฐ๋ ์ฉ์ด์ฑ\n",
"\n",
"#### ๋ฏธ์
PASS ์กฐ๊ฑด\n",
"\n",
"- ์ ๋ ๊ธฐ์ค: ์๋ ์ฌํ์ต ํ์ดํ๋ผ์ธ ์ ์ ์คํ, ์ต์ข
๋ฌธ์ 100% ์๋น\n",
"\n",
"\n",
"- ์งํ ๊ธฐ์ค: ์ต์ข
์ธ๋๋ฌผ ๊ฒ์ ๋ฐ ํ๋ก์ ํธ ์๋ฃ ๋ฐํ์๋ฃ ์์ฑ\n",
"\n",
"\n",
"- ๊ฒ์ฆ ๋ฐฉ๋ฒ: End-to-End ์ ์ฒด ํ์ดํ๋ผ์ธ ์๋ํ ์คํ ๋ฐ ์ต์ข
๊ฒ์\n",
"\n",
"\n",
"#### ์ฑ์ ์ ํ์ ๋ฉ๋ชจ\n",
"\n",
"> ๋จ์ ์ฝ๋ ์คํ ์ฌ๋ถ๋ฟ๋ง ์๋๋ผ ์ถ๋ ฅ๋ ์์น์ ๊ทผ๊ฑฐ ์๋ฃ๊ฐ ๊ตฐ ์ค๋ฌด ์๊ตฌ์ฌํญ์ ๋ถํฉํ๋์ง ๋ฉด๋ฐํ ํ์ธํ๋ค.\n",
"\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"\"\"\"ํ
๋ ๋ฉํธ๋ฆฌ ๊ฐฑ์ , ์ ํฉ์ฑ ๊ฒ์ฆ, ๋๋ฆฌํํธ ์ ๊ฒ, ์๋ ์ฌํ์ต(์ฑํผ์ธ/์ฑ๋ฆฐ์ ), ์ ๋จ ETA ์์ธก๊ณผ ์ง์ฐยท์ดํยท์ ์ฐจ ๊ฒฝ๋ณด, ์น์ธ๋๊ธฐ ๋ถ๋ฆฌ, 7์ผ ๋ฉํ ์์ ์์ธก๊น์ง ํ ๋ฒ์ ์คํํ๋ ํตํฉ ํ์ดํ๋ผ์ธ์ ์์ฑํ๊ณ ์ด์ ๋ฌธ์ ์๋น์จยท์์ฒด ์ ๊ฒยท์ธํฐํ์ด์ค ๋ช
์ธยท๋ธ๋ฆฌํ ์์ฝ์ ์ถ๋ ฅํ๋ค.\"\"\"\n",
"import importlib\n",
"import json\n",
"import sys\n",
"import numpy as np\n",
"import pandas as pd\n",
"import matplotlib.pyplot as plt\n",
"from sklearn.ensemble import HistGradientBoostingRegressor\n",
"from sklearn.linear_model import Ridge\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# [๊ต์ก์ฉ๊ฐ์์๋ฃ] ๊ฐ์ ์์ก ๋
ธ์ ยท์์ก์ ๋จ ํ
๋ ๋ฉํธ๋ฆฌ ์์ฑ๊ธฐ (์ค์ ๋๋กยท์ง๋ช
ยท๋ถ๋ ์์)\n",
"# ๊ฐ์ ๋
ธ์ (์ถ๋ฐ ๊ฑฐ์ -> ๋์ฐฉ ๊ฑฐ์ , 5๊ฐ ๊ตฌ๊ฐ)์ 5๋ถ ๊ฐ๊ฒฉ์ผ๋ก ๋ณด๊ณ ํ๋ ์ ๋จ GPSยทํ๋ฌผ ์ผ์ยทํต์ ์ํ ์๋ฃ๋ฅผ ๋ง๋ ๋ค.\n",
"# ---------------------------------------------------------------------------\n",
"PTS = [(0, 0), (30, 12), (62, 14), (88, 38), (112, 42), (135, 66)] # ๋
ธ์ ๊ผญ์ง์ (km, ๊ฐ์ ์ขํ)\n",
"ROUTE = pd.DataFrame({\"seg\": [\"R1\", \"R2\", \"R3\", \"R4\", \"R5\"], \"road\": [\"paved\", \"unpaved\", \"paved\", \"paved\", \"unpaved\"],\n",
" \"limit\": [70, 50, 70, 50, 50], \"kind\": [\"์ผ๋ฐ\", \"์ผ๋ฐ\", \"์ผ๋ฐ\", \"๊ต๋ยทํฐ๋\", \"์ผ๋ฐ\"], \"threat\": [0, 0, 1, 0, 0]})\n",
"ROUTE[\"x0\"], ROUTE[\"y0\"] = [p[0] for p in PTS[:-1]], [p[1] for p in PTS[:-1]]\n",
"ROUTE[\"x1\"], ROUTE[\"y1\"] = [p[0] for p in PTS[1:]], [p[1] for p in PTS[1:]]\n",
"ROUTE[\"km\"] = np.hypot(ROUTE[\"x1\"] - ROUTE[\"x0\"], ROUTE[\"y1\"] - ROUTE[\"y0\"])\n",
"ROUTE[\"c0\"] = np.r_[0, np.cumsum(ROUTE[\"km\"])[:-1]]\n",
"ROUTE[\"c1\"] = np.cumsum(ROUTE[\"km\"])\n",
"LEN = float(ROUTE[\"km\"].sum())\n",
"ROAD_F = {\"paved\": 1.0, \"unpaved\": 0.75}\n",
"C1L, C0L, KML = ROUTE[\"c1\"].tolist(), ROUTE[\"c0\"].tolist(), ROUTE[\"km\"].tolist() # ๋ฐ๋ณต ๊ณ์ฐ์ฉ ๋ฆฌ์คํธ(์๋)\n",
"BASE_V = [l * ROAD_F[r] for l, r in zip(ROUTE[\"limit\"], ROUTE[\"road\"])]\n",
"THR, UNP, TUN = ROUTE[\"threat\"].tolist(), (ROUTE[\"road\"] == \"unpaved\").tolist(), (ROUTE[\"kind\"] == \"๊ต๋ยทํฐ๋\").tolist()\n",
"\n",
"\n",
"def seg_of(s):\n",
" # [TODO : ํ์ ์ค์ต 10] ๊ตฐ์ํ ์ด๋ ์ํ ์ถ์ ๋ฐ ๋์ฐฉ ์์ ์๊ฐ(ETA) ์์ธก ์์คํ
์ต์ข
์์ฑ ๊ด๋ จ ํต์ฌ ๋ก์ง์ ๊ตฌํํ์ธ์.\n",
" # ํํธ: ์๋จ ์ฃผ์ ๋ฐ ๋ฐ์ดํฐ ์คํค๋ง/์ง์นจ์ ์ฐธ๊ณ ํ์ฌ ์์ฑํฉ๋๋ค.\n",
" # <<< ์ฌ๊ธฐ์ ์ฝ๋๋ฅผ ์์ฑํ์ธ์ >>>\n",
" pass # ์์ฑ ํ pass๋ฅผ ์ ๊ฑฐํ์ธ์.\n",
"def route_xy(s):\n",
" \"\"\"๋
ธ์ ์ ์งํ๊ฑฐ๋ฆฌ s(km)์ ์ขํ์ ์งํ ๋ฐฉํฅ(๋).\"\"\"\n",
" # [TODO : ํ์ ์ค์ต 10] ๊ตฐ์ํ ์ด๋ ์ํ ์ถ์ ๋ฐ ๋์ฐฉ ์์ ์๊ฐ(ETA) ์์ธก ์์คํ
์ต์ข
์์ฑ ๊ด๋ จ ํต์ฌ ๋ก์ง์ ๊ตฌํํ์ธ์.\n",
" # ํํธ: ์๋จ ์ฃผ์ ๋ฐ ๋ฐ์ดํฐ ์คํค๋ง/์ง์นจ์ ์ฐธ๊ณ ํ์ฌ ์์ฑํฉ๋๋ค.\n",
" # <<< ์ฌ๊ธฐ์ ์ฝ๋๋ฅผ ์์ฑํ์ธ์ >>>\n",
" pass # ์์ฑ ํ pass๋ฅผ ์ ๊ฑฐํ์ธ์.\n",
"def project(x, y):\n",
" \"\"\"GPS ์ขํ๋ฅผ ๋
ธ์ ์ ํฌ์ํด (์งํ๊ฑฐ๋ฆฌ km, ๋
ธ์ ๊ณผ์ ์์ง ์ดํ km)๋ฅผ ๊ตฌํ๋ค.\"\"\"\n",
" x, y = np.atleast_1d(x).astype(float), np.atleast_1d(y).astype(float)\n",
" best_s, best_d = np.zeros(len(x)), np.full(len(x), np.inf)\n",
" for r in ROUTE.itertuples():\n",
" dx, dy = r.x1 - r.x0, r.y1 - r.y0\n",
" f = np.clip(((x - r.x0) * dx + (y - r.y0) * dy) / (dx * dx + dy * dy), 0, 1)\n",
" d = np.hypot(x - (r.x0 + f * dx), y - (r.y0 + f * dy))\n",
" better = d < best_d\n",
" best_s, best_d = np.where(better, r.c0 + f * r.km, best_s), np.where(better, d, best_d)\n",
" return best_s, best_d\n",
"\n",
"\n",
"def cong(clock_min):\n",
" h = (clock_min / 60) % 24\n",
" return 0.82 if (7 <= h < 9 or 17 <= h < 19) else 1.0 # ์ถํด๊ทผ ์๊ฐ๋ ์ ์ฒด ๊ณ์(๊ฐ์ )\n",
"\n",
"\n",
"def simulate_trip(rng, tid, start_min, rain, size, cargo):\n",
" \"\"\"1๋ถ ๋จ์๋ก ์ด๋์ ๋ชจ์ํ๊ณ 5๋ถ๋ง๋ค ํ
๋ ๋ฉํธ๋ฆฌ๋ฅผ ๋จ๊ธด๋ค. ์ดํยท๋ถ์ ์ ์ฐจ ์ด๋ฒคํธ๋ ์ ๋ต ๋ผ๋ฒจ๋ก ํจ๊ป ์ ์ฅํ๋ค.\"\"\"\n",
" s, t, stop_left, rows = 0.0, 0, 0, []\n",
" dev = (rng.random() < 0.12)\n",
" dev_t0 = int(rng.integers(30, 120)) if dev else -1\n",
" dev_len = int(rng.integers(12, 25)) if dev else 0\n",
" cause = \"\"\n",
" while s < LEN:\n",
" i = seg_of(s)\n",
" v = BASE_V[i] * (0.88 if rain else 1.0) * (0.85 if (rain and UNP[i]) else 1.0) * cong(start_min + t) \\\n",
" * (0.7 if THR[i] else 1.0) * (1 - 0.012 * size) * rng.normal(1, 0.06)\n",
" off = 0.0\n",
" if dev and dev_t0 <= t < dev_t0 + dev_len:\n",
" ph = (t - dev_t0) / dev_len\n",
" off, v = 4.0 * (1 - abs(2 * ph - 1)), v * 0.4 # ๋
ธ์ ์ดํ(์ผ๊ฐํ ํํ๋ก ์ต๋ 4km)\n",
" if stop_left > 0:\n",
" v, stop_left = 0.0, stop_left - 1\n",
" elif rng.random() < 0.0015:\n",
" stop_left, cause = int(rng.integers(8, 36)), (\"maint\" if rng.random() < 0.7 else \"urgent\")\n",
" s += v / 60\n",
" t += 1\n",
" if t % 5 == 0 or s >= LEN:\n",
" x, y, hd = route_xy(min(s, LEN))\n",
" nx, ny = np.cos(np.radians(hd)), -np.sin(np.radians(hd))\n",
" j = seg_of(min(s, LEN)); tun = TUN[j]\n",
" rows.append(dict(trip=tid, t=t, clock=(start_min + t) % 1440, x=x + off * nx + rng.normal(0, 0.03), y=y + off * ny + rng.normal(0, 0.03),\n",
" speed=max(v * rng.normal(1, 0.03), 0.0), heading=(hd + rng.normal(0, 3)) % 360, rssi=-62 - 14 * tun + rng.normal(0, 3),\n",
" cargo_temp=18 + 3 * np.sin(t / 40) + rng.normal(0, 0.3), shock_g=abs(rng.normal(0.25, 0.1)) * (2.0 if UNP[j] else 1.0),\n",
" s_true=min(s, LEN), off_true=off, stopped_true=int(v == 0.0), rain=int(rain), size=size, cargo=cargo, stop_cause=cause if v == 0.0 else \"\"))\n",
" arr = t\n",
" for r in rows:\n",
" r[\"eta_true\"] = arr - r[\"t\"]\n",
" return rows\n",
"\n",
"\n",
"def make_telemetry(seed=21, n_trips=200):\n",
" \"\"\"n_trips๊ฐ ์์ก์ ๋จ ์ด๋ ๊ธฐ๋ก(๋ ์ง ์). ์ ๋จ ๊ท๋ชจ 3~12๋, ํ๋ฌผ ๊ตฌ๋ถ ์ผ๋ฐ/๋์ฅ/๋ฏผ๊ฐ.\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" rows = []\n",
" for k in range(n_trips):\n",
" rows += simulate_trip(rng, k, int(rng.integers(5 * 60, 15 * 60)), bool(rng.random() < 0.2), int(rng.integers(3, 13)), str(rng.choice([\"์ผ๋ฐ\", \"๋์ฅ\", \"๋ฏผ๊ฐ\"], p=[.6, .25, .15])))\n",
" return pd.DataFrame(rows)\n",
"\n",
"\n",
"def mape(y, p):\n",
" y, p = np.asarray(y, float), np.asarray(p, float)\n",
" return float(np.mean(np.abs(y - p) / np.abs(y)) * 100)\n",
"\n",
"\n",
"def mae(y, p):\n",
" return float(np.mean(np.abs(np.asarray(y, float) - np.asarray(p, float))))\n",
"# ---------------------------------------------------------------------------\n",
"# ---------------------------------------------------------------------------\n",
"# ETA ์์ธก ํผ์ฒ: ๊ด์ธก ์์ ๊น์ง์ ํ
๋ ๋ฉํธ๋ฆฌ์ ๋จ์ ๋
ธ์ ๊ตฌ์ฑ(ํฌ์ฅยท์ํยท๊ต๋ํฐ๋ ๊ตฌ๊ฐ ๊ฑฐ๋ฆฌ)๋ง ์ฌ์ฉ\n",
"# ---------------------------------------------------------------------------\n",
"FEATS = [\"remaining_km\", \"progress\", \"speed\", \"speed_ma3\", \"speed_std3\", \"stop_n\", \"stop_min\", \"since_stop\", \"hour_sin\", \"hour_cos\", \"cong\", \"rain\", \"size\",\n",
" \"k_๋์ฅ\", \"k_๋ฏผ๊ฐ\", \"rem_unpaved_km\", \"rem_threat_km\", \"rem_bridge_km\", \"off_km\", \"rssi\", \"base_eta\"]\n",
"\n",
"\n",
"def rem_composition(s):\n",
" \"\"\"์งํ๊ฑฐ๋ฆฌ s ์ดํ ๋จ์ ๊ตฌ๊ฐ ์ค ๋นํฌ์ฅยท์ํยท๊ต๋ํฐ๋ ๊ฑฐ๋ฆฌ(km).\"\"\"\n",
" left = np.clip(ROUTE[\"c1\"].values[None, :] - np.maximum(np.atleast_1d(s)[:, None], ROUTE[\"c0\"].values[None, :]), 0, None)\n",
" return left @ np.array(UNP, float), left @ np.array(THR, float), left @ np.array(TUN, float)\n",
"\n",
"\n",
"def add_features(df):\n",
" d = df.sort_values([\"trip\", \"t\"]).copy()\n",
" d[\"s_obs\"], d[\"off_km\"] = project(d[\"x\"].values, d[\"y\"].values)\n",
" d[\"remaining_km\"] = LEN - d[\"s_obs\"]\n",
" d[\"progress\"] = d[\"s_obs\"] / LEN\n",
" g = d.groupby(\"trip\")[\"speed\"]\n",
" d[\"speed_ma3\"] = g.rolling(3, min_periods=1).mean().reset_index(level=0, drop=True)\n",
" d[\"speed_std3\"] = g.rolling(3, min_periods=1).std().reset_index(level=0, drop=True).fillna(0)\n",
" d[\"is_stop\"] = (d[\"speed\"] < 3).astype(int)\n",
" new_stop = (d[\"is_stop\"] == 1) & (d.groupby(\"trip\")[\"is_stop\"].shift(1).fillna(0) == 0)\n",
" d[\"stop_n\"] = new_stop.astype(int).groupby(d[\"trip\"]).cumsum()\n",
" d[\"stop_min\"] = d.groupby(\"trip\")[\"is_stop\"].cumsum() * 5\n",
" blk = (d[\"is_stop\"] == 1).astype(int).groupby(d[\"trip\"]).cumsum() # ์ ์ฐจ ๋์ ํ์๋ก ๊ตฌ๊ฐ์ ๋๋์ด ๋ง์ง๋ง ์ ์ฐจ ์ดํ ๊ฒฝ๊ณผ ๊ณ์ฐ\n",
" d[\"since_stop\"] = d.groupby([\"trip\", blk]).cumcount() * 5\n",
" d[\"hour_sin\"], d[\"hour_cos\"] = np.sin(2 * np.pi * d[\"clock\"] / 1440), np.cos(2 * np.pi * d[\"clock\"] / 1440)\n",
" d[\"cong\"] = d[\"clock\"].map(cong)\n",
" d[\"k_๋์ฅ\"], d[\"k_๋ฏผ๊ฐ\"] = (d[\"cargo\"] == \"๋์ฅ\").astype(int), (d[\"cargo\"] == \"๋ฏผ๊ฐ\").astype(int)\n",
" d[\"rem_unpaved_km\"], d[\"rem_threat_km\"], d[\"rem_bridge_km\"] = rem_composition(d[\"s_obs\"].values)\n",
" d[\"base_eta\"] = np.minimum(d[\"remaining_km\"] / np.maximum(d[\"speed\"], 5.0) * 60, 600) # ํ์ฌ ์๋ ๊ธฐ๋ฐ ๋จ์ ETA(๋ถ, ์ ์ฐจ ์ ํญ์ฃผ ๋ฐฉ์ง๋ก 5km/h ํํยท600๋ถ ์ํ)\n",
" return d.reset_index(drop=True)\n",
"# ---------------------------------------------------------------------------\n",
"\n",
"libs = [\"numpy\", \"pandas\", \"scipy\", \"sklearn\", \"matplotlib\"]\n",
"env_ok = sum(importlib.util.find_spec(m) is not None for m in libs)\n",
"print(f\"[0] ํ๊ฒฝ ์ ๊ฒ: Python {sys.version_info.major}.{sys.version_info.minor}, ํ์ ๋ชจ๋ {env_ok}/{len(libs)}\")\n",
"\n",
"# ---------------- 1) ๋ฐ์ดํฐ ๊ฐฑ์ ยท์ ํฉ์ฑ ๊ฒ์ฆ(๋ฏธ์
1~2 ์์ฝ) ----------------\n",
"raw = make_telemetry(n_trips=240) # ์ด๋ 0~199 ๊ธฐ์กด + 200~239 ์ ๊ท ์
๋ ฅ๋ถ\n",
"d = add_features(raw)\n",
"chk = {\"(์ด๋,์๊ฐ) ์ ์ผ\": not raw.duplicated([\"trip\", \"t\"]).any(), \"๊ฒฐ์ธก 0\": int(raw.isna().sum().sum()) == 0,\n",
" \"์ขํ ๋ฒ์(๊ฐ์ ์์ญ)\": bool(raw.x.between(-10, 150).all() and raw.y.between(-10, 80).all()), \"์๋ 0~120km/h\": bool(raw.speed.between(0, 120).all()),\n",
" \"๋ฐฉํฅ 0~360\": bool(raw.heading.between(0, 360).all()), \"ETA ๋น์์\": bool((raw.eta_true >= 0).all())}\n",
"print(f\"[1] ์ ๊ท 40๊ฐ ์ด๋ ํฌํจ {len(raw)}ํ({raw.trip.nunique()}๊ฐ ์ด๋) ์ ํฉ์ฑ ๊ฒ์ฆ {sum(chk.values())}/{len(chk)} ํต๊ณผ\")\n",
"\n",
"# ---------------- 2) ์๋ ์ฌํ์ต: ๋๋ฆฌํํธ ์ ๊ฒ -> ์ฑ๋ฆฐ์ ํ์ต -> ์น๊ฒฉ ํ๋จ ----------------\n",
"def fit(tr):\n",
" return HistGradientBoostingRegressor(max_iter=150, max_depth=4, learning_rate=0.08, random_state=0).fit(tr[FEATS], tr[\"eta_true\"])\n",
"\n",
"\n",
"def evaluate(m, te):\n",
" te = te[te.eta_true >= 15]\n",
" p = m.predict(te[FEATS]).clip(min=0)\n",
" return mae(te[\"eta_true\"], p), mape(te[\"eta_true\"], p)\n",
"\n",
"\n",
"def retrain_pipeline(old_end, new_end, hold_n=10, drift_mape=8.0, min_gain=0.03):\n",
" champ = fit(d[d.trip < old_end])\n",
" new = d[(d.trip >= old_end) & (d.trip < new_end)]\n",
" ch_mae, ch_mape = evaluate(champ, new)\n",
" hold = d[(d.trip >= new_end - hold_n) & (d.trip < new_end)]\n",
" chall = fit(d[d.trip < new_end - hold_n])\n",
" a, b = evaluate(champ, hold)[0], evaluate(chall, hold)[0]\n",
" promote = (a - b) / a >= min_gain\n",
" return {\"champ_mape\": ch_mape, \"drift\": ch_mape > drift_mape, \"champ_mae\": a, \"chall_mae\": b, \"promote\": promote, \"model\": chall if promote else champ}\n",
"\n",
"\n",
"rt = retrain_pipeline(160, 240)\n",
"print(f\"[2] ์ฌํ์ต ํ์ดํ๋ผ์ธ: ๊ธฐ์กด ๋ชจ๋ธ์ ์ ๊ท 80๊ฐ ์ด๋ MAPE {rt['champ_mape']:.2f}% (๋๋ฆฌํํธ ๊ธฐ์ค 8.0%: {'์ด๊ณผ' if rt['drift'] else '์ด๋ด'})\")\n",
"print(f\" ๊ฒ์ฆ 10๊ฐ ์ด๋ MAE ์ฑํผ์ธ {rt['champ_mae']:.2f}๋ถ vs ์ฑ๋ฆฐ์ {rt['chall_mae']:.2f}๋ถ -> {'์ฑ๋ฆฐ์ ์น๊ฒฉ' if rt['promote'] else '๊ธฐ์กด ๋ชจ๋ธ ์ ์ง'}(๊ฐ์ 3% ์ด์์ผ ๋๋ง ์น๊ฒฉ)\")\n",
"model = rt[\"model\"]\n",
"\n",
"# ---------------- 3) ์ ๋จ ETA ์์ธก๊ณผ ๊ฒฝ๋ณด(์น์ธ๋๊ธฐ ๋ถ๋ฆฌ) ----------------\n",
"PLAN = 255\n",
"live = d[(d.trip >= 230) & (d.t == 120) & (d.eta_true > 0)].copy()\n",
"live[\"eta_pred\"] = model.predict(live[FEATS]).clip(min=0)\n",
"live[\"๊ณํ ๋๋น(๋ถ)\"] = live[\"t\"] + live[\"eta_pred\"] - PLAN\n",
"live[\"is_stop\"] = (live[\"speed\"] < 3).astype(int)\n",
"live[\"๊ฒฝ๋ณด\"] = np.select([live[\"off_km\"] >= 1, live[\"is_stop\"] == 1, live[\"๊ณํ ๋๋น(๋ถ)\"] >= 20], [\"์ดํ ์์ฌ\", \"์ ์ฐจ\", \"์ง์ฐ 20๋ถ ์ด์\"], \"์ ์\")\n",
"live[\"์ฒ๋ฆฌ\"] = np.where(live[\"๊ฒฝ๋ณด\"] != \"์ ์\", \"๊ด์ ์ฌ ํ์ธ ํ ์์กํต์ ๊ด ์น์ธ๋๊ธฐ\", \"์ถ์ ์ ์ง\")\n",
"print(f\"\\n[3] ์ถ๋ฐ ํ 120๋ถ ์ ๋จ {len(live)}๊ฐ ์ํํ(์ดํ๊ณํ {PLAN}๋ถ) [๊ต์ก์ฉ๊ฐ์์๋ฃ]\")\n",
"print(live[[\"trip\", \"progress\", \"speed\", \"eta_pred\", \"๊ณํ ๋๋น(๋ถ)\", \"๊ฒฝ๋ณด\", \"์ฒ๋ฆฌ\"]].rename(columns={\"progress\": \"์งํ๋ฅ \", \"speed\": \"์๋\", \"eta_pred\": \"ETA(๋ถ)\"}).round(1).to_string(index=False))\n",
"pend = live[live[\"์ฒ๋ฆฌ\"].str.contains(\"์น์ธ๋๊ธฐ\")]\n",
"print(f\" ๊ฒฝ๋ณด {len(pend)}๊ฑด์ ์๋์ผ๋ก ๋์ฒด ์์ก์ ์ง์ํ์ง ์๊ณ ์น์ธ๋๊ธฐ๋ก ๋ถ๋ฆฌํ๋ค(๋ฏธ์
7).\")\n",
"\n",
"# ---------------- 4) 7์ผ ๋ฉํ ์์ ์์ธก(์์ผ Ridge vs ์ ์ฃผ ๋์์ผ Baseline) ----------------\n",
"rg_ = np.random.default_rng(31)\n",
"days = np.arange(180)\n",
"dem = (40 + 6 * np.sin(2 * np.pi * days / 7) + 0.05 * days + np.where((days >= 120) & (days < 130), 12, 0) + rg_.normal(0, 2, len(days))).round() # ์ผ ๋ฉํ ์์ก ์์(๊ฐ์, ๋)\n",
"\n",
"\n",
"def design(dd):\n",
" dd = np.asarray(dd)\n",
" return np.column_stack([np.sin(2 * np.pi * dd / 7), np.cos(2 * np.pi * dd / 7), dd / 100, np.ones(len(dd)), ((dd >= 120) & (dd < 130)).astype(float)])\n",
"\n",
"\n",
"tr_d, te_d = days[:158], days[158:]\n",
"rg = Ridge(alpha=0.1, fit_intercept=False).fit(design(tr_d), dem[tr_d])\n",
"f_r, f_n = rg.predict(design(te_d)), dem[te_d - 7]\n",
"print(f\"\\n[4] ๋ฉํ ์์ ์์ธก ๊ฒ์ฆ(day 158~179, 22์ผ): Ridge MAPE {mape(dem[te_d], f_r):.1f}% vs ์ ์ฃผ ๋์์ผ {mape(dem[te_d], f_n):.1f}%\")\n",
"nxt = np.arange(180, 187)\n",
"rg = Ridge(alpha=0.1, fit_intercept=False).fit(design(days), dem)\n",
"fc = rg.predict(design(nxt))\n",
"print(\" ํฅํ 7์ผ ๋ฉํ ์์ ์์ธก(๋):\", \", \".join(f\"{int(v)}\" for v in fc), \"-> ์ ๋จ ํธ์ฑ ๊ณํ ์
๋ ฅ(๊ฒํ ํ ์ฌ์ฉ)\")\n",
"\n",
"# ---------------- 5) ์ด์ ๋ฌธ์ ์๋น์จยท์ธํฐํ์ด์ค ๋ช
์ธ ----------------\n",
"docs = {\"๋ฐ์ดํฐ ์ฌ์ ยท์คํค๋ง(M1)\": 24, \"์ ์ ๊ท์น 10์ข
(M2)\": 10, \"BaselineยทEDA(M3)\": 8, \"ETA ๋ชจ๋ธยทํผ์ฒ(M4)\": 21, \"์ดํยท์ ์ฐจ ๊ฒฝ๋ณด(M5)\": 3, \"๋์๋ณด๋ยท์ค๋ช
(M6)\": 4,\n",
" \"์น์ธ ์ ์ฐจยท๊ถํ(M7)\": 4, \"๋ถํยท์ค๋ฅ ์ํ(M8)\": 7, \"AARยทROI(M9)\": 3, \"์ด์ยท์ฌํ์ต ์ ์ฐจ(M10)\": 6, \"์ฅ์ ๋์\": 5, \"์ธํฐํ์ด์ค ๊ฐ์ด๋\": 4}\n",
"done = sum(v > 0 for v in docs.values())\n",
"iface = {\"input\": {\"telemetry\": [\"trip:int\", \"t:int\", \"x:float\", \"y:float\", \"speed:float\", \"heading:float\", \"rssi:float\"], \"format\": \"json-lines\"},\n",
" \"output\": {\"eta\": [\"trip\", \"eta_min\", \"plan_gap_min\", \"alarm\", \"status\"], \"approval\": \"PENDING_ONLY\"}, \"auth\": \"role-based(๊ฐ์)\", \"note\": \"์์ ๊ตฐ์์ ๋ณด์ฒด๊ณ๋ ๊ฐ์ ์ธํฐํ์ด์ค๋ก๋ง ์ ์\"}\n",
"print(f\"\\n[5] ์ด์ ๋ฌธ์ {len(docs)}๊ฐ ํญ๋ชฉ ์ค ์์ฑ ์๋ฃ {done}๊ฐ ({done / len(docs):.0%}) / ์ธํฐํ์ด์ค ๋ช
์ธ(JSON):\")\n",
"print(\" \", json.dumps(iface, ensure_ascii=False))\n",
"\n",
"# ---------------- 6) ์์ฒด ์ ๊ฒยท๋ธ๋ฆฌํ ----------------\n",
"te = d[(d.trip >= 200)]\n",
"fin_mae, fin_mape = evaluate(model, te)\n",
"tt = te[te.eta_true >= 15]\n",
"base_mae, base_mape = mae(tt.eta_true, tt.base_eta), mape(tt.eta_true, tt.base_eta)\n",
"checks = [(\"ํ๊ฒฝ ์ ๊ฒ ํต๊ณผ\", env_ok == len(libs)), (\"๋ฐ์ดํฐ ์ ํฉ์ฑ ์ ํญ๋ชฉ ํต๊ณผ\", all(chk.values())), (\"์ฌํ์ต ํ์ดํ๋ผ์ธ ์ ์ ์คํ(์ฑํผ์ธ/์ฑ๋ฆฐ์ ํ์ ์ถ๋ ฅ)\", rt[\"champ_mae\"] > 0),\n",
" (\"ETA MAPE 25% ์ดํ\", fin_mape <= 25), (\"๋จ์ ETA ๋๋น MAE 20% ์ด์ ๊ฐ์ \", fin_mae <= 0.8 * base_mae),\n",
" (\"7์ผ ์์ ์์ธก์ด ์ ์ฃผ ๋์์ผ Baseline๋ณด๋ค ์ ํ\", mape(dem[te_d], f_r) < mape(dem[te_d], f_n)),\n",
" (\"๊ฒฝ๋ณด ๊ฑด ์๋ ์ง์ 0๊ฑด(์น์ธ๋๊ธฐ ๋ถ๋ฆฌ)\", bool((pend[\"์ฒ๋ฆฌ\"].str.contains(\"์น์ธ๋๊ธฐ\")).all())), (\"์ด์ ๋ฌธ์ 100% ์๋น\", done == len(docs))]\n",
"print(\"\\n[6] ์์ฒด ์ ๊ฒ\")\n",
"for n, ok in checks:\n",
" print(f\" [{'PASS' if ok else 'FAIL'}] {n}\")\n",
"print(\"\\n[7] ๋ธ๋ฆฌํ ์์ฝ\")\n",
"print(f\" - ์ ๊ท ์ด๋ 40๊ฐ๋ฅผ ํฌํจํด ์ฌํ์ต ํ๋จ์ ์๋ ์ํํ๋ค(๋๋ฆฌํํธ {'๊ฐ์ง' if rt['drift'] else '๋ฏธ๊ฐ์ง'}, {'์ฑ๋ฆฐ์ ์น๊ฒฉ' if rt['promote'] else '๊ธฐ์กด ๋ชจ๋ธ ์ ์ง'}).\")\n",
"print(f\" - ์ ๊ท ๊ตฌ๊ฐ ETA MAPE {base_mape:.1f}%(๋จ์ ETA) -> {fin_mape:.1f}%, MAE {base_mae:.1f}๋ถ -> {fin_mae:.1f}๋ถ. 7์ผ ๋ฉํ ์์ ์์ธก MAPE {mape(dem[te_d], f_r):.1f}%.\")\n",
"print(f\" - 120๋ถ ์์ ์ ๋จ {len(live)}๊ฐ ์ค ๊ฒฝ๋ณด {len(pend)}๊ฑด์ ์น์ธ๋๊ธฐ๋ก ๋ถ๋ฆฌํ๋ค. ์ค์ ๋์ฒด ์์ก์ ์์กํต์ ๊ด ๊ฒฐ์ฌ์ ์งํ๊ณํต ๊ฒฐ์ฌ์ ๋ฐ๋ฅธ๋ค.\")\n",
"\n",
"fig, ax = plt.subplots(1, 2, figsize=(12, 4))\n",
"ax[0].scatter(tt.eta_true, tt.base_eta, s=4, alpha=.3, label=\"simple ETA\"); ax[0].scatter(tt.eta_true, model.predict(tt[FEATS]), s=4, alpha=.3, label=\"model\"); ax[0].plot([0, 300], [0, 300], \"k--\"); ax[0].legend(); ax[0].set_title(\"New trips: ETA actual vs predicted (min)\")\n",
"ax[1].plot(days, dem, label=\"actual\"); ax[1].plot(nxt, fc, \"r--o\", label=\"forecast\"); ax[1].legend(); ax[1].set_title(\"Daily delivery transport demand and 7-day forecast\")\n",
"plt.suptitle(\"MOV06 integrated pipeline (synthetic)\"); plt.tight_layout(); plt.show()\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"๐ก [์ฐธ๊ณ ] ๋ฏธ์
10 ์์ ์คํ ๊ฒฐ๊ณผ (Output)
\n",
"\n",
"```text\n",
"[0] ํ๊ฒฝ ์ ๊ฒ: Python 3.11, ํ์ ๋ชจ๋ 5/5\n",
"[1] ์ ๊ท 40๊ฐ ์ด๋ ํฌํจ 12364ํ(240๊ฐ ์ด๋) ์ ํฉ์ฑ ๊ฒ์ฆ 6/6 ํต๊ณผ\n",
"[2] ์ฌํ์ต ํ์ดํ๋ผ์ธ: ๊ธฐ์กด ๋ชจ๋ธ์ ์ ๊ท 80๊ฐ ์ด๋ MAPE 6.22% (๋๋ฆฌํํธ ๊ธฐ์ค 8.0%: ์ด๋ด)\n",
" ๊ฒ์ฆ 10๊ฐ ์ด๋ MAE ์ฑํผ์ธ 4.81๋ถ vs ์ฑ๋ฆฐ์ 4.27๋ถ -> ์ฑ๋ฆฐ์ ์น๊ฒฉ(๊ฐ์ 3% ์ด์์ผ ๋๋ง ์น๊ฒฉ)\n",
"\n",
"[3] ์ถ๋ฐ ํ 120๋ถ ์ ๋จ 10๊ฐ ์ํํ(์ดํ๊ณํ 255๋ถ) [๊ต์ก์ฉ๊ฐ์์๋ฃ]\n",
" trip ์งํ๋ฅ ์๋ ETA(๋ถ) ๊ณํ ๋๋น(๋ถ) ๊ฒฝ๋ณด ์ฒ๋ฆฌ\n",
" 230 0.6 40.6 117.0 -18.0 ์ ์ ์ถ์ ์ ์ง\n",
" 231 0.5 42.2 121.4 -13.6 ์ ์ ์ถ์ ์ ์ง\n",
" 232 0.5 44.5 123.2 -11.8 ์ ์ ์ถ์ ์ ์ง\n",
" 233 0.6 43.3 116.0 -19.0 ์ ์ ์ถ์ ์ ์ง\n",
" 234 0.5 40.6 149.1 14.1 ์ ์ ์ถ์ ์ ์ง\n",
" 235 0.5 45.6 122.4 -12.6 ์ ์ ์ถ์ ์ ์ง\n",
" 236 0.6 41.8 113.1 -21.9 ์ ์ ์ถ์ ์ ์ง\n",
" 237 0.4 35.2 169.5 34.5 ์ง์ฐ 20๋ถ ์ด์ ๊ด์ ์ฌ ํ์ธ ํ ์์กํต์ ๊ด ์น์ธ๋๊ธฐ\n",
" 238 0.6 37.0 117.3 -17.7 ์ ์ ์ถ์ ์ ์ง\n",
" 239 0.5 40.7 118.9 -16.1 ์ ์ ์ถ์ ์ ์ง\n",
" ๊ฒฝ๋ณด 1๊ฑด์ ์๋์ผ๋ก ๋์ฒด ์์ก์ ์ง์ํ์ง ์๊ณ ์น์ธ๋๊ธฐ๋ก ๋ถ๋ฆฌํ๋ค(๋ฏธ์
7).\n",
"\n",
"[4] ๋ฉํ ์์ ์์ธก ๊ฒ์ฆ(day 158~179, 22์ผ): Ridge MAPE 3.6% vs ์ ์ฃผ ๋์์ผ 4.5%\n",
" ํฅํ 7์ผ ๋ฉํ ์์ ์์ธก(๋): 43, 44, 49, 54, 55, 51, 46 -> ์ ๋จ ํธ์ฑ ๊ณํ ์
๋ ฅ(๊ฒํ ํ ์ฌ์ฉ)\n",
"\n",
"[5] ์ด์ ๋ฌธ์ 12๊ฐ ํญ๋ชฉ ์ค ์์ฑ ์๋ฃ 12๊ฐ (100%) / ์ธํฐํ์ด์ค ๋ช
์ธ(JSON):\n",
" {\"input\": {\"telemetry\": [\"trip:int\", \"t:int\", \"x:float\", \"y:float\", \"speed:float\", \"heading:float\", \"rssi:float\"], \"format\": \"json-lines\"}, \"output\": {\"eta\": [\"trip\", \"eta_min\", \"plan_gap_min\", \"alarm\", \"status\"], \"approval\": \"PENDING_ONLY\"}, \"auth\": \"role-based(๊ฐ์)\", \"note\": \"์์ ๊ตฐ์์ ๋ณด์ฒด๊ณ๋ ๊ฐ์ ์ธํฐํ์ด์ค๋ก๋ง ์ ์\"}\n",
"\n",
"[6] ์์ฒด ์ ๊ฒ\n",
" [PASS] ํ๊ฒฝ ์ ๊ฒ ํต๊ณผ\n",
" [PASS] ๋ฐ์ดํฐ ์ ํฉ์ฑ ์ ํญ๋ชฉ ํต๊ณผ\n",
" [PASS] ์ฌํ์ต ํ์ดํ๋ผ์ธ ์ ์ ์คํ(์ฑํผ์ธ/์ฑ๋ฆฐ์ ํ์ ์ถ๋ ฅ)\n",
" [PASS] ETA MAPE 25% ์ดํ\n",
" [PASS] ๋จ์ ETA ๋๋น MAE 20% ์ด์ ๊ฐ์ \n",
" [PASS] 7์ผ ์์ ์์ธก์ด ์ ์ฃผ ๋์์ผ Baseline๋ณด๋ค ์ ํ\n",
" [PASS] ๊ฒฝ๋ณด ๊ฑด ์๋ ์ง์ 0๊ฑด(์น์ธ๋๊ธฐ ๋ถ๋ฆฌ)\n",
" [PASS] ์ด์ ๋ฌธ์ 100% ์๋น\n",
"\n",
"[7] ๋ธ๋ฆฌํ ์์ฝ\n",
" - ์ ๊ท ์ด๋ 40๊ฐ๋ฅผ ํฌํจํด ์ฌํ์ต ํ๋จ์ ์๋ ์ํํ๋ค(๋๋ฆฌํํธ ๋ฏธ๊ฐ์ง, ์ฑ๋ฆฐ์ ์น๊ฒฉ).\n",
" - ์ ๊ท ๊ตฌ๊ฐ ETA MAPE 21.6%(๋จ์ ETA) -> 3.8%, MAE 33.7๋ถ -> 4.6๋ถ. 7์ผ ๋ฉํ ์์ ์์ธก MAPE 3.6%.\n",
" - 120๋ถ ์์ ์ ๋จ 10๊ฐ ์ค ๊ฒฝ๋ณด 1๊ฑด์ ์น์ธ๋๊ธฐ๋ก ๋ถ๋ฆฌํ๋ค. ์ค์ ๋์ฒด ์์ก์ ์์กํต์ ๊ด ๊ฒฐ์ฌ์ ์งํ๊ณํต ๊ฒฐ์ฌ์ ๋ฐ๋ฅธ๋ค.\n",
"```\n",
" \n"
]
}
],
"metadata": {
"colab": {
"provenance": []
},
"kernelspec": {
"display_name": "Python 3",
"name": "python3"
},
"language_info": {
"name": "python"
}
},
"nbformat": 4,
"nbformat_minor": 0
}