{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# ๐ŸŽฏ [MOV06] ๋‚ฉํ’ˆ์ˆ˜์š” ์˜ˆ์ธกยท๋ฌผ๋ฅ˜ํ๋ฆ„ ์ถ”์ ๊ด€๋ฆฌ\n", "### Google Colab ์‹ค์Šต ํ™˜๊ฒฝ (์ˆ˜์†กยท๋ฌผ๋ฅ˜ยท์ฐฝ๊ณ  / Level 3)\n", "\n", "[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/edunlab/milicontent2/blob/main/uploads/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", " for i, c in enumerate(C1L):\n", " if s < c:\n", " return i\n", " return len(C1L) - 1\n", "\n", "\n", "def route_xy(s):\n", " \"\"\"๋…ธ์„  ์œ„ ์ง„ํ–‰๊ฑฐ๋ฆฌ s(km)์˜ ์ขŒํ‘œ์™€ ์ง„ํ–‰ ๋ฐฉํ–ฅ(๋„).\"\"\"\n", " i = seg_of(s)\n", " f = (s - C0L[i]) / KML[i]\n", " (x0, y0), (x1, y1) = PTS[i], PTS[i + 1]\n", " return x0 + f * (x1 - x0), y0 + f * (y1 - y0), float(np.degrees(np.arctan2(x1 - x0, y1 - y0)) % 360)\n", "\n", "\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", " for i, c in enumerate(C1L):\n", " if s < c:\n", " return i\n", " return len(C1L) - 1\n", "\n", "\n", "def route_xy(s):\n", " \"\"\"๋…ธ์„  ์œ„ ์ง„ํ–‰๊ฑฐ๋ฆฌ s(km)์˜ ์ขŒํ‘œ์™€ ์ง„ํ–‰ ๋ฐฉํ–ฅ(๋„).\"\"\"\n", " i = seg_of(s)\n", " f = (s - C0L[i]) / KML[i]\n", " (x0, y0), (x1, y1) = PTS[i], PTS[i + 1]\n", " return x0 + f * (x1 - x0), y0 + f * (y1 - y0), float(np.degrees(np.arctan2(x1 - x0, y1 - y0)) % 360)\n", "\n", "\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", " for i, c in enumerate(C1L):\n", " if s < c:\n", " return i\n", " return len(C1L) - 1\n", "\n", "\n", "def route_xy(s):\n", " \"\"\"๋…ธ์„  ์œ„ ์ง„ํ–‰๊ฑฐ๋ฆฌ s(km)์˜ ์ขŒํ‘œ์™€ ์ง„ํ–‰ ๋ฐฉํ–ฅ(๋„).\"\"\"\n", " i = seg_of(s)\n", " f = (s - C0L[i]) / KML[i]\n", " (x0, y0), (x1, y1) = PTS[i], PTS[i + 1]\n", " return x0 + f * (x1 - x0), y0 + f * (y1 - y0), float(np.degrees(np.arctan2(x1 - x0, y1 - y0)) % 360)\n", "\n", "\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", " for i, c in enumerate(C1L):\n", " if s < c:\n", " return i\n", " return len(C1L) - 1\n", "\n", "\n", "def route_xy(s):\n", " \"\"\"๋…ธ์„  ์œ„ ์ง„ํ–‰๊ฑฐ๋ฆฌ s(km)์˜ ์ขŒํ‘œ์™€ ์ง„ํ–‰ ๋ฐฉํ–ฅ(๋„).\"\"\"\n", " i = seg_of(s)\n", " f = (s - C0L[i]) / KML[i]\n", " (x0, y0), (x1, y1) = PTS[i], PTS[i + 1]\n", " return x0 + f * (x1 - x0), y0 + f * (y1 - y0), float(np.degrees(np.arctan2(x1 - x0, y1 - y0)) % 360)\n", "\n", "\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", " for i, c in enumerate(C1L):\n", " if s < c:\n", " return i\n", " return len(C1L) - 1\n", "\n", "\n", "def route_xy(s):\n", " \"\"\"๋…ธ์„  ์œ„ ์ง„ํ–‰๊ฑฐ๋ฆฌ s(km)์˜ ์ขŒํ‘œ์™€ ์ง„ํ–‰ ๋ฐฉํ–ฅ(๋„).\"\"\"\n", " i = seg_of(s)\n", " f = (s - C0L[i]) / KML[i]\n", " (x0, y0), (x1, y1) = PTS[i], PTS[i + 1]\n", " return x0 + f * (x1 - x0), y0 + f * (y1 - y0), float(np.degrees(np.arctan2(x1 - x0, y1 - y0)) % 360)\n", "\n", "\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", " for i, c in enumerate(C1L):\n", " if s < c:\n", " return i\n", " return len(C1L) - 1\n", "\n", "\n", "def route_xy(s):\n", " \"\"\"๋…ธ์„  ์œ„ ์ง„ํ–‰๊ฑฐ๋ฆฌ s(km)์˜ ์ขŒํ‘œ์™€ ์ง„ํ–‰ ๋ฐฉํ–ฅ(๋„).\"\"\"\n", " i = seg_of(s)\n", " f = (s - C0L[i]) / KML[i]\n", " (x0, y0), (x1, y1) = PTS[i], PTS[i + 1]\n", " return x0 + f * (x1 - x0), y0 + f * (y1 - y0), float(np.degrees(np.arctan2(x1 - x0, y1 - y0)) % 360)\n", "\n", "\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", " for i, c in enumerate(C1L):\n", " if s < c:\n", " return i\n", " return len(C1L) - 1\n", "\n", "\n", "def route_xy(s):\n", " \"\"\"๋…ธ์„  ์œ„ ์ง„ํ–‰๊ฑฐ๋ฆฌ s(km)์˜ ์ขŒํ‘œ์™€ ์ง„ํ–‰ ๋ฐฉํ–ฅ(๋„).\"\"\"\n", " i = seg_of(s)\n", " f = (s - C0L[i]) / KML[i]\n", " (x0, y0), (x1, y1) = PTS[i], PTS[i + 1]\n", " return x0 + f * (x1 - x0), y0 + f * (y1 - y0), float(np.degrees(np.arctan2(x1 - x0, y1 - y0)) % 360)\n", "\n", "\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", " for i, c in enumerate(C1L):\n", " if s < c:\n", " return i\n", " return len(C1L) - 1\n", "\n", "\n", "def route_xy(s):\n", " \"\"\"๋…ธ์„  ์œ„ ์ง„ํ–‰๊ฑฐ๋ฆฌ s(km)์˜ ์ขŒํ‘œ์™€ ์ง„ํ–‰ ๋ฐฉํ–ฅ(๋„).\"\"\"\n", " i = seg_of(s)\n", " f = (s - C0L[i]) / KML[i]\n", " (x0, y0), (x1, y1) = PTS[i], PTS[i + 1]\n", " return x0 + f * (x1 - x0), y0 + f * (y1 - y0), float(np.degrees(np.arctan2(x1 - x0, y1 - y0)) % 360)\n", "\n", "\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", " for i, c in enumerate(C1L):\n", " if s < c:\n", " return i\n", " return len(C1L) - 1\n", "\n", "\n", "def route_xy(s):\n", " \"\"\"๋…ธ์„  ์œ„ ์ง„ํ–‰๊ฑฐ๋ฆฌ s(km)์˜ ์ขŒํ‘œ์™€ ์ง„ํ–‰ ๋ฐฉํ–ฅ(๋„).\"\"\"\n", " i = seg_of(s)\n", " f = (s - C0L[i]) / KML[i]\n", " (x0, y0), (x1, y1) = PTS[i], PTS[i + 1]\n", " return x0 + f * (x1 - x0), y0 + f * (y1 - y0), float(np.degrees(np.arctan2(x1 - x0, y1 - y0)) % 360)\n", "\n", "\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", " for i, c in enumerate(C1L):\n", " if s < c:\n", " return i\n", " return len(C1L) - 1\n", "\n", "\n", "def route_xy(s):\n", " \"\"\"๋…ธ์„  ์œ„ ์ง„ํ–‰๊ฑฐ๋ฆฌ s(km)์˜ ์ขŒํ‘œ์™€ ์ง„ํ–‰ ๋ฐฉํ–ฅ(๋„).\"\"\"\n", " i = seg_of(s)\n", " f = (s - C0L[i]) / KML[i]\n", " (x0, y0), (x1, y1) = PTS[i], PTS[i + 1]\n", " return x0 + f * (x1 - x0), y0 + f * (y1 - y0), float(np.degrees(np.arctan2(x1 - x0, y1 - y0)) % 360)\n", "\n", "\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 }