{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# ๐ฏ [MOV01] ๋ฐ์ดํฐ ๊ธฐ๋ฐ ์์ก ์์์์ธก Tool\n",
"### Google Colab ์ค์ต ํ๊ฒฝ (์์กยท๋ฌผ๋ฅยท์ฐฝ๊ณ / Level 3)\n",
"\n",
"[](https://colab.research.google.com/github/edunlab/milicontent2/blob/main/uploads/MOV01/MOV01_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",
"์์ก ์์๋ฅผ ๋ฐ์ดํฐ๋ก ์์ธกํด ๊ตฐ์ ์์ก๊ณํ์ ์ธ์ฐ๋ ์ผ์ ๊ตฌ๊ฐ๋ณ ํต๊ณผ ์ฉ๋๊ณผ ๋ณ๋ชฉ์ ํจ๊ป ๋ณด์์ผ ์ ํํด์ง๋ค. ๋ณธ ํ๋ก์ ํธ๋ ์์ก ์์์์ธก์์ ์์ธก ๋์ ์ ์, ์๊ณ์ดยทํผ์ฒ ๊ตฌ์ฑ, ๊ธฐ์ค๋ชจ๋ธ ๋น๊ต, ์ค๋ช
๊ฐ๋ฅ์ฑ, ์
๋ฌด ์๊ณ๊ฐ, ์ค์ ๋์กฐ๋ฅผ ์ตํ๋๋ก ๊ต์ก์ฉ ๊ฐ์ ๋
ธ์ ๋ง ์๋ฃ๋ฅผ ์ฝ๋๋ก ๋ง๋ค์ด ๊ตฌ๊ฐ๋ณ ์์ก ์์์ ์ง์ฒด๋ฅผ ์์ธกํ๋ค. ํ์ต์๋ ๋ณ๋ชฉ ๋ฐ์ดํฐ ์ ์, ์ง์ฒด ์ ์ฒ๋ฆฌ, BPR ๊ธฐ์ค ๋ชจ๋ธ, ์ต๋์ ๋ ๊ธฐ๋ฐ ๋ณ๋ชฉ ์๋ณ๊ณผ ํผ์ฒ ๋ชจ๋ธ, ์ฐํ ์์ยท์ ๋ฅ ์ฐ์ถ๊ณผ ์์๋ธ ๋ชจ๋ธ, ํผ์ก๋ ๋์๋ณด๋์ ์์ธ ์ค๋ช
, ์์ ๊ตฐ์๊ด ์น์ธ(HITL)๊ณผ ์ ํ๊ณํ ์ต์ ํ, 3๊ฐ ๊ตฌ๊ฐ ๋์ ์ฐจ๋จ ์คํธ๋ ์ค ์ํ, AAR ์ฌ๋ณด์ ๊ณผ ROI, ์ฌํ์ต ํ์ดํ๋ผ์ธ ํตํฉ์ ์ฐจ๋ก๋ก ์ํํ๋ค. ์ต์ข
๋ชฉํ๋ ๊ตฌ๊ฐ๋ณ ์์ยท์ง์ฒด๋ฅผ ์์ธกํด ๋ณ๋ชฉ์ ์ฌ์ ์ ์ฐพ๊ณ ์ฐํ ๋ฌผ๋ ๋ฐฐ๋ถ์์ ๋ง๋ค๋, ์ฐํ ๋ช
๋ น์ ํ๋ฌ ์ฌ๋ถ๋ ๋ฐ๋์ ์ฌ๋์ด ๊ฒฐ์ ํ๋ ์์ก ์์์์ธก ๋๊ตฌ๋ฅผ ์์ฑํ๋ ๊ฒ์ด๋ค.\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. ์ ์ ์ฃผ์ ๋ณด๊ธ๋ก(MSR) ๊ต์ฐจ๋ก, ๋ณ๋ชฉ ๊ตฌ๊ฐ(๋ณ๋ชฉ ๋ค๋ฆฌ/ํฐ๋) ๋ฐ ํต๊ณผ ๋ฅ๋ ฅ ๋ฐ์ดํฐ ์ ์\n",
"**์์์๊ฐ**: ์ฝ 1.5์๊ฐ\n",
"\n",
"### ๐ ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ์ํ ์ง์นจ\n",
"#### ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ๋น์์ฑ\n",
"\n",
"์ฃผ์ ๋ณด๊ธ๋ก(MSR) ๊ต์ฐจ๋ก ๋ฐ ๋ณ๋ชฉ ๊ตฌ๊ฐ ํต๊ณผ ๋ฅ๋ ฅ ๋ฐ์ดํฐ๋ฅผ ์ ์ํ๋ค. ์ค์ ์ ์ ํ์ฅ์์๋ ๋จ์ ํ์ง์ ๊ทธ์น์ง ์๊ณ ์ ์ํ ์ํ ํ๋จ๊ณผ ์ ๋ขฐ๋ ๋์ ์์ฌ๊ฒฐ์ ์ด ํ์์ ์ด๋ฏ๋ก, **๊ฐ์ ๋
ธ์ ๋ง์ ๊ต์ฐจ๋กยท๋ณ๋ชฉ(๊ต๋ยทํฐ๋) ๊ตฌ๊ฐ๊ณผ ํต๊ณผ ๋ฅ๋ ฅ ๋ฐ์ดํฐ๋ฅผ ์ ์ํ๊ณ , ์์ก ์์์์ธก ์
๋ฌด ํ๋ก์ธ์ค ๋งต๊ณผ ํผ์ฒ ๋ฐ์ดํฐ ์ฌ์ ์ ๋ง๋ ๋ค.** ๊ณผ์
์ ์์ํ์ฌ ์คํ์ ๋ฐฉ์งํ๊ณ ์์ ๋์๋ ฅ์ ๊ทน๋ํํด์ผ ํฉ๋๋ค.\n",
"\n",
"์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ณธ ๋ฏธ์
์์๋ **๊ตฌ๊ฐ ์ฝ๋, ์ ํ, ์ฐ์ฅ, ํผํฌ์๊ฐ ํต๊ณผ ์ฉ๋, ๊ณํ ๋ฌผ๋, ์ค์ ํต๊ณผ๋, ๋น๊ณํ ์ ๋นยท๊ธด๊ธ ๋ถ์ถยท์ฐ์ฒ ์ฌ๋ถ, ์ง์ฒด ์๊ฐ์ ํ๋์ ํ์ฉ๊ฐ์ ์ ์ํํ๊ณ ๊ฒ์ฆํ๋** ์ค์ต์ ์ฒด๊ณ์ ์ผ๋ก ์ํํฉ๋๋ค.\n",
"\n",
"ํ์ต์๋ ์ค์ต๊ฐ์ด๋์ ์ ์๋ ์ด 4๊ฐ ์ธ๋ถ ํ๋(๊ธฐ๋ณธ ๊ธฐ๋ฅ ๊ตฌํ โ ๊ฒฐ๊ณผ ์งํ ๊ฒ์ฆ โ ํ๋ผ๋ฏธํฐ ํ๋ ๋ฐ ๋์ ๊ณผ์ )์ ์์ฐจ์ ์ผ๋ก ์งํํ๋ฉฐ ํต์ฌ ํ์ ๊ธฐ์ค์ ๋ฌ์ฑํฉ๋๋ค.\n",
"\n",
"**์ด๋ฒ ๊ฒฐ๊ณผ๋ฌผ:** `docs/mov01_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": [
"\"\"\"๊ฐ์ ๋
ธ์ ๋ง์ ๊ต์ฐจ๋กยท๋ณ๋ชฉ(๊ต๋ยทํฐ๋) ๊ตฌ๊ฐ๊ณผ ํต๊ณผ ๋ฅ๋ ฅ ๋ฐ์ดํฐ๋ฅผ ์ ์ํ๊ณ , ์์ก ์์์์ธก ์
๋ฌด ํ๋ก์ธ์ค ๋งต๊ณผ 20๊ฐ ์ด์์ ํผ์ฒ ๋ฐ์ดํฐ ์ฌ์ ์ ๋ง๋ค์ด ์ค์ ๋ฐ์ดํฐ ์ปฌ๋ผ๊ณผ ๋์กฐํ๋ค.\"\"\"\n",
"import re\n",
"import numpy as np\n",
"import pandas as pd\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# [๊ต์ก์ฉ๊ฐ์์๋ฃ] ๊ฐ์ ๋
ธ์ ๋งยท๊ตฌ๊ฐ ํต๊ณผ๋ ์์ฑ๊ธฐ (์ค์ ๋๋กยท์ง๋ช
ยท๋ถ๋ ์์)\n",
"# ๊ฐ์ ์ถ๋ฐ์ง(SRC)์์ ๊ฐ์ ๋ชฉ์ ์ง(DST)๊น์ง 3๊ฐ ์ถ์ (AยทBยทC)๊ณผ ์ฐ๊ฒฐ๋ก 2๊ฐ, ๊ตฌ๊ฐ 10๊ฐ. 150์ผ์น ํผํฌ์๊ฐ ํต๊ณผ๋ยท์ง์ฒด ์๊ฐ์ ๋ง๋ ๋ค.\n",
"# ---------------------------------------------------------------------------\n",
"SEGS = pd.DataFrame([\n",
" (\"A1\", \"SRC\", \"J1\", 12, 130, \"์ผ๋ฐ\"), (\"A2\", \"J1\", \"J2\", 8, 70, \"๊ต๋\"), (\"A3\", \"J2\", \"DST\", 10, 140, \"๊ต์ฐจ๋ก\"),\n",
" (\"B1\", \"SRC\", \"J3\", 15, 100, \"์ผ๋ฐ\"), (\"B2\", \"J3\", \"J2\", 9, 80, \"ํฐ๋\"), (\"B3\", \"J3\", \"DST\", 14, 95, \"์ผ๋ฐ\"),\n",
" (\"C1\", \"SRC\", \"J4\", 18, 70, \"์ผ๋ฐ\"), (\"C2\", \"J4\", \"DST\", 20, 75, \"๊ต๋\"),\n",
" (\"X1\", \"J1\", \"J3\", 7, 50, \"๊ต์ฐจ๋ก\"), (\"X2\", \"J4\", \"J3\", 10, 60, \"์ผ๋ฐ\")],\n",
" columns=[\"seg\", \"u\", \"v\", \"km\", \"cap\", \"kind\"]) # cap = ํผํฌ์๊ฐ ํต๊ณผ ์ฉ๋(๋/์, ๊ต์ก์ฉ ๊ฐ์ ๊ฐ)\n",
"KSPD = {\"์ผ๋ฐ\": 55, \"๊ต๋\": 40, \"ํฐ๋\": 45, \"๊ต์ฐจ๋ก\": 45} # ๊ตฌ๊ฐ ์ ํ๋ณ ์์ ์ฃผํ ์๋(km/h, ๊ฐ์ ๊ฐ)\n",
"SEGS[\"t0\"] = SEGS[\"km\"] / SEGS[\"kind\"].map(KSPD) * 60 # ์์ ์ฃผํ ์๊ฐ(๋ถ)\n",
"UTIL = dict(A1=.78, A2=.85, A3=.66, B1=.70, B2=.72, B3=.64, C1=.55, C2=.58, X1=.45, X2=.40) # ํ์ ์ด์ฉ๋ฅ \n",
"\n",
"\n",
"def bpr(t0, v, cap, a=0.15, b=4.0):\n",
" \"\"\"BPR ์ง์ฒด ํจ์(๊ตํต๊ณตํ์ ํ์คํ): ํต๊ณผ๋/์ฉ๋ ๋น๊ฐ ์ปค์ง์๋ก ์ง์ฒด๊ฐ ๊ธ์ฆํ๋ค. (๋ถ)\"\"\"\n",
" return t0 * a * (v / cap) ** b\n",
"\n",
"\n",
"def make_data(seed=11, days=150):\n",
" \"\"\"๊ตฌ๊ฐ x ์ผ์ ํจ๋. v=์ค์ ํผํฌ ํต๊ณผ๋, plan_v=์์ก๊ณํ ์๋(๊ธด๊ธ ๋ถ์ถ ๋ฏธ๋ฐ์), delay=์ค์ ์ง์ฒด(๋ถ).\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" d_idx = np.arange(days)\n",
" lvl = np.array([1.0, 1.08, 1.04, 1.10, 1.02, 0.82, 0.78])[d_idx % 7] * (1 + 0.0012 * d_idx)\n",
" lvl = lvl * np.where((d_idx >= 100) & (d_idx < 110), 1.30, 1.0) # ์์ก๋ ํญ์ฆ ๊ธฐ๊ฐ(๊ฐ์)\n",
" prev, rows = {s: 0.0 for s in SEGS[\"seg\"]}, []\n",
" for d in d_idx:\n",
" rain = bool(rng.random() < 0.18)\n",
" for r in SEGS.itertuples():\n",
" maint, urgent = bool(rng.random() < 0.06), bool(rng.random() < 0.05) # ๋น๊ณํ ์ ๋น / ๊ธด๊ธ ๋ถ์ถ\n",
" v = r.cap * UTIL[r.seg] * lvl[d] * (1.25 if urgent else 1.0) * rng.normal(1, 0.05)\n",
" cap_eff = r.cap * (0.65 if maint else 1.0) * (0.92 if rain else 1.0)\n",
" delay = max(r.t0 * 0.2 * (v / cap_eff) ** 3.5 + 0.25 * prev[r.seg] + rng.normal(0, 0.4), 0.0)\n",
" prev[r.seg] = delay\n",
" rows.append(dict(day=int(d), dow=int(d % 7), seg=r.seg, kind=r.kind, km=r.km, cap=r.cap, t0=r.t0,\n",
" plan_v=v / (1.25 if urgent else 1.0) * rng.normal(1, 0.04), v=v, maint=int(maint), urgent=int(urgent),\n",
" rain=int(rain), cap_eff=cap_eff, delay=delay))\n",
" df = pd.DataFrame(rows)\n",
" df[\"transit\"] = df[\"t0\"] * (1 + 0.08 * df[\"rain\"]) + df[\"delay\"]\n",
" return df\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",
"# ์ง์ฒด ์์ธก์ฉ ํ์ ํผ์ฒ ์์ฑ(๋น์ผ ์ค์ ์์ ์ ์ ์ ์๋ ์ ๋ณด๋ง ์ฌ์ฉ: ๊ณํ ๋ฌผ๋, ๊ณต์ง ์ ๋น, ๊ธฐ์ ์๋ณด, ์ ์ผยท์ ์ฃผ ์ค์ )\n",
"# ---------------------------------------------------------------------------\n",
"FEATS = [\"plan_v\", \"cap\", \"cap_eff\", \"t0\", \"km\", \"maint\", \"rain\", \"util\", \"util_eff\", \"util_eff4\", \"bpr_base\", \"lag1_delay\", \"lag7_delay\",\n",
" \"roll7_v\", \"surge_ratio\", \"is_weekend\", \"dow_sin\", \"dow_cos\", \"k_๊ต๋\", \"k_ํฐ๋\", \"k_๊ต์ฐจ๋ก\"]\n",
"\n",
"\n",
"def add_features(df):\n",
" d = df.sort_values([\"seg\", \"day\"]).copy()\n",
" g = d.groupby(\"seg\")\n",
" d[\"lag1_delay\"] = g[\"delay\"].shift(1)\n",
" d[\"lag7_delay\"] = g[\"delay\"].shift(7)\n",
" d[\"roll7_v\"] = g[\"v\"].transform(lambda s: s.shift(1).rolling(7).mean())\n",
" d[\"util\"] = d[\"plan_v\"] / d[\"cap\"]\n",
" d[\"util_eff\"] = d[\"plan_v\"] / d[\"cap_eff\"]\n",
" d[\"util_eff4\"] = d[\"util_eff\"] ** 4\n",
" d[\"bpr_base\"] = bpr(d[\"t0\"], d[\"plan_v\"], d[\"cap\"])\n",
" d[\"surge_ratio\"] = d[\"plan_v\"] / d[\"roll7_v\"]\n",
" d[\"is_weekend\"] = (d[\"dow\"] >= 5).astype(int)\n",
" d[\"dow_sin\"], d[\"dow_cos\"] = np.sin(2 * np.pi * d[\"dow\"] / 7), np.cos(2 * np.pi * d[\"dow\"] / 7)\n",
" for k in (\"๊ต๋\", \"ํฐ๋\", \"๊ต์ฐจ๋ก\"):\n",
" d[\"k_\" + k] = (d[\"kind\"] == k).astype(int)\n",
" return d.dropna().sort_values([\"day\", \"seg\"]).reset_index(drop=True)\n",
"# ---------------------------------------------------------------------------\n",
"\n",
"# 1) ๋ฐ์ดํฐ ์ฌ์ : ์์ฒ ์ปฌ๋ผ + ํ์ ํผ์ฒ (์ด๋ฆ, ๊ตฌ๋ถ, ์๋ฃํ, ํ์ฉ ๋ฒ์/๊ฐ, ์ค๋ช
)\n",
"DICT = [\n",
" (\"seg\", \"์์ฒ\", \"str\", r\"^[ABCX]\\d$\", \"๊ตฌ๊ฐ ์ฝ๋(๊ฐ์ ๋
ธ์ ๋ง)\"), (\"kind\", \"์์ฒ\", \"str\", \"์ผ๋ฐ|๊ต๋|ํฐ๋|๊ต์ฐจ๋ก\", \"๊ตฌ๊ฐ ์ ํ(๋ณ๋ชฉ ํ๋ณด)\"),\n",
" (\"km\", \"์์ฒ\", \"float\", \"1~50\", \"๊ตฌ๊ฐ ์ฐ์ฅ(km)\"), (\"cap\", \"์์ฒ\", \"float\", \"10~300\", \"ํผํฌ์๊ฐ ํต๊ณผ ์ฉ๋(๋/์)\"),\n",
" (\"t0\", \"์์ฒ\", \"float\", \"1~120\", \"์์ ์ฃผํ ์๊ฐ(๋ถ)\"), (\"day\", \"์์ฒ\", \"int\", \"0~\", \"๊ด์ธก ์ผ์ ๋ฒํธ\"),\n",
" (\"dow\", \"์์ฒ\", \"int\", \"0~6\", \"์์ผ ๋ฒํธ\"), (\"plan_v\", \"์์ฒ\", \"float\", \"0~\", \"์์ก๊ณํ ํผํฌ ๋ฌผ๋(๋/์)\"),\n",
" (\"v\", \"์์ฒ\", \"float\", \"0~\", \"์ค์ ํผํฌ ํต๊ณผ๋(๋/์)\"), (\"maint\", \"์์ฒ\", \"int\", \"0|1\", \"๋น๊ณํ ์ ๋น ์ฌ๋ถ(ํต๊ณผ ์ฉ๋ ๊ฐ์)\"),\n",
" (\"urgent\", \"์์ฒ\", \"int\", \"0|1\", \"๊ธด๊ธ ๋ถ์ถ ์ฌ๋ถ(๋ฌผ๋ ์ฆ๊ฐ)\"), (\"rain\", \"์์ฒ\", \"int\", \"0|1\", \"์ฐ์ฒ ์ฌ๋ถ\"),\n",
" (\"cap_eff\", \"์์ฒ\", \"float\", \"0~cap\", \"์ ๋นยท๊ธฐ์ ๋ฐ์ ์ ํจ ์ฉ๋\"), (\"delay\", \"๋ชฉํ\", \"float\", \"0~\", \"๊ตฌ๊ฐ ์ง์ฒด ์๊ฐ(๋ถ)\"),\n",
" (\"transit\", \"๋ชฉํ\", \"float\", \"t0~\", \"๊ตฌ๊ฐ ํต๊ณผ์๊ฐ(๋ถ)\"),\n",
" (\"util\", \"ํ์\", \"float\", \"0~2\", \"๊ณํ๋ฌผ๋/๊ณต์นญ์ฉ๋\"), (\"util_eff\", \"ํ์\", \"float\", \"0~3\", \"๊ณํ๋ฌผ๋/์ ํจ์ฉ๋\"),\n",
" (\"util_eff4\", \"ํ์\", \"float\", \"0~\", \"์ ํจ ์ด์ฉ๋ฅ 4์ ๊ณฑ(ํผ์ก ๋น์ ํ)\"), (\"bpr_base\", \"ํ์\", \"float\", \"0~\", \"BPR ๊ธฐ์ค ์ง์ฒด(๋ถ)\"),\n",
" (\"lag1_delay\", \"ํ์\", \"float\", \"0~\", \"์ ์ผ ์ง์ฒด\"), (\"lag7_delay\", \"ํ์\", \"float\", \"0~\", \"์ ์ฃผ ๋์์ผ ์ง์ฒด\"),\n",
" (\"roll7_v\", \"ํ์\", \"float\", \"0~\", \"์ต๊ทผ 7์ผ ํ๊ท ํต๊ณผ๋\"), (\"surge_ratio\", \"ํ์\", \"float\", \"0~\", \"๊ณํ๋ฌผ๋/์ต๊ทผ ํ๊ท (ํญ์ฆ ์งํ)\"),\n",
" (\"is_weekend\", \"ํ์\", \"int\", \"0|1\", \"์ฃผ๋ง ์ฌ๋ถ\"), (\"dow_sin\", \"ํ์\", \"float\", \"-1~1\", \"์์ผ ์ํ(์ฌ์ธ)\"),\n",
" (\"dow_cos\", \"ํ์\", \"float\", \"-1~1\", \"์์ผ ์ํ(์ฝ์ฌ์ธ)\"), (\"k_๊ต๋\", \"ํ์\", \"int\", \"0|1\", \"๊ต๋ ๊ตฌ๊ฐ ๋๋ฏธ\"),\n",
" (\"k_ํฐ๋\", \"ํ์\", \"int\", \"0|1\", \"ํฐ๋ ๊ตฌ๊ฐ ๋๋ฏธ\"), (\"k_๊ต์ฐจ๋ก\", \"ํ์\", \"int\", \"0|1\", \"๊ต์ฐจ๋ก ๊ตฌ๊ฐ ๋๋ฏธ\")]\n",
"dd = pd.DataFrame(DICT, columns=[\"ํ๋\", \"๊ตฌ๋ถ\", \"ํ\", \"ํ์ฉ๊ฐ\", \"์ค๋ช
\"])\n",
"print(f\"[1] ๋ฐ์ดํฐ ์ฌ์ {len(dd)}๊ฐ ํ๋ (์์ฒ {int((dd.๊ตฌ๋ถ == '์์ฒ').sum())}, ๋ชฉํ {int((dd.๊ตฌ๋ถ == '๋ชฉํ').sum())}, ํ์ {int((dd.๊ตฌ๋ถ == 'ํ์').sum())})\")\n",
"\n",
"# 2) ์ค์ ์์ฑ ๋ฐ์ดํฐ์ ๋์กฐ: ์ฌ์ ์ ์๋ ์ปฌ๋ผ / ์ฌ์ ์๋ง ์๋ ํ๋\n",
"df = make_data()\n",
"feat = add_features(df)\n",
"miss = sorted(set(feat.columns) - set(dd[\"ํ๋\"]))\n",
"extra = sorted(set(dd[\"ํ๋\"]) - set(feat.columns))\n",
"print(f\"[2] ๋ฐ์ดํฐ ์ฌ์ ๋์กฐ: ์ฌ์ ๋๋ฝ ์ปฌ๋ผ {miss or '์์'}, ๋ฐ์ดํฐ์ ์๋ ํ๋ {extra or '์์'}\")\n",
"fe = dd[dd[\"๊ตฌ๋ถ\"] != \"๋ชฉํ\"]\n",
"print(f\" ์์ธก ํผ์ฒ ํ๋ณด(์์ฒ+ํ์ ์ค ๋ชฉํ ์ ์ธ) {len(fe)}๊ฐ -> ๋ชฉํ(20๊ฐ ์ด์) {'๋ฌ์ฑ' if len(fe) >= 20 else '๋ฏธ๋ฌ'}\")\n",
"bad_rng = {\"v\": int((feat.v < 0).sum()), \"cap_eff\": int((feat.cap_eff > feat.cap).sum()), \"util_eff\": int((feat.util_eff > 3).sum())}\n",
"print(\" ํ์ฉ ๋ฒ์ ์๋ฐ ๊ฑด์:\", bad_rng)\n",
"\n",
"# 3) ๊ตฌ๊ฐ ๋ง์คํฐ: ๋ณ๋ชฉ ํ๋ณด(๊ต๋ยทํฐ๋ยท๊ต์ฐจ๋ก)์ ํต๊ณผ ๋ฅ๋ ฅ\n",
"seg = SEGS.copy()\n",
"seg[\"๋ณ๋ชฉํ๋ณด\"] = np.where(seg[\"kind\"].isin([\"๊ต๋\", \"ํฐ๋\", \"๊ต์ฐจ๋ก\"]), \"Y\", \"\")\n",
"seg[\"ํ์ ์ด์ฉ๋ฅ \"] = seg[\"seg\"].map(UTIL)\n",
"print(\"\\n[3] ๊ตฌ๊ฐ ๋ง์คํฐ(๊ฐ์ ๋
ธ์ ๋ง, ๊ต์ก์ฉ ๊ฐ์ ๊ฐ)\")\n",
"print(seg[[\"seg\", \"u\", \"v\", \"km\", \"kind\", \"cap\", \"t0\", \"ํ์ ์ด์ฉ๋ฅ \", \"๋ณ๋ชฉํ๋ณด\"]].round(2).to_string(index=False))\n",
"\n",
"# 4) ์์ก ์์์์ธก ์
๋ฌด ํ๋ก์ธ์ค ๋งต(๋จ๊ณ, ๋ด๋น, ์
๋ ฅ ๋ฐ์ดํฐ, ์ฐ์ถ, ํํ ์์ ๋ถ)\n",
"proc = [(\"์์ก ์์ ์ ์\", \"์์ก๊ณํ์ฅ๊ต\", \"๋ถ๋๋ณ ์์ฒญ ์์\", \"์ผ์ผ ์์ก ์์ํ\", 30), (\"๊ตฌ๊ฐ ํต๊ณผ๋ ์ง๊ณ\", \"์์กํต์ ์\", \"๊ตฌ๊ฐ ํต๊ณผ ๋ณด๊ณ \", \"plan_v, v\", 40),\n",
" (\"์ ๋นยท๊ธฐ์ ์ ๋ณด ์ทจํฉ\", \"ํต์ ์/๊ธฐ์๋ฐ\", \"์ ๋น ๊ณต์ง, ์๋ณด\", \"maint, rain\", 25), (\"ํต๊ณผ ์ฉ๋ ์ฐ์ \", \"์์กํต์ ์\", \"๊ตฌ๊ฐ ๋ง์คํฐ\", \"cap, cap_eff\", 20),\n",
" (\"์ง์ฒด ์์ธก\", \"๋ถ์ ๋ด๋น\", \"ํผ์ฒ ํ
์ด๋ธ\", \"delay ์์ธก\", 45), (\"๋ณ๋ชฉ ํ์ \", \"๋ถ์ ๋ด๋น\", \"์์ธก ์ง์ฒด, ์ด์ฉ๋ฅ \", \"๋ณ๋ชฉ ๋ชฉ๋ก\", 20),\n",
" (\"์ฐํ ๊ฒํ ยท์น์ธ\", \"์์ ๊ตฐ์๊ด\", \"์ฐํ ํ๋ณด, ์ถ๊ฐ ์์\", \"์ฐํ ๋ช
๋ น(์น์ธ)\", 30), (\"๋ช
๋ น ํ๋ฌยท์ดํ\", \"์์กํต์ ์\", \"์น์ธ๋ ๋ช
๋ น\", \"์ดํ ์ด๋ฒคํธ\", 15),\n",
" (\"์ค์ ๋์กฐยท๋ณด์ \", \"๋ถ์ ๋ด๋น\", \"์์ธก ๋ ์ค์ \", \"์ค์ฐจ ๋ณด๊ณ \", 35)]\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ํ๋ก์ธ์ค ๋งต ์์ฑ ์๋ฃ(9๋จ๊ณ), ํผ์ฒ ๋ชฉ๋ก 20๊ฐ ์ด์ -> ๋ชฉํ\", \"๋ฌ์ฑ\" if len(pm) >= 5 and len(fe) >= 20 else \"๋ฏธ๋ฌ\")\n",
"print(\"๋ชจ๋ ๋
ธ์ ยท๊ตฌ๊ฐยท์์น๋ ๊ต์ก์ฉ ๊ฐ์ ์๋ฃ์ด๋ฉฐ ์ค์ ๋ณด๊ธ๋กยท์ง๋ช
ยท๋ถ๋์ ๋ฌด๊ดํ๋ค.\")\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"๐ก [์ฐธ๊ณ ] ๋ฏธ์
01 ์์ ์คํ ๊ฒฐ๊ณผ (Output)
\n",
"\n",
"```text\n",
"[1] ๋ฐ์ดํฐ ์ฌ์ 29๊ฐ ํ๋ (์์ฒ 13, ๋ชฉํ 2, ํ์ 14)\n",
"[2] ๋ฐ์ดํฐ ์ฌ์ ๋์กฐ: ์ฌ์ ๋๋ฝ ์ปฌ๋ผ ์์, ๋ฐ์ดํฐ์ ์๋ ํ๋ ์์\n",
" ์์ธก ํผ์ฒ ํ๋ณด(์์ฒ+ํ์ ์ค ๋ชฉํ ์ ์ธ) 27๊ฐ -> ๋ชฉํ(20๊ฐ ์ด์) ๋ฌ์ฑ\n",
" ํ์ฉ ๋ฒ์ ์๋ฐ ๊ฑด์: {'v': 0, 'cap_eff': 0, 'util_eff': 0}\n",
"\n",
"[3] ๊ตฌ๊ฐ ๋ง์คํฐ(๊ฐ์ ๋
ธ์ ๋ง, ๊ต์ก์ฉ ๊ฐ์ ๊ฐ)\n",
"seg u v km kind cap t0 ํ์ ์ด์ฉ๋ฅ ๋ณ๋ชฉํ๋ณด\n",
" A1 SRC J1 12 ์ผ๋ฐ 130 13.09 0.78 \n",
" A2 J1 J2 8 ๊ต๋ 70 12.00 0.85 Y\n",
" A3 J2 DST 10 ๊ต์ฐจ๋ก 140 13.33 0.66 Y\n",
" B1 SRC J3 15 ์ผ๋ฐ 100 16.36 0.70 \n",
" B2 J3 J2 9 ํฐ๋ 80 12.00 0.72 Y\n",
" B3 J3 DST 14 ์ผ๋ฐ 95 15.27 0.64 \n",
" C1 SRC J4 18 ์ผ๋ฐ 70 19.64 0.55 \n",
" C2 J4 DST 20 ๊ต๋ 75 30.00 0.58 Y\n",
" X1 J1 J3 7 ๊ต์ฐจ๋ก 50 9.33 0.45 Y\n",
" X2 J4 J3 10 ์ผ๋ฐ 60 10.91 0.40 \n",
"\n",
"[4] ์
๋ฌด ํ๋ก์ธ์ค ๋งต 9๋จ๊ณ, ํํ ํฉ๊ณ 260๋ถ (๊ฐ์ฅ ๊ธด ๋จ๊ณ: ์ง์ฒด ์์ธก)\n",
" ๋จ๊ณ ๋ด๋น ์
๋ ฅ ์ฐ์ถ ํํ(๋ถ)\n",
"1 ์์ก ์์ ์ ์ ์์ก๊ณํ์ฅ๊ต ๋ถ๋๋ณ ์์ฒญ ์์ ์ผ์ผ ์์ก ์์ํ 30\n",
"2 ๊ตฌ๊ฐ ํต๊ณผ๋ ์ง๊ณ ์์กํต์ ์ ๊ตฌ๊ฐ ํต๊ณผ ๋ณด๊ณ plan_v, v 40\n",
"3 ์ ๋นยท๊ธฐ์ ์ ๋ณด ์ทจํฉ ํต์ ์/๊ธฐ์๋ฐ ์ ๋น ๊ณต์ง, ์๋ณด maint, rain 25\n",
"4 ํต๊ณผ ์ฉ๋ ์ฐ์ ์์กํต์ ์ ๊ตฌ๊ฐ ๋ง์คํฐ cap, cap_eff 20\n",
"5 ์ง์ฒด ์์ธก ๋ถ์ ๋ด๋น ํผ์ฒ ํ
์ด๋ธ delay ์์ธก 45\n",
"6 ๋ณ๋ชฉ ํ์ ๋ถ์ ๋ด๋น ์์ธก ์ง์ฒด, ์ด์ฉ๋ฅ ๋ณ๋ชฉ ๋ชฉ๋ก 20\n",
"7 ์ฐํ ๊ฒํ ยท์น์ธ ์์ ๊ตฐ์๊ด ์ฐํ ํ๋ณด, ์ถ๊ฐ ์์ ์ฐํ ๋ช
๋ น(์น์ธ) 30\n",
"8 ๋ช
๋ น ํ๋ฌยท์ดํ ์์กํต์ ์ ์น์ธ๋ ๋ช
๋ น ์ดํ ์ด๋ฒคํธ 15\n",
"9 ์ค์ ๋์กฐยท๋ณด์ ๋ถ์ ๋ด๋น ์์ธก ๋ ์ค์ ์ค์ฐจ ๋ณด๊ณ 35\n",
"\n",
"ํ๋ก์ธ์ค ๋งต ์์ฑ ์๋ฃ(9๋จ๊ณ), ํผ์ฒ ๋ชฉ๋ก 20๊ฐ ์ด์ -> ๋ชฉํ ๋ฌ์ฑ\n",
"๋ชจ๋ ๋
ธ์ ยท๊ตฌ๊ฐยท์์น๋ ๊ต์ก์ฉ ๊ฐ์ ์๋ฃ์ด๋ฉฐ ์ค์ ๋ณด๊ธ๋กยท์ง๋ช
ยท๋ถ๋์ ๋ฌด๊ดํ๋ค.\n",
"```\n",
" \n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## ๐ฏ ๋ฏธ์
02. ์์ก ์๋ ํญ์ฆ์ ๋ฐ๋ฅธ ๊ตฌ๊ฐ๋ณ ๋ณ๋ชฉ ์ง์ฒด ์๊ฐ(Delay) ๊ณ์ฐ ์ ์ฒ๋ฆฌ ๋ชจ๋ ๊ตฌํ\n",
"**์์์๊ฐ**: ์ฝ 2.0์๊ฐ\n",
"\n",
"### ๐ ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ์ํ ์ง์นจ\n",
"#### ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ๋น์์ฑ\n",
"\n",
"์์ก๋ ํญ์ฆ์ ๋ฐ๋ฅธ ๊ตฌ๊ฐ๋ณ ์ง์ฒด ์๊ฐ ๊ณ์ฐ ์ ์ฒ๋ฆฌ ๋ชจ๋์ ๊ตฌํํ๋ค. ์ค์ ์ ์ ํ์ฅ์์๋ ๋จ์ ํ์ง์ ๊ทธ์น์ง ์๊ณ ์ ์ํ ์ํ ํ๋จ๊ณผ ์ ๋ขฐ๋ ๋์ ์์ฌ๊ฒฐ์ ์ด ํ์์ ์ด๋ฏ๋ก, **์์ก๋ ํญ์ฆ์ ๋ฐ๋ฅธ ๊ตฌ๊ฐ๋ณ ์ง์ฒด ์๊ฐ ๊ณ์ฐ์ ์ํด ์ค์ผ๋ ๊ตฌ๊ฐ ํต๊ณผ ๊ธฐ๋ก์ ์ ์ ํ๋ ์ ์ฒ๋ฆฌ ๋ชจ๋์ ๊ตฌํํ๋ค.** ๊ณผ์
์ ์์ํ์ฌ ์คํ์ ๋ฐฉ์งํ๊ณ ์์ ๋์๋ ฅ์ ๊ทน๋ํํด์ผ ํฉ๋๋ค.\n",
"\n",
"์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ณธ ๋ฏธ์
์์๋ **๊ฒฐ์ธก, ์์, ๋จ์ ์ค๋ฅ, ํ๊ธฐ ์ค๋ฅ, ๋ฏธ๋ฑ๋ก ๊ตฌ๊ฐ, ์ฉ๋ 0, ์ค๋ณต์ ์ฃผ์
ํ ๊ฐ์ ๊ธฐ๋ก์ ์ ์ ๊ท์น 10์ข
์ ์์๋๋ก ์ ์ฉ ์ํํ๊ณ ๊ฒ์ฆํ๋** ์ค์ต์ ์ฒด๊ณ์ ์ผ๋ก ์ํํฉ๋๋ค.\n",
"\n",
"ํ์ต์๋ ์ค์ต๊ฐ์ด๋์ ์ ์๋ ์ด 4๊ฐ ์ธ๋ถ ํ๋(๊ธฐ๋ณธ ๊ธฐ๋ฅ ๊ตฌํ โ ๊ฒฐ๊ณผ ์งํ ๊ฒ์ฆ โ ํ๋ผ๋ฏธํฐ ํ๋ ๋ฐ ๋์ ๊ณผ์ )์ ์์ฐจ์ ์ผ๋ก ์งํํ๋ฉฐ ํต์ฌ ํ์ ๊ธฐ์ค์ ๋ฌ์ฑํฉ๋๋ค.\n",
"\n",
"**์ด๋ฒ ๊ฒฐ๊ณผ๋ฌผ:** `src/etl/mov01_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": [
"\"\"\"์์ก๋ ํญ์ฆ์ ๋ฐ๋ฅธ ๊ตฌ๊ฐ๋ณ ์ง์ฒด ์๊ฐ ๊ณ์ฐ ์ ์ฒ๋ฆฌ ๋ชจ๋: ์ค์ผ๋ ๊ตฌ๊ฐ ํต๊ณผ ๊ธฐ๋ก์ ์ ์ ๊ท์น 10์ข
์ ์ ์ฉํ๊ณ ๋น๊ณํ ์ ๋นยท๊ธด๊ธ ๋ถ์ถ ๊ฐ์ ๊ตฐ์ ํน์ ์์ธ๋ ์ญ์ ํ์ง ์๊ณ ๋ณด์กดํ๋ฉฐ, ์ ํฉ์ฑ ๊ฒ์ฆ๊ณผ ์ ํ ํต๊ณ๋ฅผ ๋น๊ตํ๋ค.\"\"\"\n",
"import numpy as np\n",
"import pandas as pd\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# [๊ต์ก์ฉ๊ฐ์์๋ฃ] ๊ฐ์ ๋
ธ์ ๋งยท๊ตฌ๊ฐ ํต๊ณผ๋ ์์ฑ๊ธฐ (์ค์ ๋๋กยท์ง๋ช
ยท๋ถ๋ ์์)\n",
"# ๊ฐ์ ์ถ๋ฐ์ง(SRC)์์ ๊ฐ์ ๋ชฉ์ ์ง(DST)๊น์ง 3๊ฐ ์ถ์ (AยทBยทC)๊ณผ ์ฐ๊ฒฐ๋ก 2๊ฐ, ๊ตฌ๊ฐ 10๊ฐ. 150์ผ์น ํผํฌ์๊ฐ ํต๊ณผ๋ยท์ง์ฒด ์๊ฐ์ ๋ง๋ ๋ค.\n",
"# ---------------------------------------------------------------------------\n",
"SEGS = pd.DataFrame([\n",
" (\"A1\", \"SRC\", \"J1\", 12, 130, \"์ผ๋ฐ\"), (\"A2\", \"J1\", \"J2\", 8, 70, \"๊ต๋\"), (\"A3\", \"J2\", \"DST\", 10, 140, \"๊ต์ฐจ๋ก\"),\n",
" (\"B1\", \"SRC\", \"J3\", 15, 100, \"์ผ๋ฐ\"), (\"B2\", \"J3\", \"J2\", 9, 80, \"ํฐ๋\"), (\"B3\", \"J3\", \"DST\", 14, 95, \"์ผ๋ฐ\"),\n",
" (\"C1\", \"SRC\", \"J4\", 18, 70, \"์ผ๋ฐ\"), (\"C2\", \"J4\", \"DST\", 20, 75, \"๊ต๋\"),\n",
" (\"X1\", \"J1\", \"J3\", 7, 50, \"๊ต์ฐจ๋ก\"), (\"X2\", \"J4\", \"J3\", 10, 60, \"์ผ๋ฐ\")],\n",
" columns=[\"seg\", \"u\", \"v\", \"km\", \"cap\", \"kind\"]) # cap = ํผํฌ์๊ฐ ํต๊ณผ ์ฉ๋(๋/์, ๊ต์ก์ฉ ๊ฐ์ ๊ฐ)\n",
"KSPD = {\"์ผ๋ฐ\": 55, \"๊ต๋\": 40, \"ํฐ๋\": 45, \"๊ต์ฐจ๋ก\": 45} # ๊ตฌ๊ฐ ์ ํ๋ณ ์์ ์ฃผํ ์๋(km/h, ๊ฐ์ ๊ฐ)\n",
"SEGS[\"t0\"] = SEGS[\"km\"] / SEGS[\"kind\"].map(KSPD) * 60 # ์์ ์ฃผํ ์๊ฐ(๋ถ)\n",
"UTIL = dict(A1=.78, A2=.85, A3=.66, B1=.70, B2=.72, B3=.64, C1=.55, C2=.58, X1=.45, X2=.40) # ํ์ ์ด์ฉ๋ฅ \n",
"\n",
"\n",
"def bpr(t0, v, cap, a=0.15, b=4.0):\n",
" \"\"\"BPR ์ง์ฒด ํจ์(๊ตํต๊ณตํ์ ํ์คํ): ํต๊ณผ๋/์ฉ๋ ๋น๊ฐ ์ปค์ง์๋ก ์ง์ฒด๊ฐ ๊ธ์ฆํ๋ค. (๋ถ)\"\"\"\n",
" return t0 * a * (v / cap) ** b\n",
"\n",
"\n",
"def make_data(seed=11, days=150):\n",
" \"\"\"๊ตฌ๊ฐ x ์ผ์ ํจ๋. v=์ค์ ํผํฌ ํต๊ณผ๋, plan_v=์์ก๊ณํ ์๋(๊ธด๊ธ ๋ถ์ถ ๋ฏธ๋ฐ์), delay=์ค์ ์ง์ฒด(๋ถ).\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" d_idx = np.arange(days)\n",
" lvl = np.array([1.0, 1.08, 1.04, 1.10, 1.02, 0.82, 0.78])[d_idx % 7] * (1 + 0.0012 * d_idx)\n",
" lvl = lvl * np.where((d_idx >= 100) & (d_idx < 110), 1.30, 1.0) # ์์ก๋ ํญ์ฆ ๊ธฐ๊ฐ(๊ฐ์)\n",
" prev, rows = {s: 0.0 for s in SEGS[\"seg\"]}, []\n",
" for d in d_idx:\n",
" rain = bool(rng.random() < 0.18)\n",
" for r in SEGS.itertuples():\n",
" maint, urgent = bool(rng.random() < 0.06), bool(rng.random() < 0.05) # ๋น๊ณํ ์ ๋น / ๊ธด๊ธ ๋ถ์ถ\n",
" v = r.cap * UTIL[r.seg] * lvl[d] * (1.25 if urgent else 1.0) * rng.normal(1, 0.05)\n",
" cap_eff = r.cap * (0.65 if maint else 1.0) * (0.92 if rain else 1.0)\n",
" delay = max(r.t0 * 0.2 * (v / cap_eff) ** 3.5 + 0.25 * prev[r.seg] + rng.normal(0, 0.4), 0.0)\n",
" prev[r.seg] = delay\n",
" rows.append(dict(day=int(d), dow=int(d % 7), seg=r.seg, kind=r.kind, km=r.km, cap=r.cap, t0=r.t0,\n",
" plan_v=v / (1.25 if urgent else 1.0) * rng.normal(1, 0.04), v=v, maint=int(maint), urgent=int(urgent),\n",
" rain=int(rain), cap_eff=cap_eff, delay=delay))\n",
" df = pd.DataFrame(rows)\n",
" df[\"transit\"] = df[\"t0\"] * (1 + 0.08 * df[\"rain\"]) + df[\"delay\"]\n",
" return df\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_data()\n",
"rng = np.random.default_rng(2)\n",
"dirty = clean.copy()\n",
"# ---- ์ค์ผ ์ฃผ์
(๊ฐ์): ๊ฒฐ์ธกยท์์ยท๋จ์ ์ค๋ฅยทํ๊ธฐ ์ค๋ฅยท๋ฏธ๋ฑ๋ก ๊ตฌ๊ฐยท0 ์ฉ๋ยท์ค๋ณต ----\n",
"ix = rng.choice(len(dirty), 40, replace=False)\n",
"dirty.loc[ix[:12], \"v\"] = np.nan\n",
"dirty.loc[ix[12:18], \"v\"] = -dirty.loc[ix[12:18], \"v\"]\n",
"dirty.loc[ix[18:23], \"v\"] = dirty.loc[ix[18:23], \"v\"] * 10\n",
"dirty.loc[ix[23:29], \"transit\"] = np.nan\n",
"dirty.loc[ix[29:32], \"cap\"] = 0\n",
"dirty.loc[ix[32:36], \"seg\"] = dirty.loc[ix[32:36], \"seg\"].str.lower() + \" \"\n",
"ghost = dirty.iloc[ix[36:40]].copy(); ghost[\"seg\"] = \"Z9\" # ๋ง์คํฐ์ ์๋ ๊ตฌ๊ฐ ์ฝ๋๊ฐ ์์ฌ ๋ค์ด์จ ํ\n",
"dirty = pd.concat([dirty, ghost, dirty.iloc[rng.choice(len(dirty), 15, replace=False)]], ignore_index=True)\n",
"planted = {\"๊ฒฐ์ธก ํต๊ณผ๋\": 12, \"์์ ํต๊ณผ๋\": 6, \"๋จ์ ์ค๋ฅ(10๋ฐฐ)\": 5, \"๊ฒฐ์ธก ํต๊ณผ์๊ฐ\": 6, \"์ฉ๋ 0\": 3, \"๊ตฌ๊ฐ ํ๊ธฐ ์ค๋ฅ\": 4, \"๋ฏธ๋ฑ๋ก ๊ตฌ๊ฐ\": 4, \"์ค๋ณต ํ\": 15}\n",
"print(f\"[0] ์ค์ผ ๋ฐ์ดํฐ {len(dirty)}ํ (์๋ณธ {len(clean)}ํ), ์ฃผ์
์ค๋ฅ {sum(planted.values())}๊ฑด: {planted}\")\n",
"\n",
"log, w = [], dirty.copy()\n",
"master = SEGS.set_index(\"seg\")\n",
"\n",
"\n",
"def rule(name, n):\n",
" log.append((len(log) + 1, name, n))\n",
"\n",
"\n",
"# R1 ๊ตฌ๊ฐ ์ฝ๋ ํ๊ธฐ ์ ๊ทํ\n",
"before = w[\"seg\"].copy(); w[\"seg\"] = w[\"seg\"].str.strip().str.upper(); rule(\"๊ตฌ๊ฐ ์ฝ๋ ๊ณต๋ฐฑยท๋์๋ฌธ์ ์ ๊ทํ\", int((before != w[\"seg\"]).sum()))\n",
"# R2 ๋ฏธ๋ฑ๋ก ๊ตฌ๊ฐ ๊ฒฉ๋ฆฌ(์ญ์ ํ์ง ์๊ณ ๋ณ๋ ๋ณด๊ด)\n",
"bad = ~w[\"seg\"].isin(master.index); quarantine = w[bad].copy(); w = w[~bad]; rule(\"๋ฏธ๋ฑ๋ก ๊ตฌ๊ฐ ๊ฒฉ๋ฆฌ(๋ณ๋ ๋ณด๊ด)\", int(bad.sum()))\n",
"# R3 ์ค๋ณต ์ ๊ฑฐ(๊ตฌ๊ฐยท์ผ์ ํค)\n",
"n0 = len(w); w = w.drop_duplicates([\"seg\", \"day\"], keep=\"first\"); rule(\"(๊ตฌ๊ฐ, ์ผ์) ์ค๋ณต ์ ๊ฑฐ\", n0 - len(w))\n",
"# R4 ์ฉ๋ 0/๊ฒฐ์ธก์ ๊ตฌ๊ฐ ๋ง์คํฐ์์ ๋ณต์\n",
"bad = w[\"cap\"] <= 0; w.loc[bad, \"cap\"] = w.loc[bad, \"seg\"].map(master[\"cap\"]); rule(\"์ฉ๋ 0 -> ๊ตฌ๊ฐ ๋ง์คํฐ ๋ณต์\", int(bad.sum()))\n",
"# R5 ์์ ํต๊ณผ๋์ ๊ฒฐ์ธก์ผ๋ก ์ ํ(๋ถํธ ์ค๋ฅ ๊ฐ๋ฅ์ฑ -> ๋ณด๊ฐ ๋์)\n",
"bad = w[\"v\"] < 0; w.loc[bad, \"v\"] = np.nan; rule(\"์์ ํต๊ณผ๋ -> ๊ฒฐ์ธก ์ฒ๋ฆฌ\", int(bad.sum()))\n",
"# R6 ๋จ์ ์ค๋ฅ(์ฉ๋์ 3๋ฐฐ ์ด๊ณผ)๋ 1/10 ๋ณด์ \n",
"bad = w[\"v\"] > 3 * w[\"cap\"]; w.loc[bad, \"v\"] = w.loc[bad, \"v\"] / 10; rule(\"๋จ์ ์ค๋ฅ(์ฉ๋ 3๋ฐฐ ์ด๊ณผ) 1/10 ๋ณด์ \", int(bad.sum()))\n",
"# R7 ๊ฒฐ์ธก ํต๊ณผ๋: ๊ฐ์ ๊ตฌ๊ฐยท๊ฐ์ ์์ผ์ ์ค์๊ฐ์ผ๋ก ๋ณด๊ฐ(๊ณ์ ์ฑ ๋ณด๊ฐ)\n",
"bad = w[\"v\"].isna(); med = w.groupby([\"seg\", \"dow\"])[\"v\"].transform(\"median\"); w.loc[bad, \"v\"] = med[bad]; rule(\"๊ฒฐ์ธก ํต๊ณผ๋ -> ๊ตฌ๊ฐยท์์ผ ์ค์๊ฐ ๋ณด๊ฐ\", int(bad.sum()))\n",
"# R8 ๊ฒฐ์ธก ํต๊ณผ์๊ฐ: ์์ ์ฃผํ + ์ง์ฒด๋ก ์ฌ๊ณ์ฐ\n",
"bad = w[\"transit\"].isna(); w.loc[bad, \"transit\"] = w.loc[bad, \"t0\"] * (1 + 0.08 * w.loc[bad, \"rain\"]) + w.loc[bad, \"delay\"]; rule(\"๊ฒฐ์ธก ํต๊ณผ์๊ฐ ์ฌ๊ณ์ฐ\", int(bad.sum()))\n",
"# R9 ์ง์ฒด ์๊ฐ ์ ํฉ: delay = transit - ์์ ์ฃผํ(์ฐ์ฒ ๋ณด์ ), ์์๋ 0์ผ๋ก\n",
"d2 = (w[\"transit\"] - w[\"t0\"] * (1 + 0.08 * w[\"rain\"])).clip(lower=0); mism = (d2 - w[\"delay\"]).abs() > 1e-6\n",
"w[\"delay\"] = d2; rule(\"์ง์ฒด = ํต๊ณผ์๊ฐ - ์์ ์ฃผํ ์ ํฉ ์ฌ๊ณ์ฐ\", int(mism.sum()))\n",
"# R10 ์ด์์น ํ์: IQR ๋ฐ ์ง์ฒด๋ 'ํ์'๋ง ํ๊ณ , ์ ๋นยท๊ธด๊ธ ๋ถ์ถ ํ์ ์์ธ๋ก ๋ณด์กด(์ญ์ ๊ธ์ง)\n",
"q1, q3 = w[\"delay\"].quantile([.25, .75]); out = w[\"delay\"] > q3 + 3 * (q3 - q1)\n",
"w[\"outlier\"] = out.astype(int); w[\"exception_keep\"] = ((w[\"maint\"] == 1) | (w[\"urgent\"] == 1)).astype(int)\n",
"rule(\"์ง์ฒด ์ด์์น ํ๋๊ทธ(์ญ์ ์์)\", int(out.sum()))\n",
"w = w.sort_values([\"day\", \"seg\"]).reset_index(drop=True)\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",
"# ---- ์ ํฉ์ฑ ๊ฒ์ฆ ----\n",
"checks = {\"(๊ตฌ๊ฐ, ์ผ์) ์ ์ผ\": not w.duplicated([\"seg\", \"day\"]).any(), \"ํ์ ์ปฌ๋ผ ๊ฒฐ์ธก 0\": int(w[[\"seg\", \"v\", \"cap\", \"delay\", \"transit\"]].isna().sum().sum()) == 0,\n",
" \"๊ตฌ๊ฐ ์ฝ๋ ๋ง์คํฐ ํฌํจ\": bool(w[\"seg\"].isin(master.index).all()), \"ํต๊ณผ๋ 0 ์ด์ยท์ฉ๋ 3๋ฐฐ ์ดํ\": bool(((w[\"v\"] >= 0) & (w[\"v\"] <= 3 * w[\"cap\"])).all()),\n",
" \"์ฉ๋ = ๋ง์คํฐ ๊ฐ\": bool((w[\"cap\"] == w[\"seg\"].map(master[\"cap\"])).all()), \"ํต๊ณผ์๊ฐ >= ์์ ์ฃผํ\": bool((w[\"transit\"] >= w[\"t0\"] - 1e-9).all()),\n",
" \"์ง์ฒด >= 0\": bool((w[\"delay\"] >= 0).all())}\n",
"passed = sum(checks.values())\n",
"print(\"\\n[2] ์ ํฉ์ฑ ๊ฒ์ฆ\")\n",
"for k, v in checks.items():\n",
" print(f\" [{'PASS' if v else 'FAIL'}] {k}\")\n",
"print(f\" ํต๊ณผ์จ {passed / len(checks):.0%} (๋ชฉํ 100%)\")\n",
"\n",
"# ---- ๊ตฐ์ ํน์ ์์ธ ๋ณด์กด / ์๋ณธ ๋๋น ๋ณต์ ์ ํ๋ ----\n",
"ex = w[w[\"exception_keep\"] == 1]\n",
"print(f\"\\n[3] ๊ตฐ์ ํน์ ์์ธ ๋ณด์กด: ๋น๊ณํ ์ ๋น {int(w['maint'].sum())}๊ฑด, ๊ธด๊ธ ๋ถ์ถ {int(w['urgent'].sum())}๊ฑด ๋ชจ๋ ์ ์ง(์๋ณธ {int(clean['maint'].sum())}, {int(clean['urgent'].sum())}๊ฑด)\")\n",
"print(f\" ์์ธ ํ ํ๊ท ์ง์ฒด {ex['delay'].mean():.2f}๋ถ vs ์ผ๋ฐ ํ {w.loc[w['exception_keep'] == 0, 'delay'].mean():.2f}๋ถ -> ์ด์์น๋ก ์ง์ ๋ค๋ฉด ํญ์ฆ ๊ตฌ๊ฐ ์ ๋ณด๊ฐ ์ฌ๋ผ์ง๋ค\")\n",
"m = w.merge(clean[[\"seg\", \"day\", \"v\"]], on=[\"seg\", \"day\"], suffixes=(\"\", \"_true\"))\n",
"print(f\" ํต๊ณผ๋ ๋ณต์ ์ค์ฐจ(๋ณด์ ยท๋ณด๊ฐ๋ ํ {int((m['v'] != m['v_true']).sum())}๊ฑด ํฌํจ ์ ์ฒด MAE) {np.abs(m['v'] - m['v_true']).mean():.3f}๋/์\")\n",
"print(\"\\n[4] ์ ํ ๊ธฐ์ ํต๊ณ ๋น๊ต(ํต๊ณผ๋ v / ์ง์ฒด delay)\")\n",
"st = pd.DataFrame({\"์ค์ผ ์๋ณธ\": dirty[[\"v\", \"delay\"]].describe().loc[[\"count\", \"mean\", \"min\", \"max\"]].stack(), \"์ ์ ํ\": w[[\"v\", \"delay\"]].describe().loc[[\"count\", \"mean\", \"min\", \"max\"]].stack(),\n",
" \"์ ๋ต ์๋ณธ\": clean[[\"v\", \"delay\"]].describe().loc[[\"count\", \"mean\", \"min\", \"max\"]].stack()}).round(2)\n",
"print(st.to_string())\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"๐ก [์ฐธ๊ณ ] ๋ฏธ์
02 ์์ ์คํ ๊ฒฐ๊ณผ (Output)
\n",
"\n",
"```text\n",
"[0] ์ค์ผ ๋ฐ์ดํฐ 1519ํ (์๋ณธ 1500ํ), ์ฃผ์
์ค๋ฅ 55๊ฑด: {'๊ฒฐ์ธก ํต๊ณผ๋': 12, '์์ ํต๊ณผ๋': 6, '๋จ์ ์ค๋ฅ(10๋ฐฐ)': 5, '๊ฒฐ์ธก ํต๊ณผ์๊ฐ': 6, '์ฉ๋ 0': 3, '๊ตฌ๊ฐ ํ๊ธฐ ์ค๋ฅ': 4, '๋ฏธ๋ฑ๋ก ๊ตฌ๊ฐ': 4, '์ค๋ณต ํ': 15}\n",
"\n",
"[1] ์ ์ ๊ท์น ์ ์ฉ ๋ก๊ทธ\n",
" ๊ท์น ๋ด์ฉ ์ฒ๋ฆฌ ๊ฑด์\n",
" 1 ๊ตฌ๊ฐ ์ฝ๋ ๊ณต๋ฐฑยท๋์๋ฌธ์ ์ ๊ทํ 4\n",
" 2 ๋ฏธ๋ฑ๋ก ๊ตฌ๊ฐ ๊ฒฉ๋ฆฌ(๋ณ๋ ๋ณด๊ด) 4\n",
" 3 (๊ตฌ๊ฐ, ์ผ์) ์ค๋ณต ์ ๊ฑฐ 15\n",
" 4 ์ฉ๋ 0 -> ๊ตฌ๊ฐ ๋ง์คํฐ ๋ณต์ 3\n",
" 5 ์์ ํต๊ณผ๋ -> ๊ฒฐ์ธก ์ฒ๋ฆฌ 6\n",
" 6 ๋จ์ ์ค๋ฅ(์ฉ๋ 3๋ฐฐ ์ด๊ณผ) 1/10 ๋ณด์ 5\n",
" 7 ๊ฒฐ์ธก ํต๊ณผ๋ -> ๊ตฌ๊ฐยท์์ผ ์ค์๊ฐ ๋ณด๊ฐ 18\n",
" 8 ๊ฒฐ์ธก ํต๊ณผ์๊ฐ ์ฌ๊ณ์ฐ 6\n",
" 9 ์ง์ฒด = ํต๊ณผ์๊ฐ - ์์ ์ฃผํ ์ ํฉ ์ฌ๊ณ์ฐ 0\n",
" 10 ์ง์ฒด ์ด์์น ํ๋๊ทธ(์ญ์ ์์) 43\n",
" ์ ์ ๊ท์น 10์ข
์ ์ฉ(๋ชฉํ 10์ข
: ๋ฌ์ฑ)\n",
"\n",
"[2] ์ ํฉ์ฑ ๊ฒ์ฆ\n",
" [PASS] (๊ตฌ๊ฐ, ์ผ์) ์ ์ผ\n",
" [PASS] ํ์ ์ปฌ๋ผ ๊ฒฐ์ธก 0\n",
" [PASS] ๊ตฌ๊ฐ ์ฝ๋ ๋ง์คํฐ ํฌํจ\n",
" [PASS] ํต๊ณผ๋ 0 ์ด์ยท์ฉ๋ 3๋ฐฐ ์ดํ\n",
" [PASS] ์ฉ๋ = ๋ง์คํฐ ๊ฐ\n",
" [PASS] ํต๊ณผ์๊ฐ >= ์์ ์ฃผํ\n",
" [PASS] ์ง์ฒด >= 0\n",
" ํต๊ณผ์จ 100% (๋ชฉํ 100%)\n",
"\n",
"[3] ๊ตฐ์ ํน์ ์์ธ ๋ณด์กด: ๋น๊ณํ ์ ๋น 95๊ฑด, ๊ธด๊ธ ๋ถ์ถ 87๊ฑด ๋ชจ๋ ์ ์ง(์๋ณธ 95, 87๊ฑด)\n",
" ์์ธ ํ ํ๊ท ์ง์ฒด 4.37๋ถ vs ์ผ๋ฐ ํ 1.60๋ถ -> ์ด์์น๋ก ์ง์ ๋ค๋ฉด ํญ์ฆ ๊ตฌ๊ฐ ์ ๋ณด๊ฐ ์ฌ๋ผ์ง๋ค\n",
" ํต๊ณผ๋ ๋ณต์ ์ค์ฐจ(๋ณด์ ยท๋ณด๊ฐ๋ ํ 18๊ฑด ํฌํจ ์ ์ฒด MAE) 0.104๋/์\n",
"\n",
"[4] ์ ํ ๊ธฐ์ ํต๊ณ ๋น๊ต(ํต๊ณผ๋ v / ์ง์ฒด delay)\n",
" ์ค์ผ ์๋ณธ ์ ์ ํ ์ ๋ต ์๋ณธ\n",
"count v 1507.00 1500.00 1500.00\n",
" delay 1519.00 1500.00 1500.00\n",
"mean v 63.64 62.95 62.95\n",
" delay 1.94 1.93 1.93\n",
"min v -146.18 16.78 16.78\n",
" delay 0.00 0.00 0.00\n",
"max v 635.70 174.46 174.46\n",
" delay 21.79 21.79 21.79\n",
"```\n",
" \n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## ๐ฏ ๋ฏธ์
03. ๊ตฌ๊ฐ๋ณ ํต๊ณผ ์ฉ๋ ๊ธฐ๋ฐ 1์ฐจ ์์ก ์ง์ฒด ์์ธก Baseline ์๊ณ ๋ฆฌ์ฆ ๊ตฌํ\n",
"**์์์๊ฐ**: ์ฝ 2.5์๊ฐ\n",
"\n",
"### ๐ ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ์ํ ์ง์นจ\n",
"#### ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ๋น์์ฑ\n",
"\n",
"ํต๊ณผ ์ฉ๋ ๊ธฐ๋ฐ 1์ฐจ ์์ก ์ง์ฒด ์์ธก Baseline ์๊ณ ๋ฆฌ์ฆ์ ๊ตฌํํ๋ค. ์ค์ ์ ์ ํ์ฅ์์๋ ๋จ์ ํ์ง์ ๊ทธ์น์ง ์๊ณ ์ ์ํ ์ํ ํ๋จ๊ณผ ์ ๋ขฐ๋ ๋์ ์์ฌ๊ฒฐ์ ์ด ํ์์ ์ด๋ฏ๋ก, **๊ตฌ๊ฐ๋ณ ํต๊ณผ ์ฉ๋ ๊ธฐ๋ฐ BPR ์ง์ฒด ํจ์๋ก 1์ฐจ ์์ก ์ง์ฒด ์์ธก Baseline์ ๋ง๋ค๊ณ , EDA๋ก Baseline์ ํ๊ณ์ ํต์ฌ ์์ธ์ ์ฐพ๋๋ค.** ๊ณผ์
์ ์์ํ์ฌ ์คํ์ ๋ฐฉ์งํ๊ณ ์์ ๋์๋ ฅ์ ๊ทน๋ํํด์ผ ํฉ๋๋ค.\n",
"\n",
"์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ณธ ๋ฏธ์
์์๋ **๊ณํ ๋ฌผ๋์ ๊ณต์นญ ์ฉ๋์ผ๋ก ๋๋ ์ด์ฉ๋ฅ ๋ก ๊ตฌ๊ฐ ์ง์ฒด๋ฅผ ๊ณ์ฐํ๊ณ ํต๊ณผ์๊ฐ์ ์ค์ฐจ๋ฅผ ์ธก์ ์ํํ๊ณ ๊ฒ์ฆํ๋** ์ค์ต์ ์ฒด๊ณ์ ์ผ๋ก ์ํํฉ๋๋ค.\n",
"\n",
"ํ์ต์๋ ์ค์ต๊ฐ์ด๋์ ์ ์๋ ์ด 4๊ฐ ์ธ๋ถ ํ๋(๊ธฐ๋ณธ ๊ธฐ๋ฅ ๊ตฌํ โ ๊ฒฐ๊ณผ ์งํ ๊ฒ์ฆ โ ํ๋ผ๋ฏธํฐ ํ๋ ๋ฐ ๋์ ๊ณผ์ )์ ์์ฐจ์ ์ผ๋ก ์งํํ๋ฉฐ ํต์ฌ ํ์ ๊ธฐ์ค์ ๋ฌ์ฑํฉ๋๋ค.\n",
"\n",
"**์ด๋ฒ ๊ฒฐ๊ณผ๋ฌผ:** `notebooks/mov01_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": [
"\"\"\"๊ตฌ๊ฐ๋ณ ํต๊ณผ ์ฉ๋(BPR ํจ์) ๊ธฐ๋ฐ 1์ฐจ ์์ก ์ง์ฒด ์์ธก Baseline์ ๋ง๋ค์ด ์ค์ฐจ๋ฅผ ์ธก์ ํ๊ณ , EDA ์ฐจํธ 8์ข
๊ณผ ์๊ด ํผ์ฒยทํ๋ ํ (์์ 20% ๊ตฌ๊ฐ) ๋ถ์์ผ๋ก 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",
"# ๊ฐ์ ์ถ๋ฐ์ง(SRC)์์ ๊ฐ์ ๋ชฉ์ ์ง(DST)๊น์ง 3๊ฐ ์ถ์ (AยทBยทC)๊ณผ ์ฐ๊ฒฐ๋ก 2๊ฐ, ๊ตฌ๊ฐ 10๊ฐ. 150์ผ์น ํผํฌ์๊ฐ ํต๊ณผ๋ยท์ง์ฒด ์๊ฐ์ ๋ง๋ ๋ค.\n",
"# ---------------------------------------------------------------------------\n",
"SEGS = pd.DataFrame([\n",
" (\"A1\", \"SRC\", \"J1\", 12, 130, \"์ผ๋ฐ\"), (\"A2\", \"J1\", \"J2\", 8, 70, \"๊ต๋\"), (\"A3\", \"J2\", \"DST\", 10, 140, \"๊ต์ฐจ๋ก\"),\n",
" (\"B1\", \"SRC\", \"J3\", 15, 100, \"์ผ๋ฐ\"), (\"B2\", \"J3\", \"J2\", 9, 80, \"ํฐ๋\"), (\"B3\", \"J3\", \"DST\", 14, 95, \"์ผ๋ฐ\"),\n",
" (\"C1\", \"SRC\", \"J4\", 18, 70, \"์ผ๋ฐ\"), (\"C2\", \"J4\", \"DST\", 20, 75, \"๊ต๋\"),\n",
" (\"X1\", \"J1\", \"J3\", 7, 50, \"๊ต์ฐจ๋ก\"), (\"X2\", \"J4\", \"J3\", 10, 60, \"์ผ๋ฐ\")],\n",
" columns=[\"seg\", \"u\", \"v\", \"km\", \"cap\", \"kind\"]) # cap = ํผํฌ์๊ฐ ํต๊ณผ ์ฉ๋(๋/์, ๊ต์ก์ฉ ๊ฐ์ ๊ฐ)\n",
"KSPD = {\"์ผ๋ฐ\": 55, \"๊ต๋\": 40, \"ํฐ๋\": 45, \"๊ต์ฐจ๋ก\": 45} # ๊ตฌ๊ฐ ์ ํ๋ณ ์์ ์ฃผํ ์๋(km/h, ๊ฐ์ ๊ฐ)\n",
"SEGS[\"t0\"] = SEGS[\"km\"] / SEGS[\"kind\"].map(KSPD) * 60 # ์์ ์ฃผํ ์๊ฐ(๋ถ)\n",
"UTIL = dict(A1=.78, A2=.85, A3=.66, B1=.70, B2=.72, B3=.64, C1=.55, C2=.58, X1=.45, X2=.40) # ํ์ ์ด์ฉ๋ฅ \n",
"\n",
"\n",
"def bpr(t0, v, cap, a=0.15, b=4.0):\n",
" \"\"\"BPR ์ง์ฒด ํจ์(๊ตํต๊ณตํ์ ํ์คํ): ํต๊ณผ๋/์ฉ๋ ๋น๊ฐ ์ปค์ง์๋ก ์ง์ฒด๊ฐ ๊ธ์ฆํ๋ค. (๋ถ)\"\"\"\n",
" return t0 * a * (v / cap) ** b\n",
"\n",
"\n",
"def make_data(seed=11, days=150):\n",
" \"\"\"๊ตฌ๊ฐ x ์ผ์ ํจ๋. v=์ค์ ํผํฌ ํต๊ณผ๋, plan_v=์์ก๊ณํ ์๋(๊ธด๊ธ ๋ถ์ถ ๋ฏธ๋ฐ์), delay=์ค์ ์ง์ฒด(๋ถ).\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" d_idx = np.arange(days)\n",
" lvl = np.array([1.0, 1.08, 1.04, 1.10, 1.02, 0.82, 0.78])[d_idx % 7] * (1 + 0.0012 * d_idx)\n",
" lvl = lvl * np.where((d_idx >= 100) & (d_idx < 110), 1.30, 1.0) # ์์ก๋ ํญ์ฆ ๊ธฐ๊ฐ(๊ฐ์)\n",
" prev, rows = {s: 0.0 for s in SEGS[\"seg\"]}, []\n",
" for d in d_idx:\n",
" rain = bool(rng.random() < 0.18)\n",
" for r in SEGS.itertuples():\n",
" maint, urgent = bool(rng.random() < 0.06), bool(rng.random() < 0.05) # ๋น๊ณํ ์ ๋น / ๊ธด๊ธ ๋ถ์ถ\n",
" v = r.cap * UTIL[r.seg] * lvl[d] * (1.25 if urgent else 1.0) * rng.normal(1, 0.05)\n",
" cap_eff = r.cap * (0.65 if maint else 1.0) * (0.92 if rain else 1.0)\n",
" delay = max(r.t0 * 0.2 * (v / cap_eff) ** 3.5 + 0.25 * prev[r.seg] + rng.normal(0, 0.4), 0.0)\n",
" prev[r.seg] = delay\n",
" rows.append(dict(day=int(d), dow=int(d % 7), seg=r.seg, kind=r.kind, km=r.km, cap=r.cap, t0=r.t0,\n",
" plan_v=v / (1.25 if urgent else 1.0) * rng.normal(1, 0.04), v=v, maint=int(maint), urgent=int(urgent),\n",
" rain=int(rain), cap_eff=cap_eff, delay=delay))\n",
" df = pd.DataFrame(rows)\n",
" df[\"transit\"] = df[\"t0\"] * (1 + 0.08 * df[\"rain\"]) + df[\"delay\"]\n",
" return df\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",
"# ์ง์ฒด ์์ธก์ฉ ํ์ ํผ์ฒ ์์ฑ(๋น์ผ ์ค์ ์์ ์ ์ ์ ์๋ ์ ๋ณด๋ง ์ฌ์ฉ: ๊ณํ ๋ฌผ๋, ๊ณต์ง ์ ๋น, ๊ธฐ์ ์๋ณด, ์ ์ผยท์ ์ฃผ ์ค์ )\n",
"# ---------------------------------------------------------------------------\n",
"FEATS = [\"plan_v\", \"cap\", \"cap_eff\", \"t0\", \"km\", \"maint\", \"rain\", \"util\", \"util_eff\", \"util_eff4\", \"bpr_base\", \"lag1_delay\", \"lag7_delay\",\n",
" \"roll7_v\", \"surge_ratio\", \"is_weekend\", \"dow_sin\", \"dow_cos\", \"k_๊ต๋\", \"k_ํฐ๋\", \"k_๊ต์ฐจ๋ก\"]\n",
"\n",
"\n",
"def add_features(df):\n",
" d = df.sort_values([\"seg\", \"day\"]).copy()\n",
" g = d.groupby(\"seg\")\n",
" d[\"lag1_delay\"] = g[\"delay\"].shift(1)\n",
" d[\"lag7_delay\"] = g[\"delay\"].shift(7)\n",
" d[\"roll7_v\"] = g[\"v\"].transform(lambda s: s.shift(1).rolling(7).mean())\n",
" d[\"util\"] = d[\"plan_v\"] / d[\"cap\"]\n",
" d[\"util_eff\"] = d[\"plan_v\"] / d[\"cap_eff\"]\n",
" d[\"util_eff4\"] = d[\"util_eff\"] ** 4\n",
" d[\"bpr_base\"] = bpr(d[\"t0\"], d[\"plan_v\"], d[\"cap\"])\n",
" d[\"surge_ratio\"] = d[\"plan_v\"] / d[\"roll7_v\"]\n",
" d[\"is_weekend\"] = (d[\"dow\"] >= 5).astype(int)\n",
" d[\"dow_sin\"], d[\"dow_cos\"] = np.sin(2 * np.pi * d[\"dow\"] / 7), np.cos(2 * np.pi * d[\"dow\"] / 7)\n",
" for k in (\"๊ต๋\", \"ํฐ๋\", \"๊ต์ฐจ๋ก\"):\n",
" d[\"k_\" + k] = (d[\"kind\"] == k).astype(int)\n",
" return d.dropna().sort_values([\"day\", \"seg\"]).reset_index(drop=True)\n",
"# ---------------------------------------------------------------------------\n",
"\n",
"df = make_data()\n",
"d = add_features(df)\n",
"\n",
"# 1) Baseline: ๊ณํ ๋ฌผ๋ / ๊ณต์นญ ์ฉ๋์ผ๋ก BPR ์ง์ฒด ๊ณ์ฐ(์ ๋นยท๊ธฐ์ยท์ ์ผ ์ง์ฒด ๋ฏธ๋ฐ์)\n",
"d[\"pred_base\"] = d[\"bpr_base\"]\n",
"d[\"pred_transit\"] = d[\"t0\"] + d[\"pred_base\"]\n",
"print(f\"[1] Baseline(BPR) ์ง์ฒด ์์ธก: ํต๊ณผ์๊ฐ MAPE {mape(d.transit, d.pred_transit):.1f}%, ํต๊ณผ์๊ฐ MAE {mae(d.transit, d.pred_transit):.2f}๋ถ, ์ง์ฒด MAE {mae(d.delay, d.pred_base):.2f}๋ถ\")\n",
"bias = (d[\"pred_base\"] - d[\"delay\"])\n",
"grp = pd.DataFrame({\"ํ๊ท ์ค์ฐจ(์์ธก-์ค์ , ๋ถ)\": [bias[d.maint == 1].mean(), bias[d.maint == 0].mean(), bias[d.rain == 1].mean(), bias[d.rain == 0].mean(), bias[d.urgent == 1].mean(), bias[(d.day >= 100) & (d.day < 110)].mean()]},\n",
" index=[\"๋น๊ณํ ์ ๋น์ผ\", \"์ ๋น ์์\", \"์ฐ์ฒ์ผ\", \"๋ง์\", \"๊ธด๊ธ ๋ถ์ถ์ผ\", \"์์ก๋ ํญ์ฆ ๊ธฐ๊ฐ\"]).round(2)\n",
"print(\"[2] ์กฐ๊ฑด๋ณ Baseline ์ค์ฐจ(์์ = ๊ณผ์์์ธก)\")\n",
"print(grp.to_string())\n",
"print(\" -> Baseline์ ์ ๋นยท๊ธด๊ธ ๋ถ์ถยทํญ์ฆ ์กฐ๊ฑด์์ ์ง์ฒด๋ฅผ ๊ณผ์์์ธกํ๋ค(ํ๊ณ).\")\n",
"\n",
"# 2) ์๊ด ๋ถ์: ์ง์ฒด์ ๊ฐ ํผ์ฒ์ ์คํผ์ด๋ง ์๊ดยทp-value\n",
"rows = []\n",
"for f in FEATS:\n",
" r, p = stats.spearmanr(d[f], d[\"delay\"])\n",
" rows.append((f, r, p))\n",
"cor = pd.DataFrame(rows, columns=[\"ํผ์ฒ\", \"์๊ด\", \"p\"]).assign(abs_r=lambda x: x[\"์๊ด\"].abs()).sort_values(\"abs_r\", ascending=False)\n",
"sig = cor[(cor[\"abs_r\"] >= 0.3) & (cor[\"p\"] < 0.05)]\n",
"print(f\"\\n[3] ์ง์ฒด์ ์ ์ํ ์๊ด(|r|>=0.3, p<0.05) ํผ์ฒ {len(sig)}๊ฐ (๋ชฉํ 5๊ฐ ์ด์: {'๋ฌ์ฑ' if len(sig) >= 5 else '๋ฏธ๋ฌ'})\")\n",
"print(sig.head(7)[[\"ํผ์ฒ\", \"์๊ด\", \"p\"]].round(3).to_string(index=False))\n",
"t, p = stats.ttest_ind(d.loc[d.maint == 1, \"delay\"], d.loc[d.maint == 0, \"delay\"], equal_var=False)\n",
"print(f\" ๊ฐ์ค๊ฒ์ : ๋น๊ณํ ์ ๋น์ผ vs ํ์ ์ง์ฒด ํ๊ท ์ฐจ t={t:.1f}, p={p:.2g}\")\n",
"\n",
"# 3) ํ๋ ํ (ABC): ๊ตฌ๊ฐ๋ณ ์ด ์ง์ฒด ๊ธฐ์ฌ\n",
"tot = d.groupby(\"seg\")[\"delay\"].sum().sort_values(ascending=False)\n",
"cum = tot.cumsum() / tot.sum()\n",
"top = tot.index[:max(1, int(np.ceil(len(tot) * 0.2)))]\n",
"print(f\"\\n[4] ํ๋ ํ : ์์ 20% ๊ตฌ๊ฐ({', '.join(top)})์ด ์ด ์ง์ฒด์ {tot[top].sum() / tot.sum():.0%}๋ฅผ ์ฐจ์ง, ์์ 50% ๊ตฌ๊ฐ {cum.iloc[4]:.0%}\")\n",
"\n",
"# 4) EDA ์ฐจํธ 8์ข
\n",
"fig, ax = plt.subplots(2, 4, figsize=(18, 8))\n",
"daily = d.groupby(\"day\")[\"v\"].sum(); ax[0, 0].plot(daily.index, daily.values); ax[0, 0].set_title(\"1 Daily total peak volume\")\n",
"ax[0, 1].hist(d[\"delay\"], bins=40); ax[0, 1].set_title(\"2 Delay distribution\")\n",
"ax[0, 2].scatter(d[\"util\"], d[\"delay\"], s=6, alpha=.4); ax[0, 2].set_title(\"3 Utilization vs delay\")\n",
"mean_seg = d.groupby(\"seg\")[\"delay\"].mean().sort_values(); ax[0, 3].barh(mean_seg.index, mean_seg.values); ax[0, 3].set_title(\"4 Mean delay by segment\")\n",
"ax[1, 0].boxplot([d.loc[d.dow == k, \"delay\"] for k in range(7)]); ax[1, 0].set_title(\"5 Delay by weekday\")\n",
"ax[1, 1].boxplot([d.loc[d.rain == 0, \"delay\"], d.loc[d.rain == 1, \"delay\"]])\n",
"ax[1, 1].set_xticklabels([\"dry\", \"rain\"]); ax[1, 1].set_title(\"6 Rain effect\")\n",
"ax[1, 2].boxplot([d.loc[d.maint == 0, \"delay\"], d.loc[d.maint == 1, \"delay\"]])\n",
"ax[1, 2].set_xticklabels([\"normal\", \"maintenance\"]); ax[1, 2].set_title(\"7 Maintenance effect\")\n",
"ax[1, 3].bar(range(len(tot)), (tot / tot.sum()).values); ax[1, 3].plot(range(len(tot)), cum.values, \"r-o\"); ax[1, 3].set_title(\"8 Pareto of total delay\")\n",
"plt.suptitle(\"MOV01 EDA (synthetic data)\"); 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(BPR) ์ง์ฒด ์์ธก: ํต๊ณผ์๊ฐ MAPE 7.4%, ํต๊ณผ์๊ฐ MAE 1.39๋ถ, ์ง์ฒด MAE 1.23๋ถ\n",
"[2] ์กฐ๊ฑด๋ณ Baseline ์ค์ฐจ(์์ = ๊ณผ์์์ธก)\n",
" ํ๊ท ์ค์ฐจ(์์ธก-์ค์ , ๋ถ)\n",
"๋น๊ณํ ์ ๋น์ผ -4.87\n",
"์ ๋น ์์ -0.96\n",
"์ฐ์ฒ์ผ -1.67\n",
"๋ง์ -1.14\n",
"๊ธด๊ธ ๋ถ์ถ์ผ -2.62\n",
"์์ก๋ ํญ์ฆ ๊ธฐ๊ฐ -2.66\n",
" -> Baseline์ ์ ๋นยท๊ธด๊ธ ๋ถ์ถยทํญ์ฆ ์กฐ๊ฑด์์ ์ง์ฒด๋ฅผ ๊ณผ์์์ธกํ๋ค(ํ๊ณ).\n",
"\n",
"[3] ์ง์ฒด์ ์ ์ํ ์๊ด(|r|>=0.3, p<0.05) ํผ์ฒ 14๊ฐ (๋ชฉํ 5๊ฐ ์ด์: ๋ฌ์ฑ)\n",
" ํผ์ฒ ์๊ด p\n",
" util_eff 0.836 0.0\n",
" util_eff4 0.836 0.0\n",
" bpr_base 0.830 0.0\n",
" util 0.793 0.0\n",
"lag1_delay 0.730 0.0\n",
" plan_v 0.653 0.0\n",
"lag7_delay 0.650 0.0\n",
" ๊ฐ์ค๊ฒ์ : ๋น๊ณํ ์ ๋น์ผ vs ํ์ ์ง์ฒด ํ๊ท ์ฐจ t=8.2, p=1.7e-12\n",
"\n",
"[4] ํ๋ ํ : ์์ 20% ๊ตฌ๊ฐ(A2, A1)์ด ์ด ์ง์ฒด์ 33%๋ฅผ ์ฐจ์ง, ์์ 50% ๊ตฌ๊ฐ 70%\n",
"\n",
"[5] EDA ์ฐจํธ 8์ข
์์ฑ ์๋ฃ(๋ชฉํ 8์ข
์ด์: ๋ฌ์ฑ)\n",
"ํต์ฌ ๋ฐ๊ฒฌ: ์ง์ฒด๋ ์ด์ฉ๋ฅ ์ ๋น์ ํ ํจ์์ด๋ฉฐ ์ ๋นยทํญ์ฆยท๊ธด๊ธ ๋ถ์ถ์ด ๊ฒน์น๋ฉด ๊ธ์ฆํ๋ค. ์ด๋ M4 ํ์ ํผ์ฒ ์ค๊ณ์ ๊ทผ๊ฑฐ๊ฐ ๋๋ค.\n",
"```\n",
" \n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## ๐ฏ ๋ฏธ์
04. ๋คํธ์ํฌ ํ๋ฆ(Max-Flow) ์๊ณ ๋ฆฌ์ฆ ๊ธฐ๋ฐ ๋ณ๋ชฉ ๋ฐ์ ์ง์ ์ฌ์ ์๋ณ ๋ฐ ์ฐํ๋ก ํ์ ๋ชจ๋ธ ๊ณ ๋ํ\n",
"**์์์๊ฐ**: ์ฝ 3.0์๊ฐ\n",
"\n",
"### ๐ ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ์ํ ์ง์นจ\n",
"#### ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ๋น์์ฑ\n",
"\n",
"๋คํธ์ํฌ ํ๋ฆ ์๊ณ ๋ฆฌ์ฆ ๊ธฐ๋ฐ ๋ณ๋ชฉ ์ฌ์ ์๋ณ ๋ฐ ์ฐํ๋ก ํ์ ๋ชจ๋ธ์ ๊ณ ๋ํํ๋ค. ์ค์ ์ ์ ํ์ฅ์์๋ ๋จ์ ํ์ง์ ๊ทธ์น์ง ์๊ณ ์ ์ํ ์ํ ํ๋จ๊ณผ ์ ๋ขฐ๋ ๋์ ์์ฌ๊ฒฐ์ ์ด ํ์์ ์ด๋ฏ๋ก, **์ต๋์ ๋(Max-Flow) ์๊ณ ๋ฆฌ์ฆ์ผ๋ก ๋ณ๋ชฉ ๊ตฌ๊ฐ์ ์ฌ์ ์ ์๋ณํ๊ณ ๊ตฌ๊ฐ ํ์ ์ ์ฐํ๋ก๋ฅผ ์ฐพ์ผ๋ฉฐ, ํ์ ํผ์ฒ ๊ธฐ๋ฐ ๊ธฐ์ค ๋ชจ๋ธ์ ๊ตฌ์ถํ๋ค.** ๊ณผ์
์ ์์ํ์ฌ ์คํ์ ๋ฐฉ์งํ๊ณ ์์ ๋์๋ ฅ์ ๊ทน๋ํํด์ผ ํฉ๋๋ค.\n",
"\n",
"์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ณธ ๋ฏธ์
์์๋ **๋
ธ์ ๋ง์์ ์ถ๋ฐ์ง์์ ๋ชฉ์ ์ง๊น์ง์ ์ต๋ ํต๊ณผ๋ฅ๋ ฅ๊ณผ ์ต์ ์ ๋จ ๊ตฌ๊ฐ์ ๊ตฌํ๊ณ ๊ตฌ๊ฐ๋ณ ์ฆ์ค ๋ฏผ๊ฐ๋๋ฅผ ๋น๊ต ์ํํ๊ณ ๊ฒ์ฆํ๋** ์ค์ต์ ์ฒด๊ณ์ ์ผ๋ก ์ํํฉ๋๋ค.\n",
"\n",
"ํ์ต์๋ ์ค์ต๊ฐ์ด๋์ ์ ์๋ ์ด 4๊ฐ ์ธ๋ถ ํ๋(๊ธฐ๋ณธ ๊ธฐ๋ฅ ๊ตฌํ โ ๊ฒฐ๊ณผ ์งํ ๊ฒ์ฆ โ ํ๋ผ๋ฏธํฐ ํ๋ ๋ฐ ๋์ ๊ณผ์ )์ ์์ฐจ์ ์ผ๋ก ์งํํ๋ฉฐ ํต์ฌ ํ์ ๊ธฐ์ค์ ๋ฌ์ฑํฉ๋๋ค.\n",
"\n",
"**์ด๋ฒ ๊ฒฐ๊ณผ๋ฌผ:** `src/features/mov01_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": [
"\"\"\"์ต๋์ ๋(Max-Flow) ์๊ณ ๋ฆฌ์ฆ์ผ๋ก ๊ฐ์ ๋
ธ์ ๋ง์ ์ต์ ์ ๋จ(๋ณ๋ชฉ) ๊ตฌ๊ฐ์ ์ฌ์ ์๋ณํ๊ณ ๊ตฌ๊ฐ ํ์ ์ ์ฐํ ๊ฒฝ๋ก๋ฅผ ์ฐพ์ผ๋ฉฐ, ํ์ ํผ์ฒ 14์ข
๊ณผ ์๊ณ์ด ๊ต์ฐจ๊ฒ์ฆ์ผ๋ก ์ง์ฒด ์์ธก ๊ธฐ์ค ๋ชจ๋ธ(MAPE)์ ๋ง๋ ๋ค.\"\"\"\n",
"import numpy as np\n",
"import pandas as pd\n",
"import matplotlib.pyplot as plt\n",
"from scipy.optimize import linprog\n",
"from sklearn.ensemble import GradientBoostingRegressor\n",
"from sklearn.model_selection import TimeSeriesSplit\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# [๊ต์ก์ฉ๊ฐ์์๋ฃ] ๊ฐ์ ๋
ธ์ ๋งยท๊ตฌ๊ฐ ํต๊ณผ๋ ์์ฑ๊ธฐ (์ค์ ๋๋กยท์ง๋ช
ยท๋ถ๋ ์์)\n",
"# ๊ฐ์ ์ถ๋ฐ์ง(SRC)์์ ๊ฐ์ ๋ชฉ์ ์ง(DST)๊น์ง 3๊ฐ ์ถ์ (AยทBยทC)๊ณผ ์ฐ๊ฒฐ๋ก 2๊ฐ, ๊ตฌ๊ฐ 10๊ฐ. 150์ผ์น ํผํฌ์๊ฐ ํต๊ณผ๋ยท์ง์ฒด ์๊ฐ์ ๋ง๋ ๋ค.\n",
"# ---------------------------------------------------------------------------\n",
"SEGS = pd.DataFrame([\n",
" (\"A1\", \"SRC\", \"J1\", 12, 130, \"์ผ๋ฐ\"), (\"A2\", \"J1\", \"J2\", 8, 70, \"๊ต๋\"), (\"A3\", \"J2\", \"DST\", 10, 140, \"๊ต์ฐจ๋ก\"),\n",
" (\"B1\", \"SRC\", \"J3\", 15, 100, \"์ผ๋ฐ\"), (\"B2\", \"J3\", \"J2\", 9, 80, \"ํฐ๋\"), (\"B3\", \"J3\", \"DST\", 14, 95, \"์ผ๋ฐ\"),\n",
" (\"C1\", \"SRC\", \"J4\", 18, 70, \"์ผ๋ฐ\"), (\"C2\", \"J4\", \"DST\", 20, 75, \"๊ต๋\"),\n",
" (\"X1\", \"J1\", \"J3\", 7, 50, \"๊ต์ฐจ๋ก\"), (\"X2\", \"J4\", \"J3\", 10, 60, \"์ผ๋ฐ\")],\n",
" columns=[\"seg\", \"u\", \"v\", \"km\", \"cap\", \"kind\"]) # cap = ํผํฌ์๊ฐ ํต๊ณผ ์ฉ๋(๋/์, ๊ต์ก์ฉ ๊ฐ์ ๊ฐ)\n",
"KSPD = {\"์ผ๋ฐ\": 55, \"๊ต๋\": 40, \"ํฐ๋\": 45, \"๊ต์ฐจ๋ก\": 45} # ๊ตฌ๊ฐ ์ ํ๋ณ ์์ ์ฃผํ ์๋(km/h, ๊ฐ์ ๊ฐ)\n",
"SEGS[\"t0\"] = SEGS[\"km\"] / SEGS[\"kind\"].map(KSPD) * 60 # ์์ ์ฃผํ ์๊ฐ(๋ถ)\n",
"UTIL = dict(A1=.78, A2=.85, A3=.66, B1=.70, B2=.72, B3=.64, C1=.55, C2=.58, X1=.45, X2=.40) # ํ์ ์ด์ฉ๋ฅ \n",
"\n",
"\n",
"def bpr(t0, v, cap, a=0.15, b=4.0):\n",
" \"\"\"BPR ์ง์ฒด ํจ์(๊ตํต๊ณตํ์ ํ์คํ): ํต๊ณผ๋/์ฉ๋ ๋น๊ฐ ์ปค์ง์๋ก ์ง์ฒด๊ฐ ๊ธ์ฆํ๋ค. (๋ถ)\"\"\"\n",
" return t0 * a * (v / cap) ** b\n",
"\n",
"\n",
"def make_data(seed=11, days=150):\n",
" \"\"\"๊ตฌ๊ฐ x ์ผ์ ํจ๋. v=์ค์ ํผํฌ ํต๊ณผ๋, plan_v=์์ก๊ณํ ์๋(๊ธด๊ธ ๋ถ์ถ ๋ฏธ๋ฐ์), delay=์ค์ ์ง์ฒด(๋ถ).\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" d_idx = np.arange(days)\n",
" lvl = np.array([1.0, 1.08, 1.04, 1.10, 1.02, 0.82, 0.78])[d_idx % 7] * (1 + 0.0012 * d_idx)\n",
" lvl = lvl * np.where((d_idx >= 100) & (d_idx < 110), 1.30, 1.0) # ์์ก๋ ํญ์ฆ ๊ธฐ๊ฐ(๊ฐ์)\n",
" prev, rows = {s: 0.0 for s in SEGS[\"seg\"]}, []\n",
" for d in d_idx:\n",
" rain = bool(rng.random() < 0.18)\n",
" for r in SEGS.itertuples():\n",
" maint, urgent = bool(rng.random() < 0.06), bool(rng.random() < 0.05) # ๋น๊ณํ ์ ๋น / ๊ธด๊ธ ๋ถ์ถ\n",
" v = r.cap * UTIL[r.seg] * lvl[d] * (1.25 if urgent else 1.0) * rng.normal(1, 0.05)\n",
" cap_eff = r.cap * (0.65 if maint else 1.0) * (0.92 if rain else 1.0)\n",
" delay = max(r.t0 * 0.2 * (v / cap_eff) ** 3.5 + 0.25 * prev[r.seg] + rng.normal(0, 0.4), 0.0)\n",
" prev[r.seg] = delay\n",
" rows.append(dict(day=int(d), dow=int(d % 7), seg=r.seg, kind=r.kind, km=r.km, cap=r.cap, t0=r.t0,\n",
" plan_v=v / (1.25 if urgent else 1.0) * rng.normal(1, 0.04), v=v, maint=int(maint), urgent=int(urgent),\n",
" rain=int(rain), cap_eff=cap_eff, delay=delay))\n",
" df = pd.DataFrame(rows)\n",
" df[\"transit\"] = df[\"t0\"] * (1 + 0.08 * df[\"rain\"]) + df[\"delay\"]\n",
" return df\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",
"# ์ง์ฒด ์์ธก์ฉ ํ์ ํผ์ฒ ์์ฑ(๋น์ผ ์ค์ ์์ ์ ์ ์ ์๋ ์ ๋ณด๋ง ์ฌ์ฉ: ๊ณํ ๋ฌผ๋, ๊ณต์ง ์ ๋น, ๊ธฐ์ ์๋ณด, ์ ์ผยท์ ์ฃผ ์ค์ )\n",
"# ---------------------------------------------------------------------------\n",
"FEATS = [\"plan_v\", \"cap\", \"cap_eff\", \"t0\", \"km\", \"maint\", \"rain\", \"util\", \"util_eff\", \"util_eff4\", \"bpr_base\", \"lag1_delay\", \"lag7_delay\",\n",
" \"roll7_v\", \"surge_ratio\", \"is_weekend\", \"dow_sin\", \"dow_cos\", \"k_๊ต๋\", \"k_ํฐ๋\", \"k_๊ต์ฐจ๋ก\"]\n",
"\n",
"\n",
"def add_features(df):\n",
" d = df.sort_values([\"seg\", \"day\"]).copy()\n",
" g = d.groupby(\"seg\")\n",
" d[\"lag1_delay\"] = g[\"delay\"].shift(1)\n",
" d[\"lag7_delay\"] = g[\"delay\"].shift(7)\n",
" d[\"roll7_v\"] = g[\"v\"].transform(lambda s: s.shift(1).rolling(7).mean())\n",
" d[\"util\"] = d[\"plan_v\"] / d[\"cap\"]\n",
" d[\"util_eff\"] = d[\"plan_v\"] / d[\"cap_eff\"]\n",
" d[\"util_eff4\"] = d[\"util_eff\"] ** 4\n",
" d[\"bpr_base\"] = bpr(d[\"t0\"], d[\"plan_v\"], d[\"cap\"])\n",
" d[\"surge_ratio\"] = d[\"plan_v\"] / d[\"roll7_v\"]\n",
" d[\"is_weekend\"] = (d[\"dow\"] >= 5).astype(int)\n",
" d[\"dow_sin\"], d[\"dow_cos\"] = np.sin(2 * np.pi * d[\"dow\"] / 7), np.cos(2 * np.pi * d[\"dow\"] / 7)\n",
" for k in (\"๊ต๋\", \"ํฐ๋\", \"๊ต์ฐจ๋ก\"):\n",
" d[\"k_\" + k] = (d[\"kind\"] == k).astype(int)\n",
" return d.dropna().sort_values([\"day\", \"seg\"]).reset_index(drop=True)\n",
"# ---------------------------------------------------------------------------\n",
"# ---------------------------------------------------------------------------\n",
"# ๋
ธ์ ๋ง ํ๋ฆ ๋๊ตฌ: ์ต๋์ ๋(Edmonds-Karp), ์ฆ๋ถ ๋ฐฐ์ (ํผ์ก ๋ฐ์ ์ต๋จ์๊ฐ ๊ฒฝ๋ก), ๊ฒฝ๋ก ์ด๊ฑฐ\n",
"# ---------------------------------------------------------------------------\n",
"CAP = dict(zip(SEGS[\"seg\"], SEGS[\"cap\"]))\n",
"T0 = dict(zip(SEGS[\"seg\"], SEGS[\"t0\"]))\n",
"KM = dict(zip(SEGS[\"seg\"], SEGS[\"km\"]))\n",
"ARCS = {r.seg: (r.u, r.v) for r in SEGS.itertuples()}\n",
"SRC, DST = \"SRC\", \"DST\"\n",
"\n",
"\n",
"def seg_time(s, flow, cap=None, a=0.2, b=3.5):\n",
" \"\"\"๊ตฌ๊ฐ ํต๊ณผ์๊ฐ(๋ถ) = ์์ ์ฃผํ + ํผ์ก ์ง์ฒด. cap์ ์ฃผ๋ฉด ์ฉ๋์ ๋์ฒด(์ ๋นยท๊ธฐ์ ๋ฐ์).\"\"\"\n",
" r = flow / (cap or CAP[s])\n",
" return T0[s] * (1 + a * r ** b + 6.0 * max(0.0, r - 1.0)) # ์ฉ๋ ์ด๊ณผ๋ถ์ ๋๊ธฐ์ด๋ก ๊ธ์ฆ\n",
"\n",
"\n",
"def max_flow(closed=(), capmul=None):\n",
" \"\"\"์ต๋์ ๋(๋/์)๊ณผ ์ต์ ์ ๋จ ๊ตฌ๊ฐ(๋ณ๋ชฉ). capmul = {๊ตฌ๊ฐ: ์ฉ๋ ๋ฐฐ์จ}.\"\"\"\n",
" cap = {s: (0 if s in closed else CAP[s] * (capmul or {}).get(s, 1.0)) for s in CAP}\n",
" res = {}\n",
" for s, (u, v) in ARCS.items():\n",
" res[(u, v)] = res.get((u, v), 0) + cap[s]\n",
" res.setdefault((v, u), 0)\n",
" flow = 0.0\n",
" while True:\n",
" prev, q = {SRC: None}, [SRC]\n",
" for u in q:\n",
" for (a, b), c in res.items():\n",
" if a == u and c > 1e-9 and b not in prev:\n",
" prev[b] = u; q.append(b)\n",
" if DST not in prev:\n",
" break\n",
" path, x = [], DST\n",
" while prev[x]:\n",
" path.append((prev[x], x)); x = prev[x]\n",
" f = min(res[e] for e in path)\n",
" for a, b in path:\n",
" res[(a, b)] -= f; res[(b, a)] += f\n",
" flow += f\n",
" reach = set(prev)\n",
" cut = [s for s, (u, v) in ARCS.items() if u in reach and v not in reach and cap[s] > 0]\n",
" return flow, cut\n",
"\n",
"\n",
"def all_paths():\n",
" \"\"\"SRC -> DST ๋จ์ ๊ฒฝ๋ก(๊ตฌ๊ฐ ์ด๋ฆ ํํ) ์ ์ฒด.\"\"\"\n",
" out = []\n",
"\n",
" def dfs(node, segs, seen):\n",
" if node == DST:\n",
" out.append(tuple(segs)); return\n",
" for s, (u, v) in ARCS.items():\n",
" if u == node and v not in seen:\n",
" dfs(v, segs + [s], seen | {v})\n",
" dfs(SRC, [], {SRC})\n",
" return out\n",
"\n",
"\n",
"PATHS = all_paths()\n",
"\n",
"\n",
"def assign(demand, closed=(), capmul=None, steps=40):\n",
" \"\"\"์ฆ๋ถ ๋ฐฐ์ : ์์๋ฅผ steps์กฐ๊ฐ์ผ๋ก ๋๋ , ์ ์ฒด ํต๊ณผ์๊ฐ(๋์ x ์๊ฐ) ์ฆ๊ฐ๊ฐ ๊ฐ์ฅ ์์ ๊ฒฝ๋ก์ ์ฐจ๋ก๋ก ์น๋๋ค(์์คํ
์ต์ ๊ทผ์ฌ).\"\"\"\n",
" cap = {s: CAP[s] * (capmul or {}).get(s, 1.0) for s in CAP}\n",
" flow, pf = {s: 0.0 for s in CAP}, {}\n",
" ok = [p for p in PATHS if not any(s in closed for s in p)]\n",
" if not ok:\n",
" return None\n",
" for _ in range(steps):\n",
" chunk = demand / steps\n",
" best = min(ok, key=lambda p: sum((flow[s] + chunk) * seg_time(s, flow[s] + chunk, cap[s]) - flow[s] * seg_time(s, flow[s], cap[s]) for s in p))\n",
" for s in best:\n",
" flow[s] += chunk\n",
" pf[best] = pf.get(best, 0) + chunk\n",
" tt = {p: sum(seg_time(s, flow[s], cap[s]) for s in p) for p in pf}\n",
" avg = sum(pf[p] * tt[p] for p in pf) / demand\n",
" return {\"flow\": flow, \"paths\": pf, \"path_min\": tt, \"avg_min\": avg, \"util\": {s: flow[s] / cap[s] for s in cap if cap[s] > 0}}\n",
"\n",
"\n",
"FIXED_SHARE = {(\"A1\", \"A2\", \"A3\"): 0.32, (\"B1\", \"B3\"): 0.40, (\"C1\", \"C2\"): 0.28} # ๊ณผ๊ฑฐ ๊ดํ: ์ถ์ ๋ณ ๊ณ ์ ๋ฐฐ๋ถ(๊ฐ์)\n",
"\n",
"\n",
"def fixed_assign(demand, closed=(), capmul=None):\n",
" \"\"\"๊ณผ๊ฑฐ ๊ดํ ๋ฐฐ๋ถ: ์ถ์ ๋ณ ๊ณ ์ ๋น์จ. ๋งํ ์ถ์ ๋ฌผ๋์ ์ด๋ ค ์๋ ์ถ์ ์ ๋น์จ๋๋ก ์ฌ๋ถ๋ฐฐ.\"\"\"\n",
" cap = {s: CAP[s] * (capmul or {}).get(s, 1.0) for s in CAP}\n",
" live = {p: w for p, w in FIXED_SHARE.items() if not any(s in closed for s in p)}\n",
" if not live:\n",
" return None\n",
" tot = sum(live.values())\n",
" flow = {s: 0.0 for s in CAP}\n",
" pf = {p: demand * w / tot for p, w in live.items()}\n",
" for p, f in pf.items():\n",
" for s in p:\n",
" flow[s] += f\n",
" tt = {p: sum(seg_time(s, flow[s], cap[s]) for s in p) for p in pf}\n",
" return {\"flow\": flow, \"paths\": pf, \"path_min\": tt, \"avg_min\": sum(pf[p] * tt[p] for p in pf) / demand,\n",
" \"util\": {s: flow[s] / cap[s] for s in cap if cap[s] > 0}}\n",
"\n",
"\n",
"def lp_assign(demand, closed=(), capmul=None):\n",
" \"\"\"์ ํ๊ณํ(LP): ๊ตฌ๊ฐ ์ฉ๋ ์ํ(์ด์ฉ๋ฅ rho) ์๋์์ ์์ ์ฃผํ์๊ฐ ํฉ์ ์ต์ํํ๋ ๊ฒฝ๋ก๋ณ ๋ฌผ๋. ๋ถ๊ฐ๋ฅํ๋ฉด rho๋ฅผ 1.0๊น์ง ์ํ.\"\"\"\n",
" cap = {s: CAP[s] * (capmul or {}).get(s, 1.0) for s in CAP}\n",
" ok = [p for p in PATHS if not any(s in closed for s in p)]\n",
" if not ok:\n",
" return None\n",
" for rho in (0.85, 0.95, 1.0):\n",
" r = linprog([sum(T0[s] for s in p) for p in ok], A_ub=[[1.0 if s in p else 0.0 for p in ok] for s in CAP],\n",
" b_ub=[rho * cap[s] for s in CAP], A_eq=[[1.0] * len(ok)], b_eq=[demand], bounds=(0, None), method=\"highs\")\n",
" if r.status == 0:\n",
" break\n",
" else:\n",
" return assign(demand, closed, capmul) # ์ฉ๋ ์ด๊ณผ ์์: ์ฆ๋ถ ๋ฐฐ์ ์ผ๋ก ๋์ฒด(๊ณผ๋ถํ ํ์)\n",
" flow = {s: sum(r.x[i] for i, p in enumerate(ok) if s in p) for s in CAP}\n",
" pf = {p: float(r.x[i]) for i, p in enumerate(ok) if r.x[i] > 1e-6}\n",
" tt = {p: sum(seg_time(s, flow[s], cap[s]) for s in p) for p in pf}\n",
" return {\"flow\": flow, \"paths\": pf, \"path_min\": tt, \"avg_min\": sum(pf[p] * tt[p] for p in pf) / demand,\n",
" \"util\": {s: flow[s] / cap[s] for s in cap if cap[s] > 0}, \"rho\": rho}\n",
"\n",
"\n",
"def cost_of(res, tv=1000, fuel_km=450):\n",
" \"\"\"์๊ฐ๋น ๋น์ฉ(์, ๊ต์ก์ฉ ๊ฐ์ ): ๋๋น ์๊ฐ๋น์ฉ tv์/๋ถ + ์ ๋ฅ๋น fuel_km์/km.\"\"\"\n",
" return sum(f * (res[\"path_min\"][p] * tv + sum(KM[s] for s in p) * fuel_km) for p, f in res[\"paths\"].items())\n",
"# ---------------------------------------------------------------------------\n",
"\n",
"# 1) ์ต๋์ ๋๊ณผ ๋ณ๋ชฉ(์ต์ ์ ๋จ)\n",
"f0, cut0 = max_flow()\n",
"print(f\"[1] ๋
ธ์ ๋ง ์ต๋ ํต๊ณผ๋ฅ๋ ฅ {f0:.0f}๋/์, ์ต์ ์ ๋จ(๋ณ๋ชฉ) ๊ตฌ๊ฐ: {', '.join(cut0)}\")\n",
"gain = []\n",
"for s in CAP:\n",
" g, _ = max_flow(capmul={s: 1.2})\n",
" gain.append((s, SEGS.set_index(\"seg\").loc[s, \"kind\"], CAP[s], g - f0))\n",
"gain = pd.DataFrame(gain, columns=[\"๊ตฌ๊ฐ\", \"์ ํ\", \"์ฉ๋\", \"์ฉ๋ 20% ์ฆ์ค ์ ์ฆ๊ฐ(๋/์)\"]).sort_values(\"์ฉ๋ 20% ์ฆ์ค ์ ์ฆ๊ฐ(๋/์)\", ascending=False)\n",
"print(\"[2] ๊ตฌ๊ฐ๋ณ ์ฆ์ค ๋ฏผ๊ฐ๋(๋ณ๋ชฉ์ผ์๋ก ์ฆ์ค ํจ๊ณผ๊ฐ ํผ)\")\n",
"print(gain.head(5).to_string(index=False))\n",
"\n",
"# 2) ์ฌ์ ๋ณ๋ชฉ ์๋ณ: ๊ณํ ๋ฌผ๋ ๊ธฐ์ค ์ด์ฉ๋ฅ >= 0.9์ธ ๊ตฌ๊ฐ + ์ต์ ์ ๋จ ๊ตฌ๊ฐ\n",
"df = make_data(); d = add_features(df)\n",
"today = d[d.day == d.day.max()].set_index(\"seg\")\n",
"risk = today[(today.util_eff >= 0.9)].index.tolist()\n",
"print(f\"\\n[3] ์ต์ข
์ผ(day {int(d.day.max())}) ์ฌ์ ๋ณ๋ชฉ ๊ฒฝ๋ณด: ์ ํจ ์ด์ฉ๋ฅ 0.9 ์ด์ ๊ตฌ๊ฐ {risk or '์์'}\")\n",
"\n",
"# 3) ๊ตฌ๊ฐ ํ์ ์ ์ฐํ: ์ต๋์ ๋ ๋ณํ์ ์ฐํ ๊ฒฝ๋ก\n",
"D = 200\n",
"print(f\"\\n[4] ๊ตฌ๊ฐ ํ์ ์๋๋ฆฌ์ค(์์ {D}๋/์): ์ฐํ ๊ฒฝ๋ก์ ํ๊ท ํต๊ณผ์๊ฐ\")\n",
"base = assign(D)\n",
"rows = [(\"ํ์\", \"-\", f0, base[\"avg_min\"], \"AยทBยทC ๋ถ์ฐ\", \"์์ฉ\")]\n",
"for c in ((\"A2\",), (\"B2\",), (\"A3\",)):\n",
" mf, cut = max_flow(c)\n",
" r = assign(D, closed=c)\n",
" top = max(r[\"paths\"], key=r[\"paths\"].get)\n",
" rows.append((f\"{c[0]} ํ์\", \",\".join(cut), mf, r[\"avg_min\"], \"->\".join(top), \"์์ฉ\" if D <= mf else \"๊ณผ๋ถํ\"))\n",
"sc = pd.DataFrame(rows, columns=[\"์๋๋ฆฌ์ค\", \"์ ๋ณ๋ชฉ\", \"์ต๋์ ๋\", \"ํ๊ท ํต๊ณผ(๋ถ)\", \"์ฃผ ๊ฒฝ๋ก\", \"์์ ๋๋น\"]).round(1)\n",
"print(sc.to_string(index=False))\n",
"print(f\" A2 ํ์ ์ ํ๊ท ํต๊ณผ์๊ฐ +{rows[1][3] - rows[0][3]:.1f}๋ถ, ์์ {D}๋/์ ๋๋น ์ต๋์ ๋ {rows[1][2]:.0f}๋/์ -> {rows[1][5]}(๊ณผ๋ถํ๋ฉด ์ฐํ๋ง์ผ๋ก ๋ถ์กฑํ๋ฏ๋ก ๋ฌผ๋ ์กฐ์ ยทํ ์๋จ ๊ฒํ ๊ฐ ํ์)\")\n",
"\n",
"# 4) ํ์ ํผ์ฒ๋ก ์ง์ฒด ์์ธก ๊ธฐ์ค ๋ชจ๋ธ: ์๊ณ์ด ๊ต์ฐจ๊ฒ์ฆ(TimeSeriesSplit)\n",
"print(f\"\\n[5] ํ์ ํผ์ฒ {len([f for f in FEATS if f not in ['plan_v','cap','cap_eff','t0','km','maint','rain']])}๊ฐ ์์ฑ(lagยท์ด์ฉ๋ฅ ยทBPRยทํญ์ฆ๋นยท์์ผ ์ํยท๊ตฌ๊ฐ ๋๋ฏธ ๋ฑ)\")\n",
"days = np.sort(d.day.unique())\n",
"tscv = TimeSeriesSplit(n_splits=4)\n",
"res = []\n",
"for k, (a, b) in enumerate(tscv.split(days)):\n",
" tr, te = d[d.day.isin(days[a])], d[d.day.isin(days[b])]\n",
" m = GradientBoostingRegressor(n_estimators=150, max_depth=3, learning_rate=0.08, random_state=0).fit(tr[FEATS], tr[\"delay\"])\n",
" p = te[\"t0\"] * (1 + 0.08 * te[\"rain\"]) + m.predict(te[FEATS])\n",
" pb = te[\"t0\"] + te[\"bpr_base\"]\n",
" res.append((k + 1, len(tr), len(te), mape(te[\"transit\"], pb), mape(te[\"transit\"], p)))\n",
"cv = pd.DataFrame(res, columns=[\"fold\", \"ํ์ต\", \"๊ฒ์ฆ\", \"BPR MAPE(%)\", \"๊ธฐ์ค๋ชจ๋ธ MAPE(%)\"]).round(2)\n",
"print(cv.to_string(index=False))\n",
"mm = cv[\"๊ธฐ์ค๋ชจ๋ธ MAPE(%)\"].mean()\n",
"print(f\" ๊ต์ฐจ๊ฒ์ฆ ํ๊ท MAPE {mm:.2f}% (๋ชฉํ 25% ์ดํ: {'๋ฌ์ฑ' if mm <= 25 else '๋ฏธ๋ฌ'}), ํด๋๊ฐ ๋ค๋ก ๊ฐ์๋ก ํ์ต ๊ตฌ๊ฐ์ด ๊ธธ์ด์ ธ ๋ฏธ๋ ์ ๋ณด ๋์๊ฐ ์๋ค\")\n",
"m = GradientBoostingRegressor(n_estimators=150, max_depth=3, learning_rate=0.08, random_state=0).fit(d[FEATS], d[\"delay\"])\n",
"imp = pd.Series(m.feature_importances_, index=FEATS).sort_values(ascending=False)\n",
"print(\" ํผ์ฒ ์ค์๋ ์์ 5:\", \", \".join(f\"{k} {v:.2f}\" for k, v in imp.head(5).items()), f\"/ ์ค์๋ 1% ๋ฏธ๋ง {int((imp < 0.01).sum())}๊ฐ(์ฐจ์ ์ถ์ ํ๋ณด)\")\n",
"\n",
"fig, ax = plt.subplots(1, 2, figsize=(12, 4.5))\n",
"for s, (u, v) in ARCS.items():\n",
" pos = {\"SRC\": (0, 1.5), \"J1\": (1, 3), \"J2\": (2, 3), \"J3\": (1.3, 1.5), \"J4\": (1, 0), \"DST\": (3, 1.5)}\n",
" ax[0].plot([pos[u][0], pos[v][0]], [pos[u][1], pos[v][1]], \"r-\" if s in cut0 else \"gray\", lw=2 + CAP[s] / 40)\n",
" ax[0].text((pos[u][0] + pos[v][0]) / 2, (pos[u][1] + pos[v][1]) / 2, s, fontsize=9)\n",
"ax[0].set_title(\"Network (red = min-cut bottleneck)\"); ax[0].axis(\"off\")\n",
"imp.head(10)[::-1].plot.barh(ax=ax[1]); ax[1].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] ๋
ธ์ ๋ง ์ต๋ ํต๊ณผ๋ฅ๋ ฅ 290๋/์, ์ต์ ์ ๋จ(๋ณ๋ชฉ) ๊ตฌ๊ฐ: A2, B1, C1, X1\n",
"[2] ๊ตฌ๊ฐ๋ณ ์ฆ์ค ๋ฏผ๊ฐ๋(๋ณ๋ชฉ์ผ์๋ก ์ฆ์ค ํจ๊ณผ๊ฐ ํผ)\n",
"๊ตฌ๊ฐ ์ ํ ์ฉ๋ ์ฉ๋ 20% ์ฆ์ค ์ ์ฆ๊ฐ(๋/์)\n",
"B1 ์ผ๋ฐ 100 15.0\n",
"C1 ์ผ๋ฐ 70 14.0\n",
"X1 ๊ต์ฐจ๋ก 50 10.0\n",
"A2 ๊ต๋ 70 10.0\n",
"A1 ์ผ๋ฐ 130 0.0\n",
"\n",
"[3] ์ต์ข
์ผ(day 149) ์ฌ์ ๋ณ๋ชฉ ๊ฒฝ๋ณด: ์ ํจ ์ด์ฉ๋ฅ 0.9 ์ด์ ๊ตฌ๊ฐ ['A1', 'A2']\n",
"\n",
"[4] ๊ตฌ๊ฐ ํ์ ์๋๋ฆฌ์ค(์์ 200๋/์): ์ฐํ ๊ฒฝ๋ก์ ํ๊ท ํต๊ณผ์๊ฐ\n",
" ์๋๋ฆฌ์ค ์ ๋ณ๋ชฉ ์ต๋์ ๋ ํ๊ท ํต๊ณผ(๋ถ) ์ฃผ ๊ฒฝ๋ก ์์ ๋๋น\n",
" ํ์ - 290.0 40.9 AยทBยทC ๋ถ์ฐ ์์ฉ\n",
"A2 ํ์ B1,C1,X1 220.0 44.9 B1->B3 ์์ฉ\n",
"B2 ํ์ A2,B3,C1 235.0 41.0 B1->B3 ์์ฉ\n",
"A3 ํ์ B3,C1 165.0 70.9 B1->B3 ๊ณผ๋ถํ\n",
" A2 ํ์ ์ ํ๊ท ํต๊ณผ์๊ฐ +4.0๋ถ, ์์ 200๋/์ ๋๋น ์ต๋์ ๋ 220๋/์ -> ์์ฉ(๊ณผ๋ถํ๋ฉด ์ฐํ๋ง์ผ๋ก ๋ถ์กฑํ๋ฏ๋ก ๋ฌผ๋ ์กฐ์ ยทํ ์๋จ ๊ฒํ ๊ฐ ํ์)\n",
"\n",
"[5] ํ์ ํผ์ฒ 14๊ฐ ์์ฑ(lagยท์ด์ฉ๋ฅ ยทBPRยทํญ์ฆ๋นยท์์ผ ์ํยท๊ตฌ๊ฐ ๋๋ฏธ ๋ฑ)\n",
" fold ํ์ต ๊ฒ์ฆ BPR MAPE(%) ๊ธฐ์ค๋ชจ๋ธ MAPE(%)\n",
" 1 310 280 6.72 3.00\n",
" 2 590 280 6.02 2.54\n",
" 3 870 280 9.16 3.93\n",
" 4 1150 280 9.24 2.78\n",
" ๊ต์ฐจ๊ฒ์ฆ ํ๊ท MAPE 3.06% (๋ชฉํ 25% ์ดํ: ๋ฌ์ฑ), ํด๋๊ฐ ๋ค๋ก ๊ฐ์๋ก ํ์ต ๊ตฌ๊ฐ์ด ๊ธธ์ด์ ธ ๋ฏธ๋ ์ ๋ณด ๋์๊ฐ ์๋ค\n",
" ํผ์ฒ ์ค์๋ ์์ 5: util_eff 0.35, util_eff4 0.34, bpr_base 0.13, lag1_delay 0.09, km 0.02 / ์ค์๋ 1% ๋ฏธ๋ง 14๊ฐ(์ฐจ์ ์ถ์ ํ๋ณด)\n",
"```\n",
" \n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## ๐ฏ ๋ฏธ์
05. ์ฐํ ์์ก ์ ์ถ๊ฐ ์์ ์๊ฐ ๋ฐ ์ ๋ฅ ์๋ชจ๋ ์ฐ์ถ ์์ง ๊ฐ๋ฐ\n",
"**์์์๊ฐ**: ์ฝ 3.5์๊ฐ\n",
"\n",
"### ๐ ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ์ํ ์ง์นจ\n",
"#### ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ๋น์์ฑ\n",
"\n",
"์ฐํ ์์ก ์ ์ถ๊ฐ ์์ ์๊ฐ ๋ฐ ์ ๋ฅ ์๋ชจ๋ ์ฐ์ถ ์์ง์ ๊ฐ๋ฐํ๋ค. ์ค์ ์ ์ ํ์ฅ์์๋ ๋จ์ ํ์ง์ ๊ทธ์น์ง ์๊ณ ์ ์ํ ์ํ ํ๋จ๊ณผ ์ ๋ขฐ๋ ๋์ ์์ฌ๊ฒฐ์ ์ด ํ์์ ์ด๋ฏ๋ก, **์ฐํ ์์ก ์ ์ถ๊ฐ ์์ ์๊ฐ๊ณผ ์ ๋ฅ ์๋ชจ๋์ ์ฐ์ถํ๋ ์์ง์ ๊ฐ๋ฐํ๊ณ , ๊ธฐ์ค ๋ชจ๋ธ ๋๋น ์์๋ธ ์์ธก ๋ชจ๋ธ์ ๊ฐ์ ์จ์ ๊ฒ์ฆํ๋ค.** ๊ณผ์
์ ์์ํ์ฌ ์คํ์ ๋ฐฉ์งํ๊ณ ์์ ๋์๋ ฅ์ ๊ทน๋ํํด์ผ ํฉ๋๋ค.\n",
"\n",
"์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ณธ ๋ฏธ์
์์๋ **์ฆ๋ถ ๋ฐฐ์ ์ผ๋ก ๊ตฌ๊ฐ ํ์ยท์ ๋น ์๋๋ฆฌ์ค๋ณ ์ฐํ ๊ฒฝ๋ก๋ฅผ ๊ตฌํ๊ณ ๋๋น ์ถ๊ฐ ์์ ์๊ฐ, ์ถ๊ฐ ๊ฑฐ๋ฆฌ, ์ถ๊ฐ ์ ๋ฅ์ ์ผ ์ถ๊ฐ ์ ๋ฅ๋ ์ฐ์ถํ๊ณ ๋ชจ๋ธ์ ์ ์ฉํ๋** ์ค์ต์ ์ฒด๊ณ์ ์ผ๋ก ์ํํฉ๋๋ค.\n",
"\n",
"ํ์ต์๋ ์ค์ต๊ฐ์ด๋์ ์ ์๋ ์ด 4๊ฐ ์ธ๋ถ ํ๋(๊ธฐ๋ณธ ๊ธฐ๋ฅ ๊ตฌํ โ ๊ฒฐ๊ณผ ์งํ ๊ฒ์ฆ โ ํ๋ผ๋ฏธํฐ ํ๋ ๋ฐ ๋์ ๊ณผ์ )์ ์์ฐจ์ ์ผ๋ก ์งํํ๋ฉฐ ํต์ฌ ํ์ ๊ธฐ์ค์ ๋ฌ์ฑํฉ๋๋ค.\n",
"\n",
"**์ด๋ฒ ๊ฒฐ๊ณผ๋ฌผ:** `src/models/mov01_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": [
"\"\"\"์ฐํ ์์ก ์ ์ถ๊ฐ ์์ ์๊ฐ๊ณผ ์ ๋ฅ ์๋ชจ๋์ ์ฐ์ถํ๋ ์์ง์ ๊ตฌํํ๊ณ , ๊ธฐ์ค ๋ชจ๋ธ(BPR) ๋๋น ์์๋ธ ์ง์ฒด ์์ธก ๋ชจ๋ธ์ ์ค์ฐจ ๊ฐ์ ์จ๊ณผ R2๋ฅผ Hold-out ๋ฐ์ดํฐ๋ก ๊ฒ์ฆํ๋ค.\"\"\"\n",
"import numpy as np\n",
"import pandas as pd\n",
"from scipy.optimize import linprog\n",
"from sklearn.ensemble import GradientBoostingRegressor, RandomForestRegressor\n",
"from sklearn.metrics import r2_score\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# [๊ต์ก์ฉ๊ฐ์์๋ฃ] ๊ฐ์ ๋
ธ์ ๋งยท๊ตฌ๊ฐ ํต๊ณผ๋ ์์ฑ๊ธฐ (์ค์ ๋๋กยท์ง๋ช
ยท๋ถ๋ ์์)\n",
"# ๊ฐ์ ์ถ๋ฐ์ง(SRC)์์ ๊ฐ์ ๋ชฉ์ ์ง(DST)๊น์ง 3๊ฐ ์ถ์ (AยทBยทC)๊ณผ ์ฐ๊ฒฐ๋ก 2๊ฐ, ๊ตฌ๊ฐ 10๊ฐ. 150์ผ์น ํผํฌ์๊ฐ ํต๊ณผ๋ยท์ง์ฒด ์๊ฐ์ ๋ง๋ ๋ค.\n",
"# ---------------------------------------------------------------------------\n",
"SEGS = pd.DataFrame([\n",
" (\"A1\", \"SRC\", \"J1\", 12, 130, \"์ผ๋ฐ\"), (\"A2\", \"J1\", \"J2\", 8, 70, \"๊ต๋\"), (\"A3\", \"J2\", \"DST\", 10, 140, \"๊ต์ฐจ๋ก\"),\n",
" (\"B1\", \"SRC\", \"J3\", 15, 100, \"์ผ๋ฐ\"), (\"B2\", \"J3\", \"J2\", 9, 80, \"ํฐ๋\"), (\"B3\", \"J3\", \"DST\", 14, 95, \"์ผ๋ฐ\"),\n",
" (\"C1\", \"SRC\", \"J4\", 18, 70, \"์ผ๋ฐ\"), (\"C2\", \"J4\", \"DST\", 20, 75, \"๊ต๋\"),\n",
" (\"X1\", \"J1\", \"J3\", 7, 50, \"๊ต์ฐจ๋ก\"), (\"X2\", \"J4\", \"J3\", 10, 60, \"์ผ๋ฐ\")],\n",
" columns=[\"seg\", \"u\", \"v\", \"km\", \"cap\", \"kind\"]) # cap = ํผํฌ์๊ฐ ํต๊ณผ ์ฉ๋(๋/์, ๊ต์ก์ฉ ๊ฐ์ ๊ฐ)\n",
"KSPD = {\"์ผ๋ฐ\": 55, \"๊ต๋\": 40, \"ํฐ๋\": 45, \"๊ต์ฐจ๋ก\": 45} # ๊ตฌ๊ฐ ์ ํ๋ณ ์์ ์ฃผํ ์๋(km/h, ๊ฐ์ ๊ฐ)\n",
"SEGS[\"t0\"] = SEGS[\"km\"] / SEGS[\"kind\"].map(KSPD) * 60 # ์์ ์ฃผํ ์๊ฐ(๋ถ)\n",
"UTIL = dict(A1=.78, A2=.85, A3=.66, B1=.70, B2=.72, B3=.64, C1=.55, C2=.58, X1=.45, X2=.40) # ํ์ ์ด์ฉ๋ฅ \n",
"\n",
"\n",
"def bpr(t0, v, cap, a=0.15, b=4.0):\n",
" \"\"\"BPR ์ง์ฒด ํจ์(๊ตํต๊ณตํ์ ํ์คํ): ํต๊ณผ๋/์ฉ๋ ๋น๊ฐ ์ปค์ง์๋ก ์ง์ฒด๊ฐ ๊ธ์ฆํ๋ค. (๋ถ)\"\"\"\n",
" return t0 * a * (v / cap) ** b\n",
"\n",
"\n",
"def make_data(seed=11, days=150):\n",
" \"\"\"๊ตฌ๊ฐ x ์ผ์ ํจ๋. v=์ค์ ํผํฌ ํต๊ณผ๋, plan_v=์์ก๊ณํ ์๋(๊ธด๊ธ ๋ถ์ถ ๋ฏธ๋ฐ์), delay=์ค์ ์ง์ฒด(๋ถ).\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" d_idx = np.arange(days)\n",
" lvl = np.array([1.0, 1.08, 1.04, 1.10, 1.02, 0.82, 0.78])[d_idx % 7] * (1 + 0.0012 * d_idx)\n",
" lvl = lvl * np.where((d_idx >= 100) & (d_idx < 110), 1.30, 1.0) # ์์ก๋ ํญ์ฆ ๊ธฐ๊ฐ(๊ฐ์)\n",
" prev, rows = {s: 0.0 for s in SEGS[\"seg\"]}, []\n",
" for d in d_idx:\n",
" rain = bool(rng.random() < 0.18)\n",
" for r in SEGS.itertuples():\n",
" maint, urgent = bool(rng.random() < 0.06), bool(rng.random() < 0.05) # ๋น๊ณํ ์ ๋น / ๊ธด๊ธ ๋ถ์ถ\n",
" v = r.cap * UTIL[r.seg] * lvl[d] * (1.25 if urgent else 1.0) * rng.normal(1, 0.05)\n",
" cap_eff = r.cap * (0.65 if maint else 1.0) * (0.92 if rain else 1.0)\n",
" delay = max(r.t0 * 0.2 * (v / cap_eff) ** 3.5 + 0.25 * prev[r.seg] + rng.normal(0, 0.4), 0.0)\n",
" prev[r.seg] = delay\n",
" rows.append(dict(day=int(d), dow=int(d % 7), seg=r.seg, kind=r.kind, km=r.km, cap=r.cap, t0=r.t0,\n",
" plan_v=v / (1.25 if urgent else 1.0) * rng.normal(1, 0.04), v=v, maint=int(maint), urgent=int(urgent),\n",
" rain=int(rain), cap_eff=cap_eff, delay=delay))\n",
" df = pd.DataFrame(rows)\n",
" df[\"transit\"] = df[\"t0\"] * (1 + 0.08 * df[\"rain\"]) + df[\"delay\"]\n",
" return df\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",
"# ์ง์ฒด ์์ธก์ฉ ํ์ ํผ์ฒ ์์ฑ(๋น์ผ ์ค์ ์์ ์ ์ ์ ์๋ ์ ๋ณด๋ง ์ฌ์ฉ: ๊ณํ ๋ฌผ๋, ๊ณต์ง ์ ๋น, ๊ธฐ์ ์๋ณด, ์ ์ผยท์ ์ฃผ ์ค์ )\n",
"# ---------------------------------------------------------------------------\n",
"FEATS = [\"plan_v\", \"cap\", \"cap_eff\", \"t0\", \"km\", \"maint\", \"rain\", \"util\", \"util_eff\", \"util_eff4\", \"bpr_base\", \"lag1_delay\", \"lag7_delay\",\n",
" \"roll7_v\", \"surge_ratio\", \"is_weekend\", \"dow_sin\", \"dow_cos\", \"k_๊ต๋\", \"k_ํฐ๋\", \"k_๊ต์ฐจ๋ก\"]\n",
"\n",
"\n",
"def add_features(df):\n",
" d = df.sort_values([\"seg\", \"day\"]).copy()\n",
" g = d.groupby(\"seg\")\n",
" d[\"lag1_delay\"] = g[\"delay\"].shift(1)\n",
" d[\"lag7_delay\"] = g[\"delay\"].shift(7)\n",
" d[\"roll7_v\"] = g[\"v\"].transform(lambda s: s.shift(1).rolling(7).mean())\n",
" d[\"util\"] = d[\"plan_v\"] / d[\"cap\"]\n",
" d[\"util_eff\"] = d[\"plan_v\"] / d[\"cap_eff\"]\n",
" d[\"util_eff4\"] = d[\"util_eff\"] ** 4\n",
" d[\"bpr_base\"] = bpr(d[\"t0\"], d[\"plan_v\"], d[\"cap\"])\n",
" d[\"surge_ratio\"] = d[\"plan_v\"] / d[\"roll7_v\"]\n",
" d[\"is_weekend\"] = (d[\"dow\"] >= 5).astype(int)\n",
" d[\"dow_sin\"], d[\"dow_cos\"] = np.sin(2 * np.pi * d[\"dow\"] / 7), np.cos(2 * np.pi * d[\"dow\"] / 7)\n",
" for k in (\"๊ต๋\", \"ํฐ๋\", \"๊ต์ฐจ๋ก\"):\n",
" d[\"k_\" + k] = (d[\"kind\"] == k).astype(int)\n",
" return d.dropna().sort_values([\"day\", \"seg\"]).reset_index(drop=True)\n",
"# ---------------------------------------------------------------------------\n",
"# ---------------------------------------------------------------------------\n",
"# ๋
ธ์ ๋ง ํ๋ฆ ๋๊ตฌ: ์ต๋์ ๋(Edmonds-Karp), ์ฆ๋ถ ๋ฐฐ์ (ํผ์ก ๋ฐ์ ์ต๋จ์๊ฐ ๊ฒฝ๋ก), ๊ฒฝ๋ก ์ด๊ฑฐ\n",
"# ---------------------------------------------------------------------------\n",
"CAP = dict(zip(SEGS[\"seg\"], SEGS[\"cap\"]))\n",
"T0 = dict(zip(SEGS[\"seg\"], SEGS[\"t0\"]))\n",
"KM = dict(zip(SEGS[\"seg\"], SEGS[\"km\"]))\n",
"ARCS = {r.seg: (r.u, r.v) for r in SEGS.itertuples()}\n",
"SRC, DST = \"SRC\", \"DST\"\n",
"\n",
"\n",
"def seg_time(s, flow, cap=None, a=0.2, b=3.5):\n",
" \"\"\"๊ตฌ๊ฐ ํต๊ณผ์๊ฐ(๋ถ) = ์์ ์ฃผํ + ํผ์ก ์ง์ฒด. cap์ ์ฃผ๋ฉด ์ฉ๋์ ๋์ฒด(์ ๋นยท๊ธฐ์ ๋ฐ์).\"\"\"\n",
" r = flow / (cap or CAP[s])\n",
" return T0[s] * (1 + a * r ** b + 6.0 * max(0.0, r - 1.0)) # ์ฉ๋ ์ด๊ณผ๋ถ์ ๋๊ธฐ์ด๋ก ๊ธ์ฆ\n",
"\n",
"\n",
"def max_flow(closed=(), capmul=None):\n",
" \"\"\"์ต๋์ ๋(๋/์)๊ณผ ์ต์ ์ ๋จ ๊ตฌ๊ฐ(๋ณ๋ชฉ). capmul = {๊ตฌ๊ฐ: ์ฉ๋ ๋ฐฐ์จ}.\"\"\"\n",
" cap = {s: (0 if s in closed else CAP[s] * (capmul or {}).get(s, 1.0)) for s in CAP}\n",
" res = {}\n",
" for s, (u, v) in ARCS.items():\n",
" res[(u, v)] = res.get((u, v), 0) + cap[s]\n",
" res.setdefault((v, u), 0)\n",
" flow = 0.0\n",
" while True:\n",
" prev, q = {SRC: None}, [SRC]\n",
" for u in q:\n",
" for (a, b), c in res.items():\n",
" if a == u and c > 1e-9 and b not in prev:\n",
" prev[b] = u; q.append(b)\n",
" if DST not in prev:\n",
" break\n",
" path, x = [], DST\n",
" while prev[x]:\n",
" path.append((prev[x], x)); x = prev[x]\n",
" f = min(res[e] for e in path)\n",
" for a, b in path:\n",
" res[(a, b)] -= f; res[(b, a)] += f\n",
" flow += f\n",
" reach = set(prev)\n",
" cut = [s for s, (u, v) in ARCS.items() if u in reach and v not in reach and cap[s] > 0]\n",
" return flow, cut\n",
"\n",
"\n",
"def all_paths():\n",
" \"\"\"SRC -> DST ๋จ์ ๊ฒฝ๋ก(๊ตฌ๊ฐ ์ด๋ฆ ํํ) ์ ์ฒด.\"\"\"\n",
" out = []\n",
"\n",
" def dfs(node, segs, seen):\n",
" if node == DST:\n",
" out.append(tuple(segs)); return\n",
" for s, (u, v) in ARCS.items():\n",
" if u == node and v not in seen:\n",
" dfs(v, segs + [s], seen | {v})\n",
" dfs(SRC, [], {SRC})\n",
" return out\n",
"\n",
"\n",
"PATHS = all_paths()\n",
"\n",
"\n",
"def assign(demand, closed=(), capmul=None, steps=40):\n",
" \"\"\"์ฆ๋ถ ๋ฐฐ์ : ์์๋ฅผ steps์กฐ๊ฐ์ผ๋ก ๋๋ , ์ ์ฒด ํต๊ณผ์๊ฐ(๋์ x ์๊ฐ) ์ฆ๊ฐ๊ฐ ๊ฐ์ฅ ์์ ๊ฒฝ๋ก์ ์ฐจ๋ก๋ก ์น๋๋ค(์์คํ
์ต์ ๊ทผ์ฌ).\"\"\"\n",
" cap = {s: CAP[s] * (capmul or {}).get(s, 1.0) for s in CAP}\n",
" flow, pf = {s: 0.0 for s in CAP}, {}\n",
" ok = [p for p in PATHS if not any(s in closed for s in p)]\n",
" if not ok:\n",
" return None\n",
" for _ in range(steps):\n",
" chunk = demand / steps\n",
" best = min(ok, key=lambda p: sum((flow[s] + chunk) * seg_time(s, flow[s] + chunk, cap[s]) - flow[s] * seg_time(s, flow[s], cap[s]) for s in p))\n",
" for s in best:\n",
" flow[s] += chunk\n",
" pf[best] = pf.get(best, 0) + chunk\n",
" tt = {p: sum(seg_time(s, flow[s], cap[s]) for s in p) for p in pf}\n",
" avg = sum(pf[p] * tt[p] for p in pf) / demand\n",
" return {\"flow\": flow, \"paths\": pf, \"path_min\": tt, \"avg_min\": avg, \"util\": {s: flow[s] / cap[s] for s in cap if cap[s] > 0}}\n",
"\n",
"\n",
"FIXED_SHARE = {(\"A1\", \"A2\", \"A3\"): 0.32, (\"B1\", \"B3\"): 0.40, (\"C1\", \"C2\"): 0.28} # ๊ณผ๊ฑฐ ๊ดํ: ์ถ์ ๋ณ ๊ณ ์ ๋ฐฐ๋ถ(๊ฐ์)\n",
"\n",
"\n",
"def fixed_assign(demand, closed=(), capmul=None):\n",
" \"\"\"๊ณผ๊ฑฐ ๊ดํ ๋ฐฐ๋ถ: ์ถ์ ๋ณ ๊ณ ์ ๋น์จ. ๋งํ ์ถ์ ๋ฌผ๋์ ์ด๋ ค ์๋ ์ถ์ ์ ๋น์จ๋๋ก ์ฌ๋ถ๋ฐฐ.\"\"\"\n",
" cap = {s: CAP[s] * (capmul or {}).get(s, 1.0) for s in CAP}\n",
" live = {p: w for p, w in FIXED_SHARE.items() if not any(s in closed for s in p)}\n",
" if not live:\n",
" return None\n",
" tot = sum(live.values())\n",
" flow = {s: 0.0 for s in CAP}\n",
" pf = {p: demand * w / tot for p, w in live.items()}\n",
" for p, f in pf.items():\n",
" for s in p:\n",
" flow[s] += f\n",
" tt = {p: sum(seg_time(s, flow[s], cap[s]) for s in p) for p in pf}\n",
" return {\"flow\": flow, \"paths\": pf, \"path_min\": tt, \"avg_min\": sum(pf[p] * tt[p] for p in pf) / demand,\n",
" \"util\": {s: flow[s] / cap[s] for s in cap if cap[s] > 0}}\n",
"\n",
"\n",
"def lp_assign(demand, closed=(), capmul=None):\n",
" \"\"\"์ ํ๊ณํ(LP): ๊ตฌ๊ฐ ์ฉ๋ ์ํ(์ด์ฉ๋ฅ rho) ์๋์์ ์์ ์ฃผํ์๊ฐ ํฉ์ ์ต์ํํ๋ ๊ฒฝ๋ก๋ณ ๋ฌผ๋. ๋ถ๊ฐ๋ฅํ๋ฉด rho๋ฅผ 1.0๊น์ง ์ํ.\"\"\"\n",
" cap = {s: CAP[s] * (capmul or {}).get(s, 1.0) for s in CAP}\n",
" ok = [p for p in PATHS if not any(s in closed for s in p)]\n",
" if not ok:\n",
" return None\n",
" for rho in (0.85, 0.95, 1.0):\n",
" r = linprog([sum(T0[s] for s in p) for p in ok], A_ub=[[1.0 if s in p else 0.0 for p in ok] for s in CAP],\n",
" b_ub=[rho * cap[s] for s in CAP], A_eq=[[1.0] * len(ok)], b_eq=[demand], bounds=(0, None), method=\"highs\")\n",
" if r.status == 0:\n",
" break\n",
" else:\n",
" return assign(demand, closed, capmul) # ์ฉ๋ ์ด๊ณผ ์์: ์ฆ๋ถ ๋ฐฐ์ ์ผ๋ก ๋์ฒด(๊ณผ๋ถํ ํ์)\n",
" flow = {s: sum(r.x[i] for i, p in enumerate(ok) if s in p) for s in CAP}\n",
" pf = {p: float(r.x[i]) for i, p in enumerate(ok) if r.x[i] > 1e-6}\n",
" tt = {p: sum(seg_time(s, flow[s], cap[s]) for s in p) for p in pf}\n",
" return {\"flow\": flow, \"paths\": pf, \"path_min\": tt, \"avg_min\": sum(pf[p] * tt[p] for p in pf) / demand,\n",
" \"util\": {s: flow[s] / cap[s] for s in cap if cap[s] > 0}, \"rho\": rho}\n",
"\n",
"\n",
"def cost_of(res, tv=1000, fuel_km=450):\n",
" \"\"\"์๊ฐ๋น ๋น์ฉ(์, ๊ต์ก์ฉ ๊ฐ์ ): ๋๋น ์๊ฐ๋น์ฉ tv์/๋ถ + ์ ๋ฅ๋น fuel_km์/km.\"\"\"\n",
" return sum(f * (res[\"path_min\"][p] * tv + sum(KM[s] for s in p) * fuel_km) for p, f in res[\"paths\"].items())\n",
"# ---------------------------------------------------------------------------\n",
"\n",
"# ---------------- 1) ์ฐํ ์์ง: ์ถ๊ฐ ์์ ์๊ฐยท์ ๋ฅ (๊ฐ์ ๊ฐ: ๋๋น ์ฃผํ L/km, ์ ์ฒด ๊ณตํ์ L/๋ถ) ----------------\n",
"L_KM, L_IDLE = 0.30, 0.08 # ๊ต์ก์ฉ ๊ฐ์ ์ ๋ฅ ๊ณ์(์ค์ ์ฅ๋น ์ ์ ์๋)\n",
"\n",
"\n",
"def trip(res, fleet_vehicles):\n",
" \"\"\"๊ฒฝ๋ก๋ณ ๋ฌผ๋ ๋น์จ๋ก ํ๊ท ํต๊ณผ์๊ฐยท๊ฑฐ๋ฆฌยท์ ๋ฅ(๋๋น L)๋ฅผ ๊ตฌํ๊ณ ์ด๋์ผ๋ก ํ์ฐํ๋ค.\"\"\"\n",
" tot = sum(res[\"paths\"].values())\n",
" km = sum(f * sum(KM[s] for s in p) for p, f in res[\"paths\"].items()) / tot\n",
" mins = res[\"avg_min\"]\n",
" free = sum(f * sum(T0[s] for s in p) for p, f in res[\"paths\"].items()) / tot\n",
" fuel = km * L_KM + max(mins - free, 0) * L_IDLE\n",
" return {\"min\": mins, \"km\": km, \"fuel_l\": fuel, \"fleet_fuel_l\": fuel * fleet_vehicles}\n",
"\n",
"\n",
"DEMAND, FLEET = 200, 600 # ํผํฌ 200๋/์, ์ผ ์์ก 600๋(๊ฐ์ )\n",
"norm = trip(assign(DEMAND), FLEET)\n",
"rows = []\n",
"for name, closed, cm in [(\"A2 ๊ต๋ ํ์\", (\"A2\",), None), (\"B2 ํฐ๋ ํ์\", (\"B2\",), None), (\"A1 ์ฉ๋ 50% ๊ฐ์(์ ๋น)\", (), {\"A1\": 0.5}), (\"A2 ํ์ + B1 ์ ๋น(65%)\", (\"A2\",), {\"B1\": 0.65})]:\n",
" r = assign(DEMAND, closed, cm)\n",
" t = trip(r, FLEET)\n",
" rows.append((name, t[\"min\"] - norm[\"min\"], t[\"km\"] - norm[\"km\"], t[\"fuel_l\"] - norm[\"fuel_l\"], t[\"fleet_fuel_l\"] - norm[\"fleet_fuel_l\"], max(r[\"util\"].values())))\n",
"det = pd.DataFrame(rows, columns=[\"์๋๋ฆฌ์ค\", \"์ถ๊ฐ ์์(๋ถ/๋)\", \"์ถ๊ฐ ๊ฑฐ๋ฆฌ(km/๋)\", \"์ถ๊ฐ ์ ๋ฅ(L/๋)\", \"์ผ ์ถ๊ฐ ์ ๋ฅ(L)\", \"์ต๋ ์ด์ฉ๋ฅ \"]).round(2)\n",
"print(f\"[1] ์ฐํ ์์ง ๊ฒฐ๊ณผ(ํ์ ๊ธฐ์ค {norm['min']:.1f}๋ถ, {norm['km']:.1f}km, {norm['fuel_l']:.2f}L/๋)\")\n",
"print(det.to_string(index=False))\n",
"print(\" ์ ๋ฅ ๊ณ์๋ ๊ต์ก์ฉ ๊ฐ์ ๊ฐ์ด๋ค. ์ด์ฉ๋ฅ 1.0 ์ด๊ณผ ์๋๋ฆฌ์ค๋ ์ฐํ๋ง์ผ๋ก ์์ฉ์ด ์ด๋ ค์ ์ฌ๋์ ๋ฌผ๋ ์กฐ์ ํ๋จ์ด ํ์ํ๋ค.\")\n",
"\n",
"# ---------------- 2) ๊ณ ๊ธ ๋ชจ๋ธ: ๊ธฐ์ค ๋ชจ๋ธ(BPR) vs ์์๋ธ(GBR+RF), Hold-out ----------------\n",
"df = make_data(); d = add_features(df)\n",
"tr, te = d[d.day < 120], d[d.day >= 120]\n",
"gbr = GradientBoostingRegressor(n_estimators=250, max_depth=3, learning_rate=0.06, subsample=0.8, random_state=0).fit(tr[FEATS], tr[\"delay\"])\n",
"rf = RandomForestRegressor(n_estimators=150, min_samples_leaf=3, random_state=0, n_jobs=1).fit(tr[FEATS], tr[\"delay\"])\n",
"fac = te[\"t0\"] * (1 + 0.08 * te[\"rain\"])\n",
"P = {\"๊ธฐ์ค(BPR)\": te[\"t0\"] + te[\"bpr_base\"], \"GBR\": fac + gbr.predict(te[FEATS]), \"RandomForest\": fac + rf.predict(te[FEATS])}\n",
"P[\"์์๋ธ(ํ๊ท )\"] = (P[\"GBR\"] + P[\"RandomForest\"]) / 2\n",
"tab = pd.DataFrame({k: {\"MAE(๋ถ)\": mae(te.transit, v), \"RMSE(๋ถ)\": float(np.sqrt(np.mean((te.transit - v) ** 2))), \"MAPE(%)\": mape(te.transit, v), \"R2\": r2_score(te.transit, v)} for k, v in P.items()}).T.round(3)\n",
"print(f\"\\n[2] Hold-out(day 120~149, {len(te)}ํ) ํต๊ณผ์๊ฐ ์์ธก ์ฑ๋ฅ\")\n",
"print(tab.to_string())\n",
"b, e = tab.loc[\"๊ธฐ์ค(BPR)\"], tab.loc[\"์์๋ธ(ํ๊ท )\"]\n",
"print(f\" MAE ๊ฐ์ {1 - e['MAE(๋ถ)'] / b['MAE(๋ถ)']:.1%}, RMSE ๊ฐ์ {1 - e['RMSE(๋ถ)'] / b['RMSE(๋ถ)']:.1%} (๋ชฉํ 20% ์ด์: {'๋ฌ์ฑ' if 1 - e['MAE(๋ถ)'] / b['MAE(๋ถ)'] >= .2 and 1 - e['RMSE(๋ถ)'] / b['RMSE(๋ถ)'] >= .2 else '๋ฏธ๋ฌ'}), R2 {e['R2']:.3f} (๋ชฉํ 0.85 ์ด์: {'๋ฌ์ฑ' if e['R2'] >= .85 else '๋ฏธ๋ฌ'})\")\n",
"dl = te[\"delay\"]; pdl = P[\"์์๋ธ(ํ๊ท )\"] - fac\n",
"print(f\" ์ฐธ๊ณ : ์ง์ฒด ์๊ฐ๋ง ๋๊ณ ๋ณด๋ฉด R2 {r2_score(dl, pdl):.3f}, MAE {mae(dl, pdl):.2f}๋ถ (๊ธฐ์ค BPR MAE {mae(dl, te['bpr_base']):.2f}๋ถ). ํต๊ณผ์๊ฐ R2๋ ๊ตฌ๊ฐ ๊ฐ ๊ธธ์ด ์ฐจ์ด๋ฅผ ํฌํจํ๋ค.\")\n",
"\n",
"# ---------------- 3) ๋ถ๊ท์น ์ํฉ(๋น์ ํ ์์) ์ ์๋ ฅ: ์ ๋น์ผยทํญ์ฆ ๊ธฐ๊ฐ ์ค์ฐจ ----------------\n",
"stress = {\"๋น๊ณํ ์ ๋น์ผ\": te[\"maint\"] == 1, \"๊ธด๊ธ ๋ถ์ถ์ผ\": te[\"urgent\"] == 1, \"์ฐ์ฒ์ผ\": te[\"rain\"] == 1, \"๊ทธ ์ธ\": (te[\"maint\"] == 0) & (te[\"urgent\"] == 0) & (te[\"rain\"] == 0)}\n",
"rr = [(k, int(m.sum()), mae(te.transit[m], P[\"๊ธฐ์ค(BPR)\"][m]), mae(te.transit[m], P[\"์์๋ธ(ํ๊ท )\"][m])) for k, m in stress.items() if m.sum() > 0]\n",
"print(\"\\n[3] ์กฐ๊ฑด๋ณ MAE(๋ถ): ๊ธฐ์ค vs ์์๋ธ\")\n",
"print(pd.DataFrame(rr, columns=[\"์กฐ๊ฑด\", \"๊ฑด์\", \"๊ธฐ์ค\", \"์์๋ธ\"]).round(2).to_string(index=False))\n",
"print(\"ํ์ดํผํ๋ผ๋ฏธํฐ(ํธ๋ฆฌ 250๊ฐ, ๊น์ด 3, ํ์ต๋ฅ 0.06)๋ ์๊ณ์ด ๊ต์ฐจ๊ฒ์ฆ(M4) ๊ฒฐ๊ณผ๋ฅผ ๋ฐํ์ผ๋ก ์ ํ๊ณ , ํ์ต๋ ๋ชจ๋ธ์ ์ด ์คํ๊ธฐ์์๋ ์ ์ฅํ์ง ์๊ณ ์ค์ ๋ง ์ถ๋ ฅํ๋ค.\")\n",
"print(\"์ค์ :\", {\"gbr\": gbr.get_params()[\"n_estimators\"], \"rf\": rf.get_params()[\"n_estimators\"], \"features\": len(FEATS)})\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"๐ก [์ฐธ๊ณ ] ๋ฏธ์
05 ์์ ์คํ ๊ฒฐ๊ณผ (Output)
\n",
"\n",
"```text\n",
"[1] ์ฐํ ์์ง ๊ฒฐ๊ณผ(ํ์ ๊ธฐ์ค 40.9๋ถ, 31.2km, 9.63L/๋)\n",
" ์๋๋ฆฌ์ค ์ถ๊ฐ ์์(๋ถ/๋) ์ถ๊ฐ ๊ฑฐ๋ฆฌ(km/๋) ์ถ๊ฐ ์ ๋ฅ(L/๋) ์ผ ์ถ๊ฐ ์ ๋ฅ(L) ์ต๋ ์ด์ฉ๋ฅ \n",
" A2 ๊ต๋ ํ์ 3.96 2.38 0.81 485.68 1.0\n",
" B2 ํฐ๋ ํ์ 0.05 -0.12 -0.02 -10.48 1.0\n",
" A1 ์ฉ๋ 50% ๊ฐ์(์ ๋น) 0.98 0.10 0.10 60.12 1.0\n",
"A2 ํ์ + B1 ์ ๋น(65%) 12.90 3.98 1.85 1113.00 1.3\n",
" ์ ๋ฅ ๊ณ์๋ ๊ต์ก์ฉ ๊ฐ์ ๊ฐ์ด๋ค. ์ด์ฉ๋ฅ 1.0 ์ด๊ณผ ์๋๋ฆฌ์ค๋ ์ฐํ๋ง์ผ๋ก ์์ฉ์ด ์ด๋ ค์ ์ฌ๋์ ๋ฌผ๋ ์กฐ์ ํ๋จ์ด ํ์ํ๋ค.\n",
"\n",
"[2] Hold-out(day 120~149, 300ํ) ํต๊ณผ์๊ฐ ์์ธก ์ฑ๋ฅ\n",
" MAE(๋ถ) RMSE(๋ถ) MAPE(%) R2\n",
"๊ธฐ์ค(BPR) 1.763 2.662 9.245 0.829\n",
"GBR 0.494 0.877 2.784 0.981\n",
"RandomForest 0.495 0.877 2.813 0.981\n",
"์์๋ธ(ํ๊ท ) 0.475 0.847 2.725 0.983\n",
" MAE ๊ฐ์ 73.1%, RMSE ๊ฐ์ 68.2% (๋ชฉํ 20% ์ด์: ๋ฌ์ฑ), R2 0.983 (๋ชฉํ 0.85 ์ด์: ๋ฌ์ฑ)\n",
" ์ฐธ๊ณ : ์ง์ฒด ์๊ฐ๋ง ๋๊ณ ๋ณด๋ฉด R2 0.846, MAE 0.47๋ถ (๊ธฐ์ค BPR MAE 1.52๋ถ). ํต๊ณผ์๊ฐ R2๋ ๊ตฌ๊ฐ ๊ฐ ๊ธธ์ด ์ฐจ์ด๋ฅผ ํฌํจํ๋ค.\n",
"\n",
"[3] ์กฐ๊ฑด๋ณ MAE(๋ถ): ๊ธฐ์ค vs ์์๋ธ\n",
" ์กฐ๊ฑด ๊ฑด์ ๊ธฐ์ค ์์๋ธ\n",
"๋น๊ณํ ์ ๋น์ผ 23 5.72 0.81\n",
" ๊ธด๊ธ ๋ถ์ถ์ผ 16 3.98 2.17\n",
" ์ฐ์ฒ์ผ 60 3.32 0.61\n",
" ๊ทธ ์ธ 211 1.06 0.38\n",
"ํ์ดํผํ๋ผ๋ฏธํฐ(ํธ๋ฆฌ 250๊ฐ, ๊น์ด 3, ํ์ต๋ฅ 0.06)๋ ์๊ณ์ด ๊ต์ฐจ๊ฒ์ฆ(M4) ๊ฒฐ๊ณผ๋ฅผ ๋ฐํ์ผ๋ก ์ ํ๊ณ , ํ์ต๋ ๋ชจ๋ธ์ ์ด ์คํ๊ธฐ์์๋ ์ ์ฅํ์ง ์๊ณ ์ค์ ๋ง ์ถ๋ ฅํ๋ค.\n",
"์ค์ : {'gbr': 250, 'rf': 150, 'features': 21}\n",
"```\n",
" \n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## ๐ฏ ๋ฏธ์
06. ์ ์ ๋ณด๊ธ๋ก ๋ณ๋ชฉ ํผ์ก๋ ๋ฐ ์ฐํ ๊ฒฝ๋ก ๋น๊ต ์๋ฎฌ๋ ์ด์
๋์๋ณด๋ ๊ตฌํ\n",
"**์์์๊ฐ**: ์ฝ 3.0์๊ฐ\n",
"\n",
"### ๐ ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ์ํ ์ง์นจ\n",
"#### ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ๋น์์ฑ\n",
"\n",
"๋ณด๊ธ๋ก ๋ณ๋ชฉ ํผ์ก๋ ๋ฐ ์ฐํ ๊ฒฝ๋ก ๋น๊ต ๋์๋ณด๋๋ฅผ ๊ตฌํํ๋ค. ์ค์ ์ ์ ํ์ฅ์์๋ ๋จ์ ํ์ง์ ๊ทธ์น์ง ์๊ณ ์ ์ํ ์ํ ํ๋จ๊ณผ ์ ๋ขฐ๋ ๋์ ์์ฌ๊ฒฐ์ ์ด ํ์์ ์ด๋ฏ๋ก, **๋ณด๊ธ๋ก ๋ณ๋ชฉ ํผ์ก๋์ ์ฐํ ๊ฒฝ๋ก ๋น๊ต ๋์๋ณด๋๋ฅผ ๊ตฌํํ๊ณ , ์์ธก ๊ฑด๋ง๋ค ๊ธฐ์ฌ๋ ์์ ์์ธ์ ์ค๋ช
ํ๋ค.** ๊ณผ์
์ ์์ํ์ฌ ์คํ์ ๋ฐฉ์งํ๊ณ ์์ ๋์๋ ฅ์ ๊ทน๋ํํด์ผ ํฉ๋๋ค.\n",
"\n",
"์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ณธ ๋ฏธ์
์์๋ **๊ตฌ๊ฐ๋ณ ์ด์ฉ๋ฅ ํํธ๋งต, ์ค๋์ ์์ธก ์ง์ฒด์ ๊ฒฝ๋ณด ๊ธฐ์ค, ๊ณ ์ ๋ฐฐ๋ถ ๋๋น ์ฐํ ๋ฐฐ์ ๋น๊ต, ์ต์
์ฌ๋ก ์์ธ ๊ธฐ์ฌ๋๋ฅผ 2x2 ํ๋ฉด์ผ๋ก ๊ทธ๋ฆฐ๋ค ๊ณผ์ ์ ์งํํ๋** ์ค์ต์ ์ฒด๊ณ์ ์ผ๋ก ์ํํฉ๋๋ค.\n",
"\n",
"ํ์ต์๋ ์ค์ต๊ฐ์ด๋์ ์ ์๋ ์ด 4๊ฐ ์ธ๋ถ ํ๋(๊ธฐ๋ณธ ๊ธฐ๋ฅ ๊ตฌํ โ ๊ฒฐ๊ณผ ์งํ ๊ฒ์ฆ โ ํ๋ผ๋ฏธํฐ ํ๋ ๋ฐ ๋์ ๊ณผ์ )์ ์์ฐจ์ ์ผ๋ก ์งํํ๋ฉฐ ํต์ฌ ํ์ ๊ธฐ์ค์ ๋ฌ์ฑํฉ๋๋ค.\n",
"\n",
"**์ด๋ฒ ๊ฒฐ๊ณผ๋ฌผ:** `src/explainability/mov01_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": [
"\"\"\"๋ณด๊ธ๋ก ๋ณ๋ชฉ ํผ์ก๋์ ์ฐํ ๊ฒฝ๋ก ๋น๊ต ๋์๋ณด๋๋ฅผ ๊ตฌํํ๊ณ , ์์ธก ๊ฑด๋ง๋ค ๊ธฐ์ฌ๋ ์์ 3๊ฐ ์์ธ์ ์๋ ์ฐ์ถ(์์ธ๊ตฐ ์ ๊ฑฐ ๋ฐฉ์ ์ค๋ช
)ํด ์์ฐ์ด ์์ฝ์ผ๋ก ๋ณด์ฌ ์ค๋ค.\"\"\"\n",
"import numpy as np\n",
"import pandas as pd\n",
"import matplotlib.pyplot as plt\n",
"from scipy.optimize import linprog\n",
"from sklearn.ensemble import GradientBoostingRegressor\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# [๊ต์ก์ฉ๊ฐ์์๋ฃ] ๊ฐ์ ๋
ธ์ ๋งยท๊ตฌ๊ฐ ํต๊ณผ๋ ์์ฑ๊ธฐ (์ค์ ๋๋กยท์ง๋ช
ยท๋ถ๋ ์์)\n",
"# ๊ฐ์ ์ถ๋ฐ์ง(SRC)์์ ๊ฐ์ ๋ชฉ์ ์ง(DST)๊น์ง 3๊ฐ ์ถ์ (AยทBยทC)๊ณผ ์ฐ๊ฒฐ๋ก 2๊ฐ, ๊ตฌ๊ฐ 10๊ฐ. 150์ผ์น ํผํฌ์๊ฐ ํต๊ณผ๋ยท์ง์ฒด ์๊ฐ์ ๋ง๋ ๋ค.\n",
"# ---------------------------------------------------------------------------\n",
"SEGS = pd.DataFrame([\n",
" (\"A1\", \"SRC\", \"J1\", 12, 130, \"์ผ๋ฐ\"), (\"A2\", \"J1\", \"J2\", 8, 70, \"๊ต๋\"), (\"A3\", \"J2\", \"DST\", 10, 140, \"๊ต์ฐจ๋ก\"),\n",
" (\"B1\", \"SRC\", \"J3\", 15, 100, \"์ผ๋ฐ\"), (\"B2\", \"J3\", \"J2\", 9, 80, \"ํฐ๋\"), (\"B3\", \"J3\", \"DST\", 14, 95, \"์ผ๋ฐ\"),\n",
" (\"C1\", \"SRC\", \"J4\", 18, 70, \"์ผ๋ฐ\"), (\"C2\", \"J4\", \"DST\", 20, 75, \"๊ต๋\"),\n",
" (\"X1\", \"J1\", \"J3\", 7, 50, \"๊ต์ฐจ๋ก\"), (\"X2\", \"J4\", \"J3\", 10, 60, \"์ผ๋ฐ\")],\n",
" columns=[\"seg\", \"u\", \"v\", \"km\", \"cap\", \"kind\"]) # cap = ํผํฌ์๊ฐ ํต๊ณผ ์ฉ๋(๋/์, ๊ต์ก์ฉ ๊ฐ์ ๊ฐ)\n",
"KSPD = {\"์ผ๋ฐ\": 55, \"๊ต๋\": 40, \"ํฐ๋\": 45, \"๊ต์ฐจ๋ก\": 45} # ๊ตฌ๊ฐ ์ ํ๋ณ ์์ ์ฃผํ ์๋(km/h, ๊ฐ์ ๊ฐ)\n",
"SEGS[\"t0\"] = SEGS[\"km\"] / SEGS[\"kind\"].map(KSPD) * 60 # ์์ ์ฃผํ ์๊ฐ(๋ถ)\n",
"UTIL = dict(A1=.78, A2=.85, A3=.66, B1=.70, B2=.72, B3=.64, C1=.55, C2=.58, X1=.45, X2=.40) # ํ์ ์ด์ฉ๋ฅ \n",
"\n",
"\n",
"def bpr(t0, v, cap, a=0.15, b=4.0):\n",
" \"\"\"BPR ์ง์ฒด ํจ์(๊ตํต๊ณตํ์ ํ์คํ): ํต๊ณผ๋/์ฉ๋ ๋น๊ฐ ์ปค์ง์๋ก ์ง์ฒด๊ฐ ๊ธ์ฆํ๋ค. (๋ถ)\"\"\"\n",
" return t0 * a * (v / cap) ** b\n",
"\n",
"\n",
"def make_data(seed=11, days=150):\n",
" \"\"\"๊ตฌ๊ฐ x ์ผ์ ํจ๋. v=์ค์ ํผํฌ ํต๊ณผ๋, plan_v=์์ก๊ณํ ์๋(๊ธด๊ธ ๋ถ์ถ ๋ฏธ๋ฐ์), delay=์ค์ ์ง์ฒด(๋ถ).\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" d_idx = np.arange(days)\n",
" lvl = np.array([1.0, 1.08, 1.04, 1.10, 1.02, 0.82, 0.78])[d_idx % 7] * (1 + 0.0012 * d_idx)\n",
" lvl = lvl * np.where((d_idx >= 100) & (d_idx < 110), 1.30, 1.0) # ์์ก๋ ํญ์ฆ ๊ธฐ๊ฐ(๊ฐ์)\n",
" prev, rows = {s: 0.0 for s in SEGS[\"seg\"]}, []\n",
" for d in d_idx:\n",
" rain = bool(rng.random() < 0.18)\n",
" for r in SEGS.itertuples():\n",
" maint, urgent = bool(rng.random() < 0.06), bool(rng.random() < 0.05) # ๋น๊ณํ ์ ๋น / ๊ธด๊ธ ๋ถ์ถ\n",
" v = r.cap * UTIL[r.seg] * lvl[d] * (1.25 if urgent else 1.0) * rng.normal(1, 0.05)\n",
" cap_eff = r.cap * (0.65 if maint else 1.0) * (0.92 if rain else 1.0)\n",
" delay = max(r.t0 * 0.2 * (v / cap_eff) ** 3.5 + 0.25 * prev[r.seg] + rng.normal(0, 0.4), 0.0)\n",
" prev[r.seg] = delay\n",
" rows.append(dict(day=int(d), dow=int(d % 7), seg=r.seg, kind=r.kind, km=r.km, cap=r.cap, t0=r.t0,\n",
" plan_v=v / (1.25 if urgent else 1.0) * rng.normal(1, 0.04), v=v, maint=int(maint), urgent=int(urgent),\n",
" rain=int(rain), cap_eff=cap_eff, delay=delay))\n",
" df = pd.DataFrame(rows)\n",
" df[\"transit\"] = df[\"t0\"] * (1 + 0.08 * df[\"rain\"]) + df[\"delay\"]\n",
" return df\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",
"# ์ง์ฒด ์์ธก์ฉ ํ์ ํผ์ฒ ์์ฑ(๋น์ผ ์ค์ ์์ ์ ์ ์ ์๋ ์ ๋ณด๋ง ์ฌ์ฉ: ๊ณํ ๋ฌผ๋, ๊ณต์ง ์ ๋น, ๊ธฐ์ ์๋ณด, ์ ์ผยท์ ์ฃผ ์ค์ )\n",
"# ---------------------------------------------------------------------------\n",
"FEATS = [\"plan_v\", \"cap\", \"cap_eff\", \"t0\", \"km\", \"maint\", \"rain\", \"util\", \"util_eff\", \"util_eff4\", \"bpr_base\", \"lag1_delay\", \"lag7_delay\",\n",
" \"roll7_v\", \"surge_ratio\", \"is_weekend\", \"dow_sin\", \"dow_cos\", \"k_๊ต๋\", \"k_ํฐ๋\", \"k_๊ต์ฐจ๋ก\"]\n",
"\n",
"\n",
"def add_features(df):\n",
" d = df.sort_values([\"seg\", \"day\"]).copy()\n",
" g = d.groupby(\"seg\")\n",
" d[\"lag1_delay\"] = g[\"delay\"].shift(1)\n",
" d[\"lag7_delay\"] = g[\"delay\"].shift(7)\n",
" d[\"roll7_v\"] = g[\"v\"].transform(lambda s: s.shift(1).rolling(7).mean())\n",
" d[\"util\"] = d[\"plan_v\"] / d[\"cap\"]\n",
" d[\"util_eff\"] = d[\"plan_v\"] / d[\"cap_eff\"]\n",
" d[\"util_eff4\"] = d[\"util_eff\"] ** 4\n",
" d[\"bpr_base\"] = bpr(d[\"t0\"], d[\"plan_v\"], d[\"cap\"])\n",
" d[\"surge_ratio\"] = d[\"plan_v\"] / d[\"roll7_v\"]\n",
" d[\"is_weekend\"] = (d[\"dow\"] >= 5).astype(int)\n",
" d[\"dow_sin\"], d[\"dow_cos\"] = np.sin(2 * np.pi * d[\"dow\"] / 7), np.cos(2 * np.pi * d[\"dow\"] / 7)\n",
" for k in (\"๊ต๋\", \"ํฐ๋\", \"๊ต์ฐจ๋ก\"):\n",
" d[\"k_\" + k] = (d[\"kind\"] == k).astype(int)\n",
" return d.dropna().sort_values([\"day\", \"seg\"]).reset_index(drop=True)\n",
"# ---------------------------------------------------------------------------\n",
"# ---------------------------------------------------------------------------\n",
"# ๋
ธ์ ๋ง ํ๋ฆ ๋๊ตฌ: ์ต๋์ ๋(Edmonds-Karp), ์ฆ๋ถ ๋ฐฐ์ (ํผ์ก ๋ฐ์ ์ต๋จ์๊ฐ ๊ฒฝ๋ก), ๊ฒฝ๋ก ์ด๊ฑฐ\n",
"# ---------------------------------------------------------------------------\n",
"CAP = dict(zip(SEGS[\"seg\"], SEGS[\"cap\"]))\n",
"T0 = dict(zip(SEGS[\"seg\"], SEGS[\"t0\"]))\n",
"KM = dict(zip(SEGS[\"seg\"], SEGS[\"km\"]))\n",
"ARCS = {r.seg: (r.u, r.v) for r in SEGS.itertuples()}\n",
"SRC, DST = \"SRC\", \"DST\"\n",
"\n",
"\n",
"def seg_time(s, flow, cap=None, a=0.2, b=3.5):\n",
" \"\"\"๊ตฌ๊ฐ ํต๊ณผ์๊ฐ(๋ถ) = ์์ ์ฃผํ + ํผ์ก ์ง์ฒด. cap์ ์ฃผ๋ฉด ์ฉ๋์ ๋์ฒด(์ ๋นยท๊ธฐ์ ๋ฐ์).\"\"\"\n",
" r = flow / (cap or CAP[s])\n",
" return T0[s] * (1 + a * r ** b + 6.0 * max(0.0, r - 1.0)) # ์ฉ๋ ์ด๊ณผ๋ถ์ ๋๊ธฐ์ด๋ก ๊ธ์ฆ\n",
"\n",
"\n",
"def max_flow(closed=(), capmul=None):\n",
" \"\"\"์ต๋์ ๋(๋/์)๊ณผ ์ต์ ์ ๋จ ๊ตฌ๊ฐ(๋ณ๋ชฉ). capmul = {๊ตฌ๊ฐ: ์ฉ๋ ๋ฐฐ์จ}.\"\"\"\n",
" cap = {s: (0 if s in closed else CAP[s] * (capmul or {}).get(s, 1.0)) for s in CAP}\n",
" res = {}\n",
" for s, (u, v) in ARCS.items():\n",
" res[(u, v)] = res.get((u, v), 0) + cap[s]\n",
" res.setdefault((v, u), 0)\n",
" flow = 0.0\n",
" while True:\n",
" prev, q = {SRC: None}, [SRC]\n",
" for u in q:\n",
" for (a, b), c in res.items():\n",
" if a == u and c > 1e-9 and b not in prev:\n",
" prev[b] = u; q.append(b)\n",
" if DST not in prev:\n",
" break\n",
" path, x = [], DST\n",
" while prev[x]:\n",
" path.append((prev[x], x)); x = prev[x]\n",
" f = min(res[e] for e in path)\n",
" for a, b in path:\n",
" res[(a, b)] -= f; res[(b, a)] += f\n",
" flow += f\n",
" reach = set(prev)\n",
" cut = [s for s, (u, v) in ARCS.items() if u in reach and v not in reach and cap[s] > 0]\n",
" return flow, cut\n",
"\n",
"\n",
"def all_paths():\n",
" \"\"\"SRC -> DST ๋จ์ ๊ฒฝ๋ก(๊ตฌ๊ฐ ์ด๋ฆ ํํ) ์ ์ฒด.\"\"\"\n",
" out = []\n",
"\n",
" def dfs(node, segs, seen):\n",
" if node == DST:\n",
" out.append(tuple(segs)); return\n",
" for s, (u, v) in ARCS.items():\n",
" if u == node and v not in seen:\n",
" dfs(v, segs + [s], seen | {v})\n",
" dfs(SRC, [], {SRC})\n",
" return out\n",
"\n",
"\n",
"PATHS = all_paths()\n",
"\n",
"\n",
"def assign(demand, closed=(), capmul=None, steps=40):\n",
" \"\"\"์ฆ๋ถ ๋ฐฐ์ : ์์๋ฅผ steps์กฐ๊ฐ์ผ๋ก ๋๋ , ์ ์ฒด ํต๊ณผ์๊ฐ(๋์ x ์๊ฐ) ์ฆ๊ฐ๊ฐ ๊ฐ์ฅ ์์ ๊ฒฝ๋ก์ ์ฐจ๋ก๋ก ์น๋๋ค(์์คํ
์ต์ ๊ทผ์ฌ).\"\"\"\n",
" cap = {s: CAP[s] * (capmul or {}).get(s, 1.0) for s in CAP}\n",
" flow, pf = {s: 0.0 for s in CAP}, {}\n",
" ok = [p for p in PATHS if not any(s in closed for s in p)]\n",
" if not ok:\n",
" return None\n",
" for _ in range(steps):\n",
" chunk = demand / steps\n",
" best = min(ok, key=lambda p: sum((flow[s] + chunk) * seg_time(s, flow[s] + chunk, cap[s]) - flow[s] * seg_time(s, flow[s], cap[s]) for s in p))\n",
" for s in best:\n",
" flow[s] += chunk\n",
" pf[best] = pf.get(best, 0) + chunk\n",
" tt = {p: sum(seg_time(s, flow[s], cap[s]) for s in p) for p in pf}\n",
" avg = sum(pf[p] * tt[p] for p in pf) / demand\n",
" return {\"flow\": flow, \"paths\": pf, \"path_min\": tt, \"avg_min\": avg, \"util\": {s: flow[s] / cap[s] for s in cap if cap[s] > 0}}\n",
"\n",
"\n",
"FIXED_SHARE = {(\"A1\", \"A2\", \"A3\"): 0.32, (\"B1\", \"B3\"): 0.40, (\"C1\", \"C2\"): 0.28} # ๊ณผ๊ฑฐ ๊ดํ: ์ถ์ ๋ณ ๊ณ ์ ๋ฐฐ๋ถ(๊ฐ์)\n",
"\n",
"\n",
"def fixed_assign(demand, closed=(), capmul=None):\n",
" \"\"\"๊ณผ๊ฑฐ ๊ดํ ๋ฐฐ๋ถ: ์ถ์ ๋ณ ๊ณ ์ ๋น์จ. ๋งํ ์ถ์ ๋ฌผ๋์ ์ด๋ ค ์๋ ์ถ์ ์ ๋น์จ๋๋ก ์ฌ๋ถ๋ฐฐ.\"\"\"\n",
" cap = {s: CAP[s] * (capmul or {}).get(s, 1.0) for s in CAP}\n",
" live = {p: w for p, w in FIXED_SHARE.items() if not any(s in closed for s in p)}\n",
" if not live:\n",
" return None\n",
" tot = sum(live.values())\n",
" flow = {s: 0.0 for s in CAP}\n",
" pf = {p: demand * w / tot for p, w in live.items()}\n",
" for p, f in pf.items():\n",
" for s in p:\n",
" flow[s] += f\n",
" tt = {p: sum(seg_time(s, flow[s], cap[s]) for s in p) for p in pf}\n",
" return {\"flow\": flow, \"paths\": pf, \"path_min\": tt, \"avg_min\": sum(pf[p] * tt[p] for p in pf) / demand,\n",
" \"util\": {s: flow[s] / cap[s] for s in cap if cap[s] > 0}}\n",
"\n",
"\n",
"def lp_assign(demand, closed=(), capmul=None):\n",
" \"\"\"์ ํ๊ณํ(LP): ๊ตฌ๊ฐ ์ฉ๋ ์ํ(์ด์ฉ๋ฅ rho) ์๋์์ ์์ ์ฃผํ์๊ฐ ํฉ์ ์ต์ํํ๋ ๊ฒฝ๋ก๋ณ ๋ฌผ๋. ๋ถ๊ฐ๋ฅํ๋ฉด rho๋ฅผ 1.0๊น์ง ์ํ.\"\"\"\n",
" cap = {s: CAP[s] * (capmul or {}).get(s, 1.0) for s in CAP}\n",
" ok = [p for p in PATHS if not any(s in closed for s in p)]\n",
" if not ok:\n",
" return None\n",
" for rho in (0.85, 0.95, 1.0):\n",
" r = linprog([sum(T0[s] for s in p) for p in ok], A_ub=[[1.0 if s in p else 0.0 for p in ok] for s in CAP],\n",
" b_ub=[rho * cap[s] for s in CAP], A_eq=[[1.0] * len(ok)], b_eq=[demand], bounds=(0, None), method=\"highs\")\n",
" if r.status == 0:\n",
" break\n",
" else:\n",
" return assign(demand, closed, capmul) # ์ฉ๋ ์ด๊ณผ ์์: ์ฆ๋ถ ๋ฐฐ์ ์ผ๋ก ๋์ฒด(๊ณผ๋ถํ ํ์)\n",
" flow = {s: sum(r.x[i] for i, p in enumerate(ok) if s in p) for s in CAP}\n",
" pf = {p: float(r.x[i]) for i, p in enumerate(ok) if r.x[i] > 1e-6}\n",
" tt = {p: sum(seg_time(s, flow[s], cap[s]) for s in p) for p in pf}\n",
" return {\"flow\": flow, \"paths\": pf, \"path_min\": tt, \"avg_min\": sum(pf[p] * tt[p] for p in pf) / demand,\n",
" \"util\": {s: flow[s] / cap[s] for s in cap if cap[s] > 0}, \"rho\": rho}\n",
"\n",
"\n",
"def cost_of(res, tv=1000, fuel_km=450):\n",
" \"\"\"์๊ฐ๋น ๋น์ฉ(์, ๊ต์ก์ฉ ๊ฐ์ ): ๋๋น ์๊ฐ๋น์ฉ tv์/๋ถ + ์ ๋ฅ๋น fuel_km์/km.\"\"\"\n",
" return sum(f * (res[\"path_min\"][p] * tv + sum(KM[s] for s in p) * fuel_km) for p, f in res[\"paths\"].items())\n",
"# ---------------------------------------------------------------------------\n",
"\n",
"df = make_data(); d = add_features(df)\n",
"tr, te = d[d.day < 120], d[d.day >= 120].copy()\n",
"gbr = GradientBoostingRegressor(n_estimators=200, max_depth=3, learning_rate=0.08, random_state=0).fit(tr[FEATS], tr[\"delay\"])\n",
"te[\"pred\"] = gbr.predict(te[FEATS])\n",
"\n",
"# ---------------- 1) ์ค๋ช
๊ฐ๋ฅ์ฑ: ์์ธ๊ตฐ์ ํ๊ท ๊ฐ์ผ๋ก ๋ฐ๊ฟจ์ ๋ ์์ธก์ด ์ผ๋ง๋ ๋ณํ๋๊ฐ ----------------\n",
"GROUPS = {\"๊ณํ ๋ฌผ๋ยท์ด์ฉ๋ฅ \": [\"plan_v\", \"util\", \"util_eff\", \"util_eff4\", \"bpr_base\", \"surge_ratio\", \"roll7_v\"], \"์ ๋นยท์ ํจ ์ฉ๋\": [\"maint\", \"cap_eff\"],\n",
" \"์ฐ์ฒ\": [\"rain\"], \"์ ์ผยท์ ์ฃผ ์ง์ฒด\": [\"lag1_delay\", \"lag7_delay\"], \"์์ผ ํจํด\": [\"is_weekend\", \"dow_sin\", \"dow_cos\"],\n",
" \"๊ตฌ๊ฐ ํน์ฑ\": [\"cap\", \"t0\", \"km\", \"k_๊ต๋\", \"k_ํฐ๋\", \"k_๊ต์ฐจ๋ก\"]}\n",
"mean_row = tr[FEATS].mean()\n",
"contrib = pd.DataFrame(index=te.index)\n",
"for g, cols in GROUPS.items():\n",
" x = te[FEATS].copy()\n",
" x[cols] = mean_row[cols].values\n",
" contrib[g] = te[\"pred\"] - gbr.predict(x) # ์์ = ์ด ์์ธ์ด ์ง์ฒด๋ฅผ ๋๋ฆผ\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) < 0.01).sum())\n",
"print(f\"[1] ์์ธก {len(te)}๊ฑด ์ค ๊ธฐ์ฌ๋ ์์ 3๊ฐ ์์ธ ์๋ ์ฐ์ถ {ok}๊ฑด ({ok / len(te):.0%}, ๋ชฉํ 100%)\")\n",
"imp = contrib.abs().mean().sort_values(ascending=False)\n",
"print(f\" (3๋ฒ์งธ ์์ธ ๊ธฐ์ฌ๊ฐ 0.01๋ถ ๋ฏธ๋ง์ธ ๋ฏธ๋ฏธํ ๊ฑด {weak}๊ฑด: ์์๋ ์ฐ์ถ๋๋ ์ค๋ช
๋ ฅ์ ๋ฎ์)\")\n",
"print(\" ์์ธ๊ตฐ๋ณ ํ๊ท ๊ธฐ์ฌ ํฌ๊ธฐ(๋ถ):\", \", \".join(f\"{k} {v:.2f}\" for k, v in imp.items()))\n",
"\n",
"\n",
"def explain(i):\n",
" r, c = te.loc[i], contrib.loc[i]\n",
" parts = [f\"{g}({c[g]:+.1f}๋ถ)\" for g in top3[i]]\n",
" sev = \"์ ์ฒด\" if r[\"util_eff\"] >= 0.9 else \"ํผ์ก\" if r[\"util_eff\"] >= 0.7 else \"์ํ\"\n",
" cause = {\"์ ๋นยท์ ํจ ์ฉ๋\": \"๋น๊ณํ ์ ๋น๋ก ํต๊ณผ ์ฉ๋์ด ์ค์ด\" if r[\"maint\"] else \"์ ํจ ์ฉ๋ ๋ณํ๋ก\", \"์ฐ์ฒ\": \"์ฐ์ฒ ์ํฅ์ผ๋ก\", \"๊ณํ ๋ฌผ๋ยท์ด์ฉ๋ฅ \": \"๊ณํ ๋ฌผ๋์ด ์ฉ๋์ ๊ทผ์ ํด\",\n",
" \"์ ์ผยท์ ์ฃผ ์ง์ฒด\": \"์ ์ผ ์ง์ฒด๊ฐ ์ด์ด์ ธ\", \"์์ผ ํจํด\": \"์์ผ ํจํด ๋๋ฌธ์\", \"๊ตฌ๊ฐ ํน์ฑ\": \"๊ตฌ๊ฐ ํน์ฑ์\"}[top3[i][0]]\n",
" return f\"{r['seg']} ๊ตฌ๊ฐ day {int(r['day'])}: {cause} ์ง์ฒด {r['pred']:.1f}๋ถ ์์({sev}). ์ฃผ์ ์์ธ {', '.join(parts)}\"\n",
"\n",
"\n",
"print(\"\\n[2] ์์ฐ์ด ์ค๋ช
(์์ธก ์ง์ฒด ์์ 4๊ฑด)\")\n",
"for i in te[\"pred\"].nlargest(4).index:\n",
" print(\" -\", explain(i))\n",
"\n",
"# ---------------- 2) ๋์๋ณด๋ ๋ฐ์ดํฐ: ์ต์ข
์ผ ๊ตฌ๊ฐ๋ณ ํผ์ก๋ยท๊ฒฝ๋ณด ----------------\n",
"last = te[te.day == te.day.max()].set_index(\"seg\")\n",
"last[\"์ํ\"] = np.where(last.util_eff >= 0.9, \"์ ์ฒด\", np.where(last.util_eff >= 0.7, \"ํผ์ก\", \"์ํ\"))\n",
"print(f\"\\n[3] ์ต์ข
์ผ ๊ตฌ๊ฐ ์ํํ: \" + \", \".join(f\"{s} {r.์ํ}({r.util_eff:.2f})\" for s, r in last.iterrows()))\n",
"alert = last[last[\"pred\"] >= 5].index.tolist()\n",
"print(f\" ์ง์ฒด 5๋ถ ์ด์ ์์ธก ๊ฒฝ๋ณด ๊ตฌ๊ฐ: {alert or '์์'} -> ์ฐํ ๊ฒํ ๋์(์ค์ ํ๋ฌ์ ์์ ๊ตฐ์๊ด ์น์ธ ํ)\")\n",
"\n",
"# ---------------- 3) ์ฐํ ๊ฒฝ๋ก ๋น๊ต: ๊ณผ๊ฑฐ ๊ดํ(์ถ์ ๊ณ ์ ๋ฐฐ๋ถ) vs ์ฆ๋ถ ๋ฐฐ์ ----------------\n",
"D = 200\n",
"cmp = []\n",
"for name, closed, cm in [(\"ํ์\", (), None), (\"A2 ํ์\", (\"A2\",), None), (\"A1 ์ ๋น(50%)\", (), {\"A1\": 0.5}), (\"B1 ์ ๋น(65%)\", (), {\"B1\": 0.65})]:\n",
" a, f = assign(D, closed, cm), fixed_assign(D, closed, cm)\n",
" cmp.append((name, f[\"avg_min\"], a[\"avg_min\"], max(f[\"util\"].values()), max(a[\"util\"].values())))\n",
"cm_df = pd.DataFrame(cmp, columns=[\"์๋๋ฆฌ์ค\", \"๊ณ ์ ๋ฐฐ๋ถ(๋ถ)\", \"์ฐํ ๋ฐฐ์ (๋ถ)\", \"๊ณ ์ ์ต๋ ์ด์ฉ๋ฅ \", \"๋ฐฐ์ ์ต๋ ์ด์ฉ๋ฅ \"]).round(2)\n",
"cm_df[\"๋จ์ถ๋ฅ \"] = (1 - cm_df[\"์ฐํ ๋ฐฐ์ (๋ถ)\"] / cm_df[\"๊ณ ์ ๋ฐฐ๋ถ(๋ถ)\"]).map(\"{:.0%}\".format)\n",
"print(f\"\\n[4] ์ฐํ ๊ฒฝ๋ก ๋น๊ต(์์ {D}๋/์)\")\n",
"print(cm_df.to_string(index=False))\n",
"\n",
"fig, ax = plt.subplots(2, 2, figsize=(13, 8))\n",
"piv = te.pivot(index=\"seg\", columns=\"day\", values=\"util_eff\").iloc[:, -14:]\n",
"im = ax[0, 0].imshow(piv.values, aspect=\"auto\", cmap=\"YlOrRd\", vmin=0.3, vmax=1.2); ax[0, 0].set_yticks(range(len(piv))); ax[0, 0].set_yticklabels(piv.index)\n",
"ax[0, 0].set_title(\"1 Congestion heatmap (effective utilization, last 14 days)\"); plt.colorbar(im, ax=ax[0, 0])\n",
"colors = [\"tab:red\" if s == \"์ ์ฒด\" else \"tab:orange\" if s == \"ํผ์ก\" else \"tab:green\" for s in last[\"์ํ\"]]\n",
"ax[0, 1].bar(last.index, last[\"pred\"], color=colors); ax[0, 1].axhline(5, ls=\"--\", c=\"k\"); ax[0, 1].set_title(\"2 Predicted delay today (min), dashed = alert 5\")\n",
"x = np.arange(len(cm_df)); ax[1, 0].bar(x - .2, cm_df[\"๊ณ ์ ๋ฐฐ๋ถ(๋ถ)\"], .4, label=\"fixed share\"); ax[1, 0].bar(x + .2, cm_df[\"์ฐํ ๋ฐฐ์ (๋ถ)\"], .4, label=\"detour assignment\")\n",
"ax[1, 0].set_xticks(x); ax[1, 0].set_xticklabels([\"normal\", \"A2 closed\", \"A1 maint\", \"B1 maint\"]); ax[1, 0].legend(); ax[1, 0].set_title(\"3 Detour comparison (avg transit min)\")\n",
"worst = te[\"pred\"].idxmax(); c = contrib.loc[worst]\n",
"ax[1, 1].barh([\"plan/util\", \"maint/cap\", \"rain\", \"lag delay\", \"weekday\", \"segment\"], c[list(GROUPS)].values); ax[1, 1].set_title(\"4 Contribution for worst case (min)\")\n",
"plt.suptitle(\"MOV01 bottleneck dashboard (synthetic)\"); plt.tight_layout(); plt.show()\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"๐ก [์ฐธ๊ณ ] ๋ฏธ์
06 ์์ ์คํ ๊ฒฐ๊ณผ (Output)
\n",
"\n",
"```text\n",
"[1] ์์ธก 300๊ฑด ์ค ๊ธฐ์ฌ๋ ์์ 3๊ฐ ์์ธ ์๋ ์ฐ์ถ 300๊ฑด (100%, ๋ชฉํ 100%)\n",
" (3๋ฒ์งธ ์์ธ ๊ธฐ์ฌ๊ฐ 0.01๋ถ ๋ฏธ๋ง์ธ ๋ฏธ๋ฏธํ ๊ฑด 7๊ฑด: ์์๋ ์ฐ์ถ๋๋ ์ค๋ช
๋ ฅ์ ๋ฎ์)\n",
" ์์ธ๊ตฐ๋ณ ํ๊ท ๊ธฐ์ฌ ํฌ๊ธฐ(๋ถ): ๊ณํ ๋ฌผ๋ยท์ด์ฉ๋ฅ 0.95, ์ ์ผยท์ ์ฃผ ์ง์ฒด 0.41, ๊ตฌ๊ฐ ํน์ฑ 0.15, ์์ผ ํจํด 0.06, ์ ๋นยท์ ํจ ์ฉ๋ 0.04, ์ฐ์ฒ 0.02\n",
"\n",
"[2] ์์ฐ์ด ์ค๋ช
(์์ธก ์ง์ฒด ์์ 4๊ฑด)\n",
" - C2 ๊ตฌ๊ฐ day 134: ๊ณํ ๋ฌผ๋์ด ์ฉ๋์ ๊ทผ์ ํด ์ง์ฒด 12.4๋ถ ์์(์ ์ฒด). ์ฃผ์ ์์ธ ๊ณํ ๋ฌผ๋ยท์ด์ฉ๋ฅ (+9.1๋ถ), ๊ตฌ๊ฐ ํน์ฑ(+4.0๋ถ), ์ ๋นยท์ ํจ ์ฉ๋(+2.0๋ถ)\n",
" - B1 ๊ตฌ๊ฐ day 120: ๊ณํ ๋ฌผ๋์ด ์ฉ๋์ ๊ทผ์ ํด ์ง์ฒด 10.9๋ถ ์์(์ ์ฒด). ์ฃผ์ ์์ธ ๊ณํ ๋ฌผ๋ยท์ด์ฉ๋ฅ (+8.8๋ถ), ์ ๋นยท์ ํจ ์ฉ๋(+0.9๋ถ), ๊ตฌ๊ฐ ํน์ฑ(+0.4๋ถ)\n",
" - A1 ๊ตฌ๊ฐ day 142: ๊ณํ ๋ฌผ๋์ด ์ฉ๋์ ๊ทผ์ ํด ์ง์ฒด 10.5๋ถ ์์(์ ์ฒด). ์ฃผ์ ์์ธ ๊ณํ ๋ฌผ๋ยท์ด์ฉ๋ฅ (+7.5๋ถ), ์ ๋นยท์ ํจ ์ฉ๋(+0.7๋ถ), ์ ์ผยท์ ์ฃผ ์ง์ฒด(-0.1๋ถ)\n",
" - A1 ๊ตฌ๊ฐ day 137: ๊ณํ ๋ฌผ๋์ด ์ฉ๋์ ๊ทผ์ ํด ์ง์ฒด 10.3๋ถ ์์(์ ์ฒด). ์ฃผ์ ์์ธ ๊ณํ ๋ฌผ๋ยท์ด์ฉ๋ฅ (+7.7๋ถ), ์ ๋นยท์ ํจ ์ฉ๋(+0.7๋ถ), ์ฐ์ฒ(+0.2๋ถ)\n",
"\n",
"[3] ์ต์ข
์ผ ๊ตฌ๊ฐ ์ํํ: A1 ์ ์ฒด(0.97), A2 ์ ์ฒด(0.97), A3 ํผ์ก(0.82), B1 ํผ์ก(0.89), B2 ํผ์ก(0.83), B3 ํผ์ก(0.81), C1 ์ํ(0.60), C2 ์ํ(0.69), X1 ์ํ(0.61), X2 ์ํ(0.46)\n",
" ์ง์ฒด 5๋ถ ์ด์ ์์ธก ๊ฒฝ๋ณด ๊ตฌ๊ฐ: ์์ -> ์ฐํ ๊ฒํ ๋์(์ค์ ํ๋ฌ์ ์์ ๊ตฐ์๊ด ์น์ธ ํ)\n",
"\n",
"[4] ์ฐํ ๊ฒฝ๋ก ๋น๊ต(์์ 200๋/์)\n",
" ์๋๋ฆฌ์ค ๊ณ ์ ๋ฐฐ๋ถ(๋ถ) ์ฐํ ๋ฐฐ์ (๋ถ) ๊ณ ์ ์ต๋ ์ด์ฉ๋ฅ ๋ฐฐ์ ์ต๋ ์ด์ฉ๋ฅ ๋จ์ถ๋ฅ \n",
" ํ์ 41.91 40.91 0.91 1.0 2%\n",
" A2 ํ์ 91.40 44.87 1.24 1.0 51%\n",
"A1 ์ ๋น(50%) 42.64 41.89 0.98 1.0 2%\n",
"B1 ์ ๋น(65%) 53.08 42.14 1.23 1.0 21%\n",
"```\n",
" \n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## ๐ฏ ๋ฏธ์
07. ์ฐํ๋ก ์์ก ๋ช
๋ น ํ๋ฌ์ ์ํ ์์ ๊ตฐ์๊ด HITL ํต์ ์ํฌํ๋ก์ฐ ์ค๊ณ\n",
"**์์์๊ฐ**: ์ฝ 3.5์๊ฐ\n",
"\n",
"### ๐ ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ์ํ ์ง์นจ\n",
"#### ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ๋น์์ฑ\n",
"\n",
"์ฐํ๋ก ์์ก ๋ช
๋ น ํ๋ฌ์ ์ํ ์์ ๊ตฐ์๊ด HITL ์ํฌํ๋ก์ฐ๋ฅผ ์ค๊ณํ๋ค. ์ค์ ์ ์ ํ์ฅ์์๋ ๋จ์ ํ์ง์ ๊ทธ์น์ง ์๊ณ ์ ์ํ ์ํ ํ๋จ๊ณผ ์ ๋ขฐ๋ ๋์ ์์ฌ๊ฒฐ์ ์ด ํ์์ ์ด๋ฏ๋ก, **์ฐํ ์์ก ๋ช
๋ น์ ํ๋ฌ ์ฌ๋ถ๋ฅผ ์์ ๊ตฐ์๊ด์ด ์ง์ ์น์ธยท๋ฐ๋ คํ๋ ์ธ๊ฐ ๊ฐ์
(HITL) ๊ฒฐ์ฌ ์ํฌํ๋ก์ฐ๋ฅผ ์ค๊ณํ๊ณ , ์ ํ๊ณํ์ผ๋ก ์ฐํ ๋ฌผ๋ ๋ฐฐ๋ถ์ ์ต์ ํํ๋ค.** ๊ณผ์
์ ์์ํ์ฌ ์คํ์ ๋ฐฉ์งํ๊ณ ์์ ๋์๋ ฅ์ ๊ทน๋ํํด์ผ ํฉ๋๋ค.\n",
"\n",
"์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ณธ ๋ฏธ์
์์๋ **์ ํ๊ณํ(LP)์ผ๋ก ๊ตฌ๊ฐ ์ฉ๋ ์ํ ์๋์์ ๊ฒฝ๋ก๋ณ ๋ฌผ๋์ ๋ฐฐ๋ถํ๊ณ ์๋๋ฆฌ์ค 6์ข
๊ณผ ๊ณผ๊ฑฐ 30์ผ ์กฐ๊ฑด์์ ๊ณ ์ ๋ฐฐ๋ถ ๋๋น ๋น์ฉ ์ ๊ฐ๋ฅ ์ ๋ฐฑํ
์คํธํ๋ฉฐ ๋๋ฐ ์์ ๋ฏผ๊ฐ๋ ํ์ธํ๊ณ ์ ํจ์ฑ์ ์ ๊ฒํ๋** ์ค์ต์ ์ฒด๊ณ์ ์ผ๋ก ์ํํฉ๋๋ค.\n",
"\n",
"ํ์ต์๋ ์ค์ต๊ฐ์ด๋์ ์ ์๋ ์ด 4๊ฐ ์ธ๋ถ ํ๋(๊ธฐ๋ณธ ๊ธฐ๋ฅ ๊ตฌํ โ ๊ฒฐ๊ณผ ์งํ ๊ฒ์ฆ โ ํ๋ผ๋ฏธํฐ ํ๋ ๋ฐ ๋์ ๊ณผ์ )์ ์์ฐจ์ ์ผ๋ก ์งํํ๋ฉฐ ํต์ฌ ํ์ ๊ธฐ์ค์ ๋ฌ์ฑํฉ๋๋ค.\n",
"\n",
"**์ด๋ฒ ๊ฒฐ๊ณผ๋ฌผ:** `src/optimization/mov01_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": [
"\"\"\"์ ํ๊ณํ(LP)์ผ๋ก ์ฐํ ์์ก ๋ฌผ๋ ๋ฐฐ๋ถ์ ์ต์ ํํด ๊ณผ๊ฑฐ ๊ดํ ๋๋น ๋น์ฉ ์ ๊ฐ์ ๋ฐฑํ
์คํธํ๊ณ , ์ฐํ ์์ก ๋ช
๋ น์ ์์ ๊ตฐ์๊ด์ด ์ง์ ์น์ธยท๋ฐ๋ คํ๋ 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.optimize import linprog\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# [๊ต์ก์ฉ๊ฐ์์๋ฃ] ๊ฐ์ ๋
ธ์ ๋งยท๊ตฌ๊ฐ ํต๊ณผ๋ ์์ฑ๊ธฐ (์ค์ ๋๋กยท์ง๋ช
ยท๋ถ๋ ์์)\n",
"# ๊ฐ์ ์ถ๋ฐ์ง(SRC)์์ ๊ฐ์ ๋ชฉ์ ์ง(DST)๊น์ง 3๊ฐ ์ถ์ (AยทBยทC)๊ณผ ์ฐ๊ฒฐ๋ก 2๊ฐ, ๊ตฌ๊ฐ 10๊ฐ. 150์ผ์น ํผํฌ์๊ฐ ํต๊ณผ๋ยท์ง์ฒด ์๊ฐ์ ๋ง๋ ๋ค.\n",
"# ---------------------------------------------------------------------------\n",
"SEGS = pd.DataFrame([\n",
" (\"A1\", \"SRC\", \"J1\", 12, 130, \"์ผ๋ฐ\"), (\"A2\", \"J1\", \"J2\", 8, 70, \"๊ต๋\"), (\"A3\", \"J2\", \"DST\", 10, 140, \"๊ต์ฐจ๋ก\"),\n",
" (\"B1\", \"SRC\", \"J3\", 15, 100, \"์ผ๋ฐ\"), (\"B2\", \"J3\", \"J2\", 9, 80, \"ํฐ๋\"), (\"B3\", \"J3\", \"DST\", 14, 95, \"์ผ๋ฐ\"),\n",
" (\"C1\", \"SRC\", \"J4\", 18, 70, \"์ผ๋ฐ\"), (\"C2\", \"J4\", \"DST\", 20, 75, \"๊ต๋\"),\n",
" (\"X1\", \"J1\", \"J3\", 7, 50, \"๊ต์ฐจ๋ก\"), (\"X2\", \"J4\", \"J3\", 10, 60, \"์ผ๋ฐ\")],\n",
" columns=[\"seg\", \"u\", \"v\", \"km\", \"cap\", \"kind\"]) # cap = ํผํฌ์๊ฐ ํต๊ณผ ์ฉ๋(๋/์, ๊ต์ก์ฉ ๊ฐ์ ๊ฐ)\n",
"KSPD = {\"์ผ๋ฐ\": 55, \"๊ต๋\": 40, \"ํฐ๋\": 45, \"๊ต์ฐจ๋ก\": 45} # ๊ตฌ๊ฐ ์ ํ๋ณ ์์ ์ฃผํ ์๋(km/h, ๊ฐ์ ๊ฐ)\n",
"SEGS[\"t0\"] = SEGS[\"km\"] / SEGS[\"kind\"].map(KSPD) * 60 # ์์ ์ฃผํ ์๊ฐ(๋ถ)\n",
"UTIL = dict(A1=.78, A2=.85, A3=.66, B1=.70, B2=.72, B3=.64, C1=.55, C2=.58, X1=.45, X2=.40) # ํ์ ์ด์ฉ๋ฅ \n",
"\n",
"\n",
"def bpr(t0, v, cap, a=0.15, b=4.0):\n",
" \"\"\"BPR ์ง์ฒด ํจ์(๊ตํต๊ณตํ์ ํ์คํ): ํต๊ณผ๋/์ฉ๋ ๋น๊ฐ ์ปค์ง์๋ก ์ง์ฒด๊ฐ ๊ธ์ฆํ๋ค. (๋ถ)\"\"\"\n",
" return t0 * a * (v / cap) ** b\n",
"\n",
"\n",
"def make_data(seed=11, days=150):\n",
" \"\"\"๊ตฌ๊ฐ x ์ผ์ ํจ๋. v=์ค์ ํผํฌ ํต๊ณผ๋, plan_v=์์ก๊ณํ ์๋(๊ธด๊ธ ๋ถ์ถ ๋ฏธ๋ฐ์), delay=์ค์ ์ง์ฒด(๋ถ).\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" d_idx = np.arange(days)\n",
" lvl = np.array([1.0, 1.08, 1.04, 1.10, 1.02, 0.82, 0.78])[d_idx % 7] * (1 + 0.0012 * d_idx)\n",
" lvl = lvl * np.where((d_idx >= 100) & (d_idx < 110), 1.30, 1.0) # ์์ก๋ ํญ์ฆ ๊ธฐ๊ฐ(๊ฐ์)\n",
" prev, rows = {s: 0.0 for s in SEGS[\"seg\"]}, []\n",
" for d in d_idx:\n",
" rain = bool(rng.random() < 0.18)\n",
" for r in SEGS.itertuples():\n",
" maint, urgent = bool(rng.random() < 0.06), bool(rng.random() < 0.05) # ๋น๊ณํ ์ ๋น / ๊ธด๊ธ ๋ถ์ถ\n",
" v = r.cap * UTIL[r.seg] * lvl[d] * (1.25 if urgent else 1.0) * rng.normal(1, 0.05)\n",
" cap_eff = r.cap * (0.65 if maint else 1.0) * (0.92 if rain else 1.0)\n",
" delay = max(r.t0 * 0.2 * (v / cap_eff) ** 3.5 + 0.25 * prev[r.seg] + rng.normal(0, 0.4), 0.0)\n",
" prev[r.seg] = delay\n",
" rows.append(dict(day=int(d), dow=int(d % 7), seg=r.seg, kind=r.kind, km=r.km, cap=r.cap, t0=r.t0,\n",
" plan_v=v / (1.25 if urgent else 1.0) * rng.normal(1, 0.04), v=v, maint=int(maint), urgent=int(urgent),\n",
" rain=int(rain), cap_eff=cap_eff, delay=delay))\n",
" df = pd.DataFrame(rows)\n",
" df[\"transit\"] = df[\"t0\"] * (1 + 0.08 * df[\"rain\"]) + df[\"delay\"]\n",
" return df\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",
"# ๋
ธ์ ๋ง ํ๋ฆ ๋๊ตฌ: ์ต๋์ ๋(Edmonds-Karp), ์ฆ๋ถ ๋ฐฐ์ (ํผ์ก ๋ฐ์ ์ต๋จ์๊ฐ ๊ฒฝ๋ก), ๊ฒฝ๋ก ์ด๊ฑฐ\n",
"# ---------------------------------------------------------------------------\n",
"CAP = dict(zip(SEGS[\"seg\"], SEGS[\"cap\"]))\n",
"T0 = dict(zip(SEGS[\"seg\"], SEGS[\"t0\"]))\n",
"KM = dict(zip(SEGS[\"seg\"], SEGS[\"km\"]))\n",
"ARCS = {r.seg: (r.u, r.v) for r in SEGS.itertuples()}\n",
"SRC, DST = \"SRC\", \"DST\"\n",
"\n",
"\n",
"def seg_time(s, flow, cap=None, a=0.2, b=3.5):\n",
" \"\"\"๊ตฌ๊ฐ ํต๊ณผ์๊ฐ(๋ถ) = ์์ ์ฃผํ + ํผ์ก ์ง์ฒด. cap์ ์ฃผ๋ฉด ์ฉ๋์ ๋์ฒด(์ ๋นยท๊ธฐ์ ๋ฐ์).\"\"\"\n",
" r = flow / (cap or CAP[s])\n",
" return T0[s] * (1 + a * r ** b + 6.0 * max(0.0, r - 1.0)) # ์ฉ๋ ์ด๊ณผ๋ถ์ ๋๊ธฐ์ด๋ก ๊ธ์ฆ\n",
"\n",
"\n",
"def max_flow(closed=(), capmul=None):\n",
" \"\"\"์ต๋์ ๋(๋/์)๊ณผ ์ต์ ์ ๋จ ๊ตฌ๊ฐ(๋ณ๋ชฉ). capmul = {๊ตฌ๊ฐ: ์ฉ๋ ๋ฐฐ์จ}.\"\"\"\n",
" cap = {s: (0 if s in closed else CAP[s] * (capmul or {}).get(s, 1.0)) for s in CAP}\n",
" res = {}\n",
" for s, (u, v) in ARCS.items():\n",
" res[(u, v)] = res.get((u, v), 0) + cap[s]\n",
" res.setdefault((v, u), 0)\n",
" flow = 0.0\n",
" while True:\n",
" prev, q = {SRC: None}, [SRC]\n",
" for u in q:\n",
" for (a, b), c in res.items():\n",
" if a == u and c > 1e-9 and b not in prev:\n",
" prev[b] = u; q.append(b)\n",
" if DST not in prev:\n",
" break\n",
" path, x = [], DST\n",
" while prev[x]:\n",
" path.append((prev[x], x)); x = prev[x]\n",
" f = min(res[e] for e in path)\n",
" for a, b in path:\n",
" res[(a, b)] -= f; res[(b, a)] += f\n",
" flow += f\n",
" reach = set(prev)\n",
" cut = [s for s, (u, v) in ARCS.items() if u in reach and v not in reach and cap[s] > 0]\n",
" return flow, cut\n",
"\n",
"\n",
"def all_paths():\n",
" \"\"\"SRC -> DST ๋จ์ ๊ฒฝ๋ก(๊ตฌ๊ฐ ์ด๋ฆ ํํ) ์ ์ฒด.\"\"\"\n",
" out = []\n",
"\n",
" def dfs(node, segs, seen):\n",
" if node == DST:\n",
" out.append(tuple(segs)); return\n",
" for s, (u, v) in ARCS.items():\n",
" if u == node and v not in seen:\n",
" dfs(v, segs + [s], seen | {v})\n",
" dfs(SRC, [], {SRC})\n",
" return out\n",
"\n",
"\n",
"PATHS = all_paths()\n",
"\n",
"\n",
"def assign(demand, closed=(), capmul=None, steps=40):\n",
" \"\"\"์ฆ๋ถ ๋ฐฐ์ : ์์๋ฅผ steps์กฐ๊ฐ์ผ๋ก ๋๋ , ์ ์ฒด ํต๊ณผ์๊ฐ(๋์ x ์๊ฐ) ์ฆ๊ฐ๊ฐ ๊ฐ์ฅ ์์ ๊ฒฝ๋ก์ ์ฐจ๋ก๋ก ์น๋๋ค(์์คํ
์ต์ ๊ทผ์ฌ).\"\"\"\n",
" cap = {s: CAP[s] * (capmul or {}).get(s, 1.0) for s in CAP}\n",
" flow, pf = {s: 0.0 for s in CAP}, {}\n",
" ok = [p for p in PATHS if not any(s in closed for s in p)]\n",
" if not ok:\n",
" return None\n",
" for _ in range(steps):\n",
" chunk = demand / steps\n",
" best = min(ok, key=lambda p: sum((flow[s] + chunk) * seg_time(s, flow[s] + chunk, cap[s]) - flow[s] * seg_time(s, flow[s], cap[s]) for s in p))\n",
" for s in best:\n",
" flow[s] += chunk\n",
" pf[best] = pf.get(best, 0) + chunk\n",
" tt = {p: sum(seg_time(s, flow[s], cap[s]) for s in p) for p in pf}\n",
" avg = sum(pf[p] * tt[p] for p in pf) / demand\n",
" return {\"flow\": flow, \"paths\": pf, \"path_min\": tt, \"avg_min\": avg, \"util\": {s: flow[s] / cap[s] for s in cap if cap[s] > 0}}\n",
"\n",
"\n",
"FIXED_SHARE = {(\"A1\", \"A2\", \"A3\"): 0.32, (\"B1\", \"B3\"): 0.40, (\"C1\", \"C2\"): 0.28} # ๊ณผ๊ฑฐ ๊ดํ: ์ถ์ ๋ณ ๊ณ ์ ๋ฐฐ๋ถ(๊ฐ์)\n",
"\n",
"\n",
"def fixed_assign(demand, closed=(), capmul=None):\n",
" \"\"\"๊ณผ๊ฑฐ ๊ดํ ๋ฐฐ๋ถ: ์ถ์ ๋ณ ๊ณ ์ ๋น์จ. ๋งํ ์ถ์ ๋ฌผ๋์ ์ด๋ ค ์๋ ์ถ์ ์ ๋น์จ๋๋ก ์ฌ๋ถ๋ฐฐ.\"\"\"\n",
" cap = {s: CAP[s] * (capmul or {}).get(s, 1.0) for s in CAP}\n",
" live = {p: w for p, w in FIXED_SHARE.items() if not any(s in closed for s in p)}\n",
" if not live:\n",
" return None\n",
" tot = sum(live.values())\n",
" flow = {s: 0.0 for s in CAP}\n",
" pf = {p: demand * w / tot for p, w in live.items()}\n",
" for p, f in pf.items():\n",
" for s in p:\n",
" flow[s] += f\n",
" tt = {p: sum(seg_time(s, flow[s], cap[s]) for s in p) for p in pf}\n",
" return {\"flow\": flow, \"paths\": pf, \"path_min\": tt, \"avg_min\": sum(pf[p] * tt[p] for p in pf) / demand,\n",
" \"util\": {s: flow[s] / cap[s] for s in cap if cap[s] > 0}}\n",
"\n",
"\n",
"def lp_assign(demand, closed=(), capmul=None):\n",
" \"\"\"์ ํ๊ณํ(LP): ๊ตฌ๊ฐ ์ฉ๋ ์ํ(์ด์ฉ๋ฅ rho) ์๋์์ ์์ ์ฃผํ์๊ฐ ํฉ์ ์ต์ํํ๋ ๊ฒฝ๋ก๋ณ ๋ฌผ๋. ๋ถ๊ฐ๋ฅํ๋ฉด rho๋ฅผ 1.0๊น์ง ์ํ.\"\"\"\n",
" cap = {s: CAP[s] * (capmul or {}).get(s, 1.0) for s in CAP}\n",
" ok = [p for p in PATHS if not any(s in closed for s in p)]\n",
" if not ok:\n",
" return None\n",
" for rho in (0.85, 0.95, 1.0):\n",
" r = linprog([sum(T0[s] for s in p) for p in ok], A_ub=[[1.0 if s in p else 0.0 for p in ok] for s in CAP],\n",
" b_ub=[rho * cap[s] for s in CAP], A_eq=[[1.0] * len(ok)], b_eq=[demand], bounds=(0, None), method=\"highs\")\n",
" if r.status == 0:\n",
" break\n",
" else:\n",
" return assign(demand, closed, capmul) # ์ฉ๋ ์ด๊ณผ ์์: ์ฆ๋ถ ๋ฐฐ์ ์ผ๋ก ๋์ฒด(๊ณผ๋ถํ ํ์)\n",
" flow = {s: sum(r.x[i] for i, p in enumerate(ok) if s in p) for s in CAP}\n",
" pf = {p: float(r.x[i]) for i, p in enumerate(ok) if r.x[i] > 1e-6}\n",
" tt = {p: sum(seg_time(s, flow[s], cap[s]) for s in p) for p in pf}\n",
" return {\"flow\": flow, \"paths\": pf, \"path_min\": tt, \"avg_min\": sum(pf[p] * tt[p] for p in pf) / demand,\n",
" \"util\": {s: flow[s] / cap[s] for s in cap if cap[s] > 0}, \"rho\": rho}\n",
"\n",
"\n",
"def cost_of(res, tv=1000, fuel_km=450):\n",
" \"\"\"์๊ฐ๋น ๋น์ฉ(์, ๊ต์ก์ฉ ๊ฐ์ ): ๋๋น ์๊ฐ๋น์ฉ tv์/๋ถ + ์ ๋ฅ๋น fuel_km์/km.\"\"\"\n",
" return sum(f * (res[\"path_min\"][p] * tv + sum(KM[s] for s in p) * fuel_km) for p, f in res[\"paths\"].items())\n",
"# ---------------------------------------------------------------------------\n",
"\n",
"# ---------------- 1) ์ต์ ํ ์๋ฎฌ๋ ์ดํฐ: ๊ณผ๊ฑฐ ๊ดํ(์ถ์ ๊ณ ์ ๋ฐฐ๋ถ) vs LP ----------------\n",
"SC = [(\"ํ์\", 200, (), None), (\"์์ก๋ ํญ์ฆ(+30%)\", 260, (), None), (\"A2 ๊ต๋ ์ ๋น(์ฉ๋ 65%)\", 200, (), {\"A2\": .65}), (\"A2 ๊ต๋ ํ์\", 200, (\"A2\",), None),\n",
" (\"A2 ํ์ + ์ฐ์ฒ\", 200, (\"A2\",), {k: .92 for k in CAP}), (\"B1 ์ ๋น + ํญ์ฆ\", 250, (), {\"B1\": .65})]\n",
"rows, plans = [], {}\n",
"for name, D, cl, cm in SC:\n",
" f, l = fixed_assign(D, cl, cm), lp_assign(D, cl, cm)\n",
" cf, cl_ = cost_of(f), cost_of(l)\n",
" rows.append((name, D, f[\"avg_min\"], l[\"avg_min\"], cf / 1e4, cl_ / 1e4, 1 - cl_ / cf))\n",
" plans[name] = (D, cl, cm, l, cf, cl_)\n",
"res = pd.DataFrame(rows, columns=[\"์๋๋ฆฌ์ค\", \"์์\", \"๊ณ ์ ๋ฐฐ๋ถ(๋ถ)\", \"LP(๋ถ)\", \"๊ณ ์ ๋น์ฉ(๋ง์/์)\", \"LP ๋น์ฉ(๋ง์/์)\", \"์ ๊ฐ๋ฅ \"])\n",
"print(\"[1] ์๋๋ฆฌ์ค๋ณ ๋น์ฉ ๋น๊ต(์๊ฐยท์ ๋ฅ ๋จ๊ฐ๋ ๊ต์ก์ฉ ๊ฐ์ ๊ฐ)\")\n",
"print(res.assign(์ ๊ฐ๋ฅ =res[\"์ ๊ฐ๋ฅ \"].map(\"{:.1%}\".format)).round(1).to_string(index=False))\n",
"avg = res[\"์ ๊ฐ๋ฅ \"].mean()\n",
"print(f\" ์๋๋ฆฌ์ค 6์ข
ํ๊ท ์ ๊ฐ {avg:.1%}, ์ฐํ๊ฐ ํ์ํ 4๊ฐ ์๋๋ฆฌ์ค ํ๊ท {res['์ ๊ฐ๋ฅ '].iloc[2:].mean():.1%} (๋ชฉํ 15% ์ด์: {'๋ฌ์ฑ' if avg >= .15 else '๋ฏธ๋ฌ'}), ํ์๋ {res['์ ๊ฐ๋ฅ '].iloc[0]:.1%}์ ๊ทธ์น๋ค\")\n",
"\n",
"# ---------------- 2) ๋ฐฑํ
์คํ
: ๊ณผ๊ฑฐ 30์ผ(day 120~149) ์ค์ ์กฐ๊ฑด์ผ๋ก ์ฌํ ----------------\n",
"df = make_data()\n",
"rec = []\n",
"for day, g in df[df.day >= 120].groupby(\"day\"):\n",
" D = float(g[g.seg.isin([\"A1\", \"B1\", \"C1\"])][\"plan_v\"].sum())\n",
" cm = {r.seg: r.cap_eff / r.cap for r in g.itertuples() if r.cap_eff < r.cap}\n",
" f, l = fixed_assign(D, (), cm), lp_assign(D, (), cm)\n",
" rec.append((day, D, cost_of(f), cost_of(l)))\n",
"bt = pd.DataFrame(rec, columns=[\"day\", \"D\", \"fix\", \"lp\"])\n",
"bt[\"save\"] = 1 - bt[\"lp\"] / bt[\"fix\"]\n",
"print(f\"\\n[2] ๋ฐฑํ
์คํ
(30์ผ): ํ๊ท ์ ๊ฐ {bt['save'].mean():.1%}(๋ชฉํ 15% ์ด์: {'๋ฌ์ฑ' if bt['save'].mean() >= .15 else '๋ฏธ๋ฌ'}), ์ต๋ {bt['save'].max():.1%}(day {int(bt.loc[bt['save'].idxmax(), 'day'])}), ์ ๊ฐ 0 ์ดํ์ธ ๋ {int((bt['save'] <= 0).sum())}์ผ\")\n",
"print(f\" ์ ๊ฐ 10% ๋ฏธ๋ง์ธ ๋ {int((bt['save'] < .10).sum())}์ผ: ํจ๊ณผ๋ ์ ๋นยทํญ์ฆ์ด ๊ฒน์น ๋ ์ ์ง์ค๋๋ค. ๊ณผ๊ฑฐ ๊ดํ ๋ฐฐ๋ถ์ ๊ฐ์ ์ด๋ฏ๋ก ์ค์ ๋ถ๋ ์ค์ ์ผ๋ก๋ ๋ณ๋ ๊ฒ์ฆ์ด ํ์ํ๋ค.\")\n",
"\n",
"# ---------------- 3) ๋ฏผ๊ฐ๋: ๋๋ฐ ์์(์ ์ ์ ํ ๊ฐ์ ) ----------------\n",
"sens = []\n",
"for k in (0.8, 1.0, 1.2, 1.4, 1.6):\n",
" f, l = fixed_assign(200 * k, (\"A2\",)), lp_assign(200 * k, (\"A2\",))\n",
" sens.append((f\"x{k}\", 200 * k, max(l[\"util\"].values()), 1 - cost_of(l) / cost_of(f)))\n",
"sdf = pd.DataFrame(sens, columns=[\"์์ ๋ฐฐ์จ\", \"์์\", \"LP ์ต๋ ์ด์ฉ๋ฅ \", \"์ ๊ฐ๋ฅ \"])\n",
"print(\"[3] ๋๋ฐ ์์ ๋ฏผ๊ฐ๋(A2 ํ์ ์ํฉ):\", \"; \".join(f\"{r['์์ ๋ฐฐ์จ']} -> ์ด์ฉ๋ฅ {r['LP ์ต๋ ์ด์ฉ๋ฅ ']:.2f}, ์ ๊ฐ {r['์ ๊ฐ๋ฅ ']:.0%}\" for _, r in sdf.iterrows()))\n",
"print(\" ์์ x1.2 ์ด์์์๋ ์ต๋์ ๋(220๋/์)์ ๋์ด ์ฐํ๋ง์ผ๋ก ์์ฉํ ์ ์์ผ๋ฏ๋ก ๋ฌผ๋ ์กฐ์ ๊ฒฐ์ ์ด ํ์ํ๋ค.\")\n",
"\n",
"# ---------------- 4) HITL ๊ฒฐ์ฌ ์ํฌํ๋ก์ฐ ----------------\n",
"USERS = {\"plan_a\": \"PLN\", \"plan_b\": \"PLN\", \"ops_kim\": \"OPS\", \"ops_lee\": \"OPS\", \"sup_park\": \"SUP\", \"aud_han\": \"AUD\"}\n",
"PERM = {\"PLN\": {\"submit\", \"redraft\"}, \"OPS\": {\"approve\", \"reject\"}, \"SUP\": {\"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, scen, 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",
" D, cl, cm, l, cf, cl_ = plans[scen]\n",
" extra = l[\"avg_min\"] - plans[\"ํ์\"][3][\"avg_min\"]\n",
" big = extra >= 3 or max(l[\"util\"].values()) > 0.98 # ์ถ๊ฐ ์์ 3๋ถ ์ด์ ๋๋ ์ฉ๋ ํฌํ -> 2๋จ๊ณ ๊ฒฐ์ฌ\n",
" did = f\"ORD{len(self.docs) + 1:02d}\"\n",
" self.docs[did] = {\"scen\": scen, \"line\": [\"OPS\", \"SUP\"] if big else [\"OPS\"], \"step\": 0, \"status\": \"๊ฒํ ๋๊ธฐ\", \"drafter\": user, \"done\": [], \"parent\": parent}\n",
" self._audit(user, \"submit\", did, f\"{scen} ์ถ๊ฐ์์ {extra:+.1f}๋ถ\")\n",
" return did\n",
"\n",
" def decide(self, user, did, act, reason=\"\"):\n",
" role, d = USERS.get(user), self.docs.get(did)\n",
" if role is None: return self.deny(user, act, did, \"๋ฏธ๋ฑ๋ก ์ฌ์ฉ์\")\n",
" if d is None or d[\"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 d[\"line\"][d[\"step\"]] != role: return self.deny(user, act, did, \"๊ฒฐ์ฌ ์์ ์๋ฐ\")\n",
" if user == d[\"drafter\"] or user in d[\"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",
" d[\"status\"] = \"๋ฐ๋ ค\"\n",
" else:\n",
" d[\"done\"].append(user); d[\"step\"] += 1\n",
" d[\"status\"] = \"์น์ธ\" if d[\"step\"] == len(d[\"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",
"wf = Flow()\n",
"names = [\"A2 ๊ต๋ ํ์\", \"A2 ๊ต๋ ์ ๋น(์ฉ๋ 65%)\", \"์์ก๋ ํญ์ฆ(+30%)\", \"B1 ์ ๋น + ํญ์ฆ\", \"A2 ํ์ + ์ฐ์ฒ\", \"ํ์\"]\n",
"ids = [wf.submit(\"plan_a\" if i % 2 == 0 else \"plan_b\", n) for i, n in enumerate(names)]\n",
"print(f\"\\n[4] AI ์ฐํ์ {len(ids)}๊ฑด ์ ๋ถ '๊ฒํ ๋๊ธฐ' ๋ฑ๋ก, ์๋ ์น์ธ {sum(wf.docs[i]['status'] == '์น์ธ' for i in ids)}๊ฑด (๋ชฉํ 0๊ฑด). 2๋จ๊ณ ๊ฒฐ์ฌ ๋์ {[i for i in ids if len(wf.docs[i]['line']) == 2]}\")\n",
"wf.decide(\"ops_kim\", ids[0], \"reject\", \"์ถ๊ฐ ์์์ ์ฉ๋ ํฌํ ์ฐ๋ ค, ๋ฌผ๋ ์กฐ์ ์ ํฌํจํด ์ฌ๊ธฐ์\")\n",
"new = wf.submit(\"plan_a\", names[0], parent=ids[0])\n",
"wf.decide(\"ops_kim\", new, \"approve\", \"์ฐํ ๊ฒฝ๋ก ์ฉ๋ ํ์ธ, ์๊ธ ์น์ธ ์์ ํจ\")\n",
"wf.decide(\"sup_park\", new, \"approve\", \"์ฐํ ์ํ, ์ดํ ์ค ์ง์ฒด ๋ณด๊ณ ์กฐ๊ฑด\")\n",
"wf.decide(\"ops_lee\", ids[1], \"approve\", \"์ ๋น ๊ตฌ๊ฐ ์ฉ๋ ๊ฐ์๋ถ ์ฐํ ๋ฐ์ ํ๋น\")\n",
"wf.decide(\"ops_kim\", ids[5], \"approve\", \"ํ์ ๋ฌผ๋์ผ๋ก ๋ณ๊ฒฝ ์ฌ์ ์์ด ๊ธฐ์กด ์ ์ง\")\n",
"attacks = [(\"plan_a\", ids[2], \"approve\", \"๋ด๊ฐ ์ฌ๋ฆฐ ์์ ์ง์ ์น์ธํ๋ค\", \"์์ฒญ์ ์๊ธฐ ์น์ธ\"), (\"aud_han\", ids[2], \"approve\", \"๊ฐ์ฌ ๊ณ์ ์ผ๋ก ์น์ธ ์๋\", \"๊ฐ์ฌ ์ญํ ์น์ธ\"),\n",
" (\"ghost\", ids[2], \"approve\", \"์ธ๋ถ ๊ณ์ ์ผ๋ก ์น์ธ ์๋\", \"๋ฏธ๋ฑ๋ก ์ฌ์ฉ์\"), (\"ops_kim\", ids[2], \"approve\", \"\", \"์ฌ์ ์๋ ์น์ธ\"),\n",
" (\"ops_kim\", ids[2], \"reject\", \"์งง์\", \"์ฌ์ ์งง์ ๋ฐ๋ ค\"), (\"sup_park\", ids[2], \"approve\", \"์๊ธ์๊ฐ ์์๋ฅผ ๊ฑด๋๋ฐ์ด ์น์ธ\", \"๊ฒฐ์ฌ ์์ ๊ฑด๋๋ฐ๊ธฐ\"),\n",
" (\"ops_kim\", ids[5], \"approve\", \"์ด๋ฏธ ์น์ธ๋ ๋ฌธ์๋ฅผ ๋ค์ ์น์ธ\", \"์ฒ๋ฆฌ ์๋ฃ ์ฌ์น์ธ\"), (\"sup_park\", new, \"reject\", \"์น์ธ๋ ๋ฌธ์๋ฅผ ๋ค์ง์ด ๋ฐ๋ ค\", \"์น์ธ ํ ๋ฒ๋ณต\"),\n",
" (\"plan_b\", ids[3], \"approve\", \"์์ฒญ์ ๊ณ์ ์ผ๋ก ์น์ธ ์๋ํจ\", \"์์ฒญ์ ๊ถํ ์๋ฐ\")]\n",
"for u, doc, act, rs, _ in attacks:\n",
" wf.decide(u, doc, act, rs)\n",
"wf.edit(\"plan_a\", new) # 10) ์น์ธ ํ ๋ช
๋ น ๋ณ๊ฒฝ\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:<9} {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\" {ids[0]}(๋ฐ๋ ค) -> {new}(์ฌ๊ธฐ์, parent ์ฐ๊ฒฐ) ์ด๋ ฅ:\", [(r['action'], r['user']) for r in wf.log if r['doc'] in (ids[0], 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=\"fixed share\"); ax[0].bar(x + .2, res[\"LP ๋น์ฉ(๋ง์/์)\"], .4, label=\"LP\")\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(\"Cost per hour by scenario\")\n",
"ax[1].plot(sdf[\"์์\"], sdf[\"์ ๊ฐ๋ฅ \"] * 100, \"o-\"); ax[1].set_title(\"Sensitivity: demand vs saving (%), A2 closed\"); ax[1].set_xlabel(\"demand (veh/h)\")\n",
"plt.tight_layout(); plt.show()\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"๐ก [์ฐธ๊ณ ] ๋ฏธ์
07 ์์ ์คํ ๊ฒฐ๊ณผ (Output)
\n",
"\n",
"```text\n",
"[1] ์๋๋ฆฌ์ค๋ณ ๋น์ฉ ๋น๊ต(์๊ฐยท์ ๋ฅ ๋จ๊ฐ๋ ๊ต์ก์ฉ ๊ฐ์ ๊ฐ)\n",
" ์๋๋ฆฌ์ค ์์ ๊ณ ์ ๋ฐฐ๋ถ(๋ถ) LP(๋ถ) ๊ณ ์ ๋น์ฉ(๋ง์/์) LP ๋น์ฉ(๋ง์/์) ์ ๊ฐ๋ฅ \n",
" ํ์ 200 41.9 41.8 1124.8 1122.7 0.2%\n",
" ์์ก๋ ํญ์ฆ(+30%) 260 57.2 44.8 1860.5 1547.9 16.8%\n",
"A2 ๊ต๋ ์ ๋น(์ฉ๋ 65%) 200 53.3 42.2 1351.6 1138.9 15.7%\n",
" A2 ๊ต๋ ํ์ 200 91.4 45.1 2122.3 1207.2 43.1%\n",
" A2 ํ์ + ์ฐ์ฒ 200 119.7 47.0 2688.8 1245.4 53.7%\n",
" B1 ์ ๋น + ํญ์ฆ 250 76.5 46.2 2270.9 1523.7 32.9%\n",
" ์๋๋ฆฌ์ค 6์ข
ํ๊ท ์ ๊ฐ 27.1%, ์ฐํ๊ฐ ํ์ํ 4๊ฐ ์๋๋ฆฌ์ค ํ๊ท 36.4% (๋ชฉํ 15% ์ด์: ๋ฌ์ฑ), ํ์๋ 0.2%์ ๊ทธ์น๋ค\n",
"\n",
"[2] ๋ฐฑํ
์คํ
(30์ผ): ํ๊ท ์ ๊ฐ 16.0%(๋ชฉํ 15% ์ด์: ๋ฌ์ฑ), ์ต๋ 48.5%(day 134), ์ ๊ฐ 0 ์ดํ์ธ ๋ 0์ผ\n",
" ์ ๊ฐ 10% ๋ฏธ๋ง์ธ ๋ 12์ผ: ํจ๊ณผ๋ ์ ๋นยทํญ์ฆ์ด ๊ฒน์น ๋ ์ ์ง์ค๋๋ค. ๊ณผ๊ฑฐ ๊ดํ ๋ฐฐ๋ถ์ ๊ฐ์ ์ด๋ฏ๋ก ์ค์ ๋ถ๋ ์ค์ ์ผ๋ก๋ ๋ณ๋ ๊ฒ์ฆ์ด ํ์ํ๋ค.\n",
"[3] ๋๋ฐ ์์ ๋ฏผ๊ฐ๋(A2 ํ์ ์ํฉ): x0.8 -> ์ด์ฉ๋ฅ 0.85, ์ ๊ฐ 4%; x1.0 -> ์ด์ฉ๋ฅ 0.95, ์ ๊ฐ 43%; x1.2 -> ์ด์ฉ๋ฅ 1.32, ์ ๊ฐ 58%; x1.4 -> ์ด์ฉ๋ฅ 1.54, ์ ๊ฐ 58%; x1.6 -> ์ด์ฉ๋ฅ 1.76, ์ ๊ฐ 56%\n",
" ์์ x1.2 ์ด์์์๋ ์ต๋์ ๋(220๋/์)์ ๋์ด ์ฐํ๋ง์ผ๋ก ์์ฉํ ์ ์์ผ๋ฏ๋ก ๋ฌผ๋ ์กฐ์ ๊ฒฐ์ ์ด ํ์ํ๋ค.\n",
"\n",
"[4] AI ์ฐํ์ 6๊ฑด ์ ๋ถ '๊ฒํ ๋๊ธฐ' ๋ฑ๋ก, ์๋ ์น์ธ 0๊ฑด (๋ชฉํ 0๊ฑด). 2๋จ๊ณ ๊ฒฐ์ฌ ๋์ ['ORD01', 'ORD03', 'ORD04', 'ORD05']\n",
" ๊ถํ ์ฐํ ์๋ 10๊ฑด ์ค ์ฐจ๋จ 10๊ฑด (์ฐจ๋จ์จ 100%, ๋ชฉํ 100%)\n",
" ์ฐจ๋จ plan_a approve ORD03: PLN ์ญํ ์ approve ๋ถ๊ฐ\n",
" ์ฐจ๋จ aud_han approve ORD03: AUD ์ญํ ์ approve ๋ถ๊ฐ\n",
" ์ฐจ๋จ ghost approve ORD03: ๋ฏธ๋ฑ๋ก ์ฌ์ฉ์\n",
" ์ฐจ๋จ ops_kim approve ORD03: ์ฌ์ 10์ ์ด์ ํ์\n",
" ๊ฐ์ฌ ๋ก๊ทธ 22๊ฑด ํด์ ์ฒด์ธ ๋ฌด๊ฒฐ์ฑ 100%; 4๋ฒ ๊ธฐ๋ก ์ฌํ ์์ ์ ์๋ณ์กฐ ๊ฒ์ถ(4๋ฒ)\n",
" ORD01(๋ฐ๋ ค) -> ORD07(์ฌ๊ธฐ์, parent ์ฐ๊ฒฐ) ์ด๋ ฅ: [('submit', 'plan_a'), ('reject', 'ops_kim'), ('submit', 'plan_a'), ('approve', 'ops_kim'), ('approve', 'sup_park'), ('DENIED:reject', 'sup_park'), ('DENIED:edit', 'plan_a')]\n",
" ๋ฌธ์ ์ํ: {'์น์ธ': 3, '๊ฒํ ๋๊ธฐ': 3, '๋ฐ๋ ค': 1} | ์ค์ ์ฐํ ๋ช
๋ น ํ๋ฌ์ ์งํ๊ณํต(์์ ๊ตฐ์๊ด ์น์ธ) ๊ฒฐ์ฌ์ ๋ฐ๋ฅธ๋ค.\n",
"```\n",
" \n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## ๐ฏ ๋ฏธ์
08. ์ฃผ์ ์ถ์ 3๊ฐ ๋์ ๋ด์ ์ํฉ ๋ชจ์ ์ฐํ ์์ก ์๋ฎฌ๋ ์ด์
์คํธ๋ ์ค ํ
์คํธ\n",
"**์์์๊ฐ**: ์ฝ 3.0์๊ฐ\n",
"\n",
"### ๐ ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ์ํ ์ง์นจ\n",
"#### ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ๋น์์ฑ\n",
"\n",
"์ฃผ์ ์ถ์ ๋ด์ ๋ชจ์ ์ฐํ ์์ก ์๋ฎฌ๋ ์ด์
์คํธ๋ ์ค ํ
์คํธ๋ฅผ ์ํํ๋ค. ์ค์ ์ ์ ํ์ฅ์์๋ ๋จ์ ํ์ง์ ๊ทธ์น์ง ์๊ณ ์ ์ํ ์ํ ํ๋จ๊ณผ ์ ๋ขฐ๋ ๋์ ์์ฌ๊ฒฐ์ ์ด ํ์์ ์ด๋ฏ๋ก, **๊ตฌ๊ฐ 3๊ฐ๊ฐ ๋์์ ๋งํ๋ ์ํฉ์ ๋ชจ์ํ๋ ์คํธ๋ ์ค ์ํ์ผ๋ก ๋
ธ์ ๋ง์ ์ทจ์ฝ ์กฐํฉ๊ณผ ๋์๋ณด๋ ๊ฐฑ์ ์ฑ๋ฅยท๊ฒฝ๋ณด ์ ํ๋๋ฅผ ํ์ธํ๋ค.** ๊ณผ์
์ ์์ํ์ฌ ์คํ์ ๋ฐฉ์งํ๊ณ ์์ ๋์๋ ฅ์ ๊ทน๋ํํด์ผ ํฉ๋๋ค.\n",
"\n",
"์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ณธ ๋ฏธ์
์์๋ **๊ตฌ๊ฐ 10๊ฐ ์ค 3๊ฐ๊ฐ ๋์์ ๋งํ๋ 120๊ฐ ์กฐํฉ๊ณผ 3๊ฐ ์ถ์ ์ ์ ๊ทผ ๊ตฌ๊ฐ ๋์ ๋ด์๋ฅผ ๋ชจ์ํด ๋จ์ , ์์ ์ด๊ณผ, ์์ฉ ๊ฐ๋ฅ ์กฐํฉ์ ๋ถ๋ฅํ๊ณ ๋จ์ ์กฐํฉ์ ์์ฃผ ๋ฑ์ฅํ๋ ๊ตฌ๊ฐ์ ์ด์คํ ํ๋ณด๋ก ์ ์ ์ํํ๊ณ ๊ฒ์ฆํ๋** ์ค์ต์ ์ฒด๊ณ์ ์ผ๋ก ์ํํฉ๋๋ค.\n",
"\n",
"ํ์ต์๋ ์ค์ต๊ฐ์ด๋์ ์ ์๋ ์ด 4๊ฐ ์ธ๋ถ ํ๋(๊ธฐ๋ณธ ๊ธฐ๋ฅ ๊ตฌํ โ ๊ฒฐ๊ณผ ์งํ ๊ฒ์ฆ โ ํ๋ผ๋ฏธํฐ ํ๋ ๋ฐ ๋์ ๊ณผ์ )์ ์์ฐจ์ ์ผ๋ก ์งํํ๋ฉฐ ํต์ฌ ํ์ ๊ธฐ์ค์ ๋ฌ์ฑํฉ๋๋ค.\n",
"\n",
"**์ด๋ฒ ๊ฒฐ๊ณผ๋ฌผ:** `src/dashboard/mov01_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": [
"\"\"\"๊ฐ์ ๋
ธ์ ๋ง์์ ๊ตฌ๊ฐ 3๊ฐ๊ฐ ๋์์ ๋งํ๋ ๋ชจ๋ ์กฐํฉ(120๊ฐ์ง)๊ณผ ์ถ์ 3๊ฐ ์ ๋ฉด ๋ด์๋ฅผ ๋ชจ์ํด ๋จ์ ยท๊ณผ๋ถํยท์ถ๊ฐ ์ง์ฒด๋ฅผ ํ๊ฐํ๊ณ , ๋์๋ณด๋ ๊ฐฑ์ ์๊ฐ(2์ด ์ด๋ด)๊ณผ ์ง์ฒด ๊ฒฝ๋ณด ์ ํ๋(95% ์ด์)๋ฅผ ์คํธ๋ ์ค ์กฐ๊ฑด์์ ์ธก์ ํ๋ค.\"\"\"\n",
"import itertools\n",
"import time\n",
"import numpy as np\n",
"import pandas as pd\n",
"import matplotlib.pyplot as plt\n",
"from scipy.optimize import linprog\n",
"from sklearn.ensemble import GradientBoostingRegressor\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# [๊ต์ก์ฉ๊ฐ์์๋ฃ] ๊ฐ์ ๋
ธ์ ๋งยท๊ตฌ๊ฐ ํต๊ณผ๋ ์์ฑ๊ธฐ (์ค์ ๋๋กยท์ง๋ช
ยท๋ถ๋ ์์)\n",
"# ๊ฐ์ ์ถ๋ฐ์ง(SRC)์์ ๊ฐ์ ๋ชฉ์ ์ง(DST)๊น์ง 3๊ฐ ์ถ์ (AยทBยทC)๊ณผ ์ฐ๊ฒฐ๋ก 2๊ฐ, ๊ตฌ๊ฐ 10๊ฐ. 150์ผ์น ํผํฌ์๊ฐ ํต๊ณผ๋ยท์ง์ฒด ์๊ฐ์ ๋ง๋ ๋ค.\n",
"# ---------------------------------------------------------------------------\n",
"SEGS = pd.DataFrame([\n",
" (\"A1\", \"SRC\", \"J1\", 12, 130, \"์ผ๋ฐ\"), (\"A2\", \"J1\", \"J2\", 8, 70, \"๊ต๋\"), (\"A3\", \"J2\", \"DST\", 10, 140, \"๊ต์ฐจ๋ก\"),\n",
" (\"B1\", \"SRC\", \"J3\", 15, 100, \"์ผ๋ฐ\"), (\"B2\", \"J3\", \"J2\", 9, 80, \"ํฐ๋\"), (\"B3\", \"J3\", \"DST\", 14, 95, \"์ผ๋ฐ\"),\n",
" (\"C1\", \"SRC\", \"J4\", 18, 70, \"์ผ๋ฐ\"), (\"C2\", \"J4\", \"DST\", 20, 75, \"๊ต๋\"),\n",
" (\"X1\", \"J1\", \"J3\", 7, 50, \"๊ต์ฐจ๋ก\"), (\"X2\", \"J4\", \"J3\", 10, 60, \"์ผ๋ฐ\")],\n",
" columns=[\"seg\", \"u\", \"v\", \"km\", \"cap\", \"kind\"]) # cap = ํผํฌ์๊ฐ ํต๊ณผ ์ฉ๋(๋/์, ๊ต์ก์ฉ ๊ฐ์ ๊ฐ)\n",
"KSPD = {\"์ผ๋ฐ\": 55, \"๊ต๋\": 40, \"ํฐ๋\": 45, \"๊ต์ฐจ๋ก\": 45} # ๊ตฌ๊ฐ ์ ํ๋ณ ์์ ์ฃผํ ์๋(km/h, ๊ฐ์ ๊ฐ)\n",
"SEGS[\"t0\"] = SEGS[\"km\"] / SEGS[\"kind\"].map(KSPD) * 60 # ์์ ์ฃผํ ์๊ฐ(๋ถ)\n",
"UTIL = dict(A1=.78, A2=.85, A3=.66, B1=.70, B2=.72, B3=.64, C1=.55, C2=.58, X1=.45, X2=.40) # ํ์ ์ด์ฉ๋ฅ \n",
"\n",
"\n",
"def bpr(t0, v, cap, a=0.15, b=4.0):\n",
" \"\"\"BPR ์ง์ฒด ํจ์(๊ตํต๊ณตํ์ ํ์คํ): ํต๊ณผ๋/์ฉ๋ ๋น๊ฐ ์ปค์ง์๋ก ์ง์ฒด๊ฐ ๊ธ์ฆํ๋ค. (๋ถ)\"\"\"\n",
" return t0 * a * (v / cap) ** b\n",
"\n",
"\n",
"def make_data(seed=11, days=150):\n",
" \"\"\"๊ตฌ๊ฐ x ์ผ์ ํจ๋. v=์ค์ ํผํฌ ํต๊ณผ๋, plan_v=์์ก๊ณํ ์๋(๊ธด๊ธ ๋ถ์ถ ๋ฏธ๋ฐ์), delay=์ค์ ์ง์ฒด(๋ถ).\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" d_idx = np.arange(days)\n",
" lvl = np.array([1.0, 1.08, 1.04, 1.10, 1.02, 0.82, 0.78])[d_idx % 7] * (1 + 0.0012 * d_idx)\n",
" lvl = lvl * np.where((d_idx >= 100) & (d_idx < 110), 1.30, 1.0) # ์์ก๋ ํญ์ฆ ๊ธฐ๊ฐ(๊ฐ์)\n",
" prev, rows = {s: 0.0 for s in SEGS[\"seg\"]}, []\n",
" for d in d_idx:\n",
" rain = bool(rng.random() < 0.18)\n",
" for r in SEGS.itertuples():\n",
" maint, urgent = bool(rng.random() < 0.06), bool(rng.random() < 0.05) # ๋น๊ณํ ์ ๋น / ๊ธด๊ธ ๋ถ์ถ\n",
" v = r.cap * UTIL[r.seg] * lvl[d] * (1.25 if urgent else 1.0) * rng.normal(1, 0.05)\n",
" cap_eff = r.cap * (0.65 if maint else 1.0) * (0.92 if rain else 1.0)\n",
" delay = max(r.t0 * 0.2 * (v / cap_eff) ** 3.5 + 0.25 * prev[r.seg] + rng.normal(0, 0.4), 0.0)\n",
" prev[r.seg] = delay\n",
" rows.append(dict(day=int(d), dow=int(d % 7), seg=r.seg, kind=r.kind, km=r.km, cap=r.cap, t0=r.t0,\n",
" plan_v=v / (1.25 if urgent else 1.0) * rng.normal(1, 0.04), v=v, maint=int(maint), urgent=int(urgent),\n",
" rain=int(rain), cap_eff=cap_eff, delay=delay))\n",
" df = pd.DataFrame(rows)\n",
" df[\"transit\"] = df[\"t0\"] * (1 + 0.08 * df[\"rain\"]) + df[\"delay\"]\n",
" return df\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",
"# ์ง์ฒด ์์ธก์ฉ ํ์ ํผ์ฒ ์์ฑ(๋น์ผ ์ค์ ์์ ์ ์ ์ ์๋ ์ ๋ณด๋ง ์ฌ์ฉ: ๊ณํ ๋ฌผ๋, ๊ณต์ง ์ ๋น, ๊ธฐ์ ์๋ณด, ์ ์ผยท์ ์ฃผ ์ค์ )\n",
"# ---------------------------------------------------------------------------\n",
"FEATS = [\"plan_v\", \"cap\", \"cap_eff\", \"t0\", \"km\", \"maint\", \"rain\", \"util\", \"util_eff\", \"util_eff4\", \"bpr_base\", \"lag1_delay\", \"lag7_delay\",\n",
" \"roll7_v\", \"surge_ratio\", \"is_weekend\", \"dow_sin\", \"dow_cos\", \"k_๊ต๋\", \"k_ํฐ๋\", \"k_๊ต์ฐจ๋ก\"]\n",
"\n",
"\n",
"def add_features(df):\n",
" d = df.sort_values([\"seg\", \"day\"]).copy()\n",
" g = d.groupby(\"seg\")\n",
" d[\"lag1_delay\"] = g[\"delay\"].shift(1)\n",
" d[\"lag7_delay\"] = g[\"delay\"].shift(7)\n",
" d[\"roll7_v\"] = g[\"v\"].transform(lambda s: s.shift(1).rolling(7).mean())\n",
" d[\"util\"] = d[\"plan_v\"] / d[\"cap\"]\n",
" d[\"util_eff\"] = d[\"plan_v\"] / d[\"cap_eff\"]\n",
" d[\"util_eff4\"] = d[\"util_eff\"] ** 4\n",
" d[\"bpr_base\"] = bpr(d[\"t0\"], d[\"plan_v\"], d[\"cap\"])\n",
" d[\"surge_ratio\"] = d[\"plan_v\"] / d[\"roll7_v\"]\n",
" d[\"is_weekend\"] = (d[\"dow\"] >= 5).astype(int)\n",
" d[\"dow_sin\"], d[\"dow_cos\"] = np.sin(2 * np.pi * d[\"dow\"] / 7), np.cos(2 * np.pi * d[\"dow\"] / 7)\n",
" for k in (\"๊ต๋\", \"ํฐ๋\", \"๊ต์ฐจ๋ก\"):\n",
" d[\"k_\" + k] = (d[\"kind\"] == k).astype(int)\n",
" return d.dropna().sort_values([\"day\", \"seg\"]).reset_index(drop=True)\n",
"# ---------------------------------------------------------------------------\n",
"# ---------------------------------------------------------------------------\n",
"# ๋
ธ์ ๋ง ํ๋ฆ ๋๊ตฌ: ์ต๋์ ๋(Edmonds-Karp), ์ฆ๋ถ ๋ฐฐ์ (ํผ์ก ๋ฐ์ ์ต๋จ์๊ฐ ๊ฒฝ๋ก), ๊ฒฝ๋ก ์ด๊ฑฐ\n",
"# ---------------------------------------------------------------------------\n",
"CAP = dict(zip(SEGS[\"seg\"], SEGS[\"cap\"]))\n",
"T0 = dict(zip(SEGS[\"seg\"], SEGS[\"t0\"]))\n",
"KM = dict(zip(SEGS[\"seg\"], SEGS[\"km\"]))\n",
"ARCS = {r.seg: (r.u, r.v) for r in SEGS.itertuples()}\n",
"SRC, DST = \"SRC\", \"DST\"\n",
"\n",
"\n",
"def seg_time(s, flow, cap=None, a=0.2, b=3.5):\n",
" \"\"\"๊ตฌ๊ฐ ํต๊ณผ์๊ฐ(๋ถ) = ์์ ์ฃผํ + ํผ์ก ์ง์ฒด. cap์ ์ฃผ๋ฉด ์ฉ๋์ ๋์ฒด(์ ๋นยท๊ธฐ์ ๋ฐ์).\"\"\"\n",
" r = flow / (cap or CAP[s])\n",
" return T0[s] * (1 + a * r ** b + 6.0 * max(0.0, r - 1.0)) # ์ฉ๋ ์ด๊ณผ๋ถ์ ๋๊ธฐ์ด๋ก ๊ธ์ฆ\n",
"\n",
"\n",
"def max_flow(closed=(), capmul=None):\n",
" \"\"\"์ต๋์ ๋(๋/์)๊ณผ ์ต์ ์ ๋จ ๊ตฌ๊ฐ(๋ณ๋ชฉ). capmul = {๊ตฌ๊ฐ: ์ฉ๋ ๋ฐฐ์จ}.\"\"\"\n",
" cap = {s: (0 if s in closed else CAP[s] * (capmul or {}).get(s, 1.0)) for s in CAP}\n",
" res = {}\n",
" for s, (u, v) in ARCS.items():\n",
" res[(u, v)] = res.get((u, v), 0) + cap[s]\n",
" res.setdefault((v, u), 0)\n",
" flow = 0.0\n",
" while True:\n",
" prev, q = {SRC: None}, [SRC]\n",
" for u in q:\n",
" for (a, b), c in res.items():\n",
" if a == u and c > 1e-9 and b not in prev:\n",
" prev[b] = u; q.append(b)\n",
" if DST not in prev:\n",
" break\n",
" path, x = [], DST\n",
" while prev[x]:\n",
" path.append((prev[x], x)); x = prev[x]\n",
" f = min(res[e] for e in path)\n",
" for a, b in path:\n",
" res[(a, b)] -= f; res[(b, a)] += f\n",
" flow += f\n",
" reach = set(prev)\n",
" cut = [s for s, (u, v) in ARCS.items() if u in reach and v not in reach and cap[s] > 0]\n",
" return flow, cut\n",
"\n",
"\n",
"def all_paths():\n",
" \"\"\"SRC -> DST ๋จ์ ๊ฒฝ๋ก(๊ตฌ๊ฐ ์ด๋ฆ ํํ) ์ ์ฒด.\"\"\"\n",
" out = []\n",
"\n",
" def dfs(node, segs, seen):\n",
" if node == DST:\n",
" out.append(tuple(segs)); return\n",
" for s, (u, v) in ARCS.items():\n",
" if u == node and v not in seen:\n",
" dfs(v, segs + [s], seen | {v})\n",
" dfs(SRC, [], {SRC})\n",
" return out\n",
"\n",
"\n",
"PATHS = all_paths()\n",
"\n",
"\n",
"def assign(demand, closed=(), capmul=None, steps=40):\n",
" \"\"\"์ฆ๋ถ ๋ฐฐ์ : ์์๋ฅผ steps์กฐ๊ฐ์ผ๋ก ๋๋ , ์ ์ฒด ํต๊ณผ์๊ฐ(๋์ x ์๊ฐ) ์ฆ๊ฐ๊ฐ ๊ฐ์ฅ ์์ ๊ฒฝ๋ก์ ์ฐจ๋ก๋ก ์น๋๋ค(์์คํ
์ต์ ๊ทผ์ฌ).\"\"\"\n",
" cap = {s: CAP[s] * (capmul or {}).get(s, 1.0) for s in CAP}\n",
" flow, pf = {s: 0.0 for s in CAP}, {}\n",
" ok = [p for p in PATHS if not any(s in closed for s in p)]\n",
" if not ok:\n",
" return None\n",
" for _ in range(steps):\n",
" chunk = demand / steps\n",
" best = min(ok, key=lambda p: sum((flow[s] + chunk) * seg_time(s, flow[s] + chunk, cap[s]) - flow[s] * seg_time(s, flow[s], cap[s]) for s in p))\n",
" for s in best:\n",
" flow[s] += chunk\n",
" pf[best] = pf.get(best, 0) + chunk\n",
" tt = {p: sum(seg_time(s, flow[s], cap[s]) for s in p) for p in pf}\n",
" avg = sum(pf[p] * tt[p] for p in pf) / demand\n",
" return {\"flow\": flow, \"paths\": pf, \"path_min\": tt, \"avg_min\": avg, \"util\": {s: flow[s] / cap[s] for s in cap if cap[s] > 0}}\n",
"\n",
"\n",
"FIXED_SHARE = {(\"A1\", \"A2\", \"A3\"): 0.32, (\"B1\", \"B3\"): 0.40, (\"C1\", \"C2\"): 0.28} # ๊ณผ๊ฑฐ ๊ดํ: ์ถ์ ๋ณ ๊ณ ์ ๋ฐฐ๋ถ(๊ฐ์)\n",
"\n",
"\n",
"def fixed_assign(demand, closed=(), capmul=None):\n",
" \"\"\"๊ณผ๊ฑฐ ๊ดํ ๋ฐฐ๋ถ: ์ถ์ ๋ณ ๊ณ ์ ๋น์จ. ๋งํ ์ถ์ ๋ฌผ๋์ ์ด๋ ค ์๋ ์ถ์ ์ ๋น์จ๋๋ก ์ฌ๋ถ๋ฐฐ.\"\"\"\n",
" cap = {s: CAP[s] * (capmul or {}).get(s, 1.0) for s in CAP}\n",
" live = {p: w for p, w in FIXED_SHARE.items() if not any(s in closed for s in p)}\n",
" if not live:\n",
" return None\n",
" tot = sum(live.values())\n",
" flow = {s: 0.0 for s in CAP}\n",
" pf = {p: demand * w / tot for p, w in live.items()}\n",
" for p, f in pf.items():\n",
" for s in p:\n",
" flow[s] += f\n",
" tt = {p: sum(seg_time(s, flow[s], cap[s]) for s in p) for p in pf}\n",
" return {\"flow\": flow, \"paths\": pf, \"path_min\": tt, \"avg_min\": sum(pf[p] * tt[p] for p in pf) / demand,\n",
" \"util\": {s: flow[s] / cap[s] for s in cap if cap[s] > 0}}\n",
"\n",
"\n",
"def lp_assign(demand, closed=(), capmul=None):\n",
" \"\"\"์ ํ๊ณํ(LP): ๊ตฌ๊ฐ ์ฉ๋ ์ํ(์ด์ฉ๋ฅ rho) ์๋์์ ์์ ์ฃผํ์๊ฐ ํฉ์ ์ต์ํํ๋ ๊ฒฝ๋ก๋ณ ๋ฌผ๋. ๋ถ๊ฐ๋ฅํ๋ฉด rho๋ฅผ 1.0๊น์ง ์ํ.\"\"\"\n",
" cap = {s: CAP[s] * (capmul or {}).get(s, 1.0) for s in CAP}\n",
" ok = [p for p in PATHS if not any(s in closed for s in p)]\n",
" if not ok:\n",
" return None\n",
" for rho in (0.85, 0.95, 1.0):\n",
" r = linprog([sum(T0[s] for s in p) for p in ok], A_ub=[[1.0 if s in p else 0.0 for p in ok] for s in CAP],\n",
" b_ub=[rho * cap[s] for s in CAP], A_eq=[[1.0] * len(ok)], b_eq=[demand], bounds=(0, None), method=\"highs\")\n",
" if r.status == 0:\n",
" break\n",
" else:\n",
" return assign(demand, closed, capmul) # ์ฉ๋ ์ด๊ณผ ์์: ์ฆ๋ถ ๋ฐฐ์ ์ผ๋ก ๋์ฒด(๊ณผ๋ถํ ํ์)\n",
" flow = {s: sum(r.x[i] for i, p in enumerate(ok) if s in p) for s in CAP}\n",
" pf = {p: float(r.x[i]) for i, p in enumerate(ok) if r.x[i] > 1e-6}\n",
" tt = {p: sum(seg_time(s, flow[s], cap[s]) for s in p) for p in pf}\n",
" return {\"flow\": flow, \"paths\": pf, \"path_min\": tt, \"avg_min\": sum(pf[p] * tt[p] for p in pf) / demand,\n",
" \"util\": {s: flow[s] / cap[s] for s in cap if cap[s] > 0}, \"rho\": rho}\n",
"\n",
"\n",
"def cost_of(res, tv=1000, fuel_km=450):\n",
" \"\"\"์๊ฐ๋น ๋น์ฉ(์, ๊ต์ก์ฉ ๊ฐ์ ): ๋๋น ์๊ฐ๋น์ฉ tv์/๋ถ + ์ ๋ฅ๋น fuel_km์/km.\"\"\"\n",
" return sum(f * (res[\"path_min\"][p] * tv + sum(KM[s] for s in p) * fuel_km) for p, f in res[\"paths\"].items())\n",
"# ---------------------------------------------------------------------------\n",
"\n",
"D = 150\n",
"base = assign(D)\n",
"print(f\"[0] ํ์: ์์ {D}๋/์, ํ๊ท ํต๊ณผ {base['avg_min']:.1f}๋ถ, ์ต๋์ ๋ {max_flow()[0]:.0f}๋/์\")\n",
"\n",
"# ---------------- 1) ์ถ์ 3๊ฐ(AยทBยทC) ์ ๋ฉด ๋ด์ ----------------\n",
"axes_cut = (\"A3\", \"B3\", \"C2\") # 3๊ฐ ์ถ์ ์ ๋ชฉ์ ์ง ์ ๊ทผ ๊ตฌ๊ฐ\n",
"mf, _ = max_flow(axes_cut)\n",
"print(f\"[1] 3๊ฐ ์ถ์ ์ ๊ทผ ๊ตฌ๊ฐ(A3ยทB3ยทC2) ๋์ ๋ด์: ์ต๋์ ๋ {mf:.0f}๋/์, ๊ฒฝ๋ก {len(PATHS) - len([p for p in PATHS if not any(s in axes_cut for s in p)])}/{len(PATHS)}๊ฐ ์ฐจ๋จ -> \"\n",
" + (\"๋๋ก ์์ก ๋ถ๊ฐ. ๊ณต์คยทํด์ ๋ฑ ํ ์๋จ ๋๋ ๋ฌผ๋ ๋ณด๋ฅ ๊ฒฐ์ ์ด ํ์ํ๋ฉฐ ์ฌ๋์ด ํ๋จํ๋ค\" if assign(D, axes_cut) is None else \"์ฐํ ๊ฐ๋ฅ\"))\n",
"\n",
"# ---------------- 2) ๊ตฌ๊ฐ 3๊ฐ ๋์ ๋ด์ 120์กฐํฉ ----------------\n",
"rows = []\n",
"t0 = time.perf_counter()\n",
"for c in itertools.combinations(CAP, 3):\n",
" flow, cut = max_flow(c)\n",
" r = assign(D, c) if flow > 0 else None\n",
" rows.append({\"closed\": \"+\".join(c), \"maxflow\": flow, \"avg_min\": r[\"avg_min\"] if r else np.nan, \"max_util\": max(r[\"util\"].values()) if r else np.nan})\n",
"el = time.perf_counter() - t0\n",
"st = pd.DataFrame(rows)\n",
"st[\"์ํ\"] = np.where(st[\"maxflow\"] <= 0, \"๋จ์ \", np.where(st[\"maxflow\"] < D, \"์์ ์ด๊ณผ\", np.where(st[\"avg_min\"] > base[\"avg_min\"] * 1.5, \"์ฌ๊ฐ ์ง์ฒด\", \"์์ฉ\")))\n",
"vc = st[\"์ํ\"].value_counts()\n",
"print(f\"\\n[2] 120๊ฐ ์กฐํฉ ํ๊ฐ({el:.2f}์ด): \" + \", \".join(f\"{k} {v}\" for k, v in vc.items()))\n",
"ok_rows = st[st[\"์ํ\"].isin([\"์์ฉ\", \"์ฌ๊ฐ ์ง์ฒด\"])]\n",
"print(f\" ์์ฉ ๊ฐ๋ฅ ์กฐํฉ์ ํ๊ท ํต๊ณผ์๊ฐ {ok_rows['avg_min'].mean():.1f}๋ถ(ํ์ {base['avg_min']:.1f}๋ถ), ์ต์
์์ฉ ์กฐํฉ {ok_rows.loc[ok_rows['avg_min'].idxmax(), 'closed']} {ok_rows['avg_min'].max():.1f}๋ถ\")\n",
"cnt = pd.Series([s for c in st[st[\"์ํ\"] == \"๋จ์ \"][\"closed\"] for s in c.split(\"+\")]).value_counts()\n",
"print(f\" ๋จ์ ์กฐํฉ์ ์์ฃผ ํฌํจ๋ ๊ตฌ๊ฐ: {', '.join(f'{k}({v})' for k, v in cnt.head(4).items())} -> ์ด์คํ(์ฐํ๋ก ํ๋ณด) ์ฐ์ ํ๋ณด\")\n",
"\n",
"# ---------------- 3) ๋์๋ณด๋ ๊ฐฑ์ ๋ถํ: ๊ตฌ๊ฐ ์์ธก + ์ต๋์ ๋ + ๋ฐฐ์ ์ ๋ฐ๋ณต ----------------\n",
"df = make_data(); d = add_features(df)\n",
"tr, te = d[d.day < 120], d[d.day >= 120].copy()\n",
"gbr = GradientBoostingRegressor(n_estimators=200, max_depth=3, learning_rate=0.08, random_state=0).fit(tr[FEATS], tr[\"delay\"])\n",
"\n",
"\n",
"def refresh(day, closed=()):\n",
" x = te[te.day == day]\n",
" pred = gbr.predict(x[FEATS])\n",
" flow, cut = max_flow(closed)\n",
" r = assign(float(x[x.seg.isin([\"A1\", \"B1\", \"C1\"])][\"plan_v\"].sum()), closed)\n",
" return pred, flow, cut, r\n",
"\n",
"\n",
"lat = []\n",
"rng = np.random.default_rng(8)\n",
"combos = list(itertools.combinations(CAP, 2))\n",
"for k in range(60):\n",
" c = combos[int(rng.integers(len(combos)))]\n",
" t1 = time.perf_counter(); refresh(120 + k % 30, c); lat.append(time.perf_counter() - t1)\n",
"lat = np.array(lat)\n",
"print(f\"\\n[3] ๋์๋ณด๋ ๋ฐ์ดํฐ ๊ฐฑ์ 60ํ(๊ตฌ๊ฐ 2๊ฐ ํ์ ๋ฌด์์): ํ๊ท {lat.mean() * 1000:.0f}ms, ์ต๋ {lat.max() * 1000:.0f}ms (๋ชฉํ 2์ด ์ด๋ด: {'๋ฌ์ฑ' if lat.max() < 2 else '๋ฏธ๋ฌ'}; ํ๋ฉด ๋ ๋๋ง ์๊ฐ์ ์ ์ธ)\")\n",
"\n",
"# ---------------- 4) ๊ฒฝ๋ณด ์ ํ๋: ์์ธก ์ง์ฒด 5๋ถ ์ด์ ๊ฒฝ๋ณด vs ์ค์ 5๋ถ ์ด์ ----------------\n",
"te[\"pred\"] = gbr.predict(te[FEATS])\n",
"a_pred, a_true = te[\"pred\"] >= 5, te[\"delay\"] >= 5\n",
"acc = float((a_pred == a_true).mean())\n",
"tp, fp, fn = int((a_pred & a_true).sum()), int((a_pred & ~a_true).sum()), int((~a_pred & a_true).sum())\n",
"print(f\"[4] ๊ฒฝ๋ณด ์ ํ๋: {acc:.1%} (๋ชฉํ 95% ์ด์: {'๋ฌ์ฑ' if acc >= .95 else '๋ฏธ๋ฌ'}), ์ค์ ์ง์ฒด 5๋ถ ์ด์ {int(a_true.sum())}๊ฑด ์ค ํ์ง {tp}๊ฑด(์ฌํ์จ {tp / max(tp + fn, 1):.0%}), ์ค๊ฒฝ๋ณด {fp}๊ฑด\")\n",
"print(f\" ๊ฒฝ๋ณด ๊ธฐ์ค์ 3๋ถ์ผ๋ก ๋ฎ์ถ๋ฉด: \", end=\"\")\n",
"a3 = te[\"pred\"] >= 3; t3 = te[\"delay\"] >= 5\n",
"print(f\"์ฌํ์จ {float((a3 & t3).sum() / max(t3.sum(), 1)):.0%}, ์ค๊ฒฝ๋ณด {int((a3 & ~t3).sum())}๊ฑด -> ๋์น์ง ์๋ ์ชฝ์ด ์ค์ํ๋ฉด ๊ธฐ์ค์ ๋ฎ์ถ๋ ์ค๊ฒฝ๋ณด ํผ๋ก๋ฅผ ํจ๊ป ๋ณธ๋ค\")\n",
"print(\"๋ชจ๋ ๋ชจ์๋ ๊ต์ก์ฉ ๊ฐ์ ๋
ธ์ ์ด๋ฉฐ, ์ค์ ๋ด์ยท์ฐํ ๊ฒฐ์ฌ์ ์งํ๊ณํต(์์ ๊ตฐ์๊ด ์น์ธ)์ ๋ฐ๋ฅธ๋ค.\")\n",
"\n",
"fig, ax = plt.subplots(1, 3, figsize=(15, 4))\n",
"vc.rename(index={\"๋จ์ \": \"cut off\", \"์์ ์ด๊ณผ\": \"over demand\", \"์์ฉ\": \"ok\", \"์ฌ๊ฐ ์ง์ฒด\": \"severe delay\"}).plot.bar(ax=ax[0], color=\"tab:blue\")\n",
"ax[0].set_title(\"120 triple-closure outcomes\"); ax[0].set_xlabel(\"\"); ax[0].tick_params(axis=\"x\", rotation=15)\n",
"ax[1].hist(ok_rows[\"avg_min\"], bins=15); ax[1].axvline(base[\"avg_min\"], c=\"r\"); ax[1].set_title(\"Avg transit (min) of feasible combos\")\n",
"ax[2].hist(lat * 1000, bins=15); ax[2].set_title(\"Refresh latency (ms)\")\n",
"plt.tight_layout(); plt.show()\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"๐ก [์ฐธ๊ณ ] ๋ฏธ์
08 ์์ ์คํ ๊ฒฐ๊ณผ (Output)
\n",
"\n",
"```text\n",
"[0] ํ์: ์์ 150๋/์, ํ๊ท ํต๊ณผ 37.8๋ถ, ์ต๋์ ๋ 290๋/์\n",
"[1] 3๊ฐ ์ถ์ ์ ๊ทผ ๊ตฌ๊ฐ(A3ยทB3ยทC2) ๋์ ๋ด์: ์ต๋์ ๋ 0๋/์, ๊ฒฝ๋ก 8/8๊ฐ ์ฐจ๋จ -> ๋๋ก ์์ก ๋ถ๊ฐ. ๊ณต์คยทํด์ ๋ฑ ํ ์๋จ ๋๋ ๋ฌผ๋ ๋ณด๋ฅ ๊ฒฐ์ ์ด ํ์ํ๋ฉฐ ์ฌ๋์ด ํ๋จํ๋ค\n",
"\n",
"[2] 120๊ฐ ์กฐํฉ ํ๊ฐ(0.12์ด): ์์ ์ด๊ณผ 73, ์์ฉ 44, ๋จ์ 3\n",
" ์์ฉ ๊ฐ๋ฅ ์กฐํฉ์ ํ๊ท ํต๊ณผ์๊ฐ 43.3๋ถ(ํ์ 37.8๋ถ), ์ต์
์์ฉ ์กฐํฉ A1+A2+B3 52.5๋ถ\n",
" ๋จ์ ์กฐํฉ์ ์์ฃผ ํฌํจ๋ ๊ตฌ๊ฐ: C1(2), A3(2), B3(2), A1(1) -> ์ด์คํ(์ฐํ๋ก ํ๋ณด) ์ฐ์ ํ๋ณด\n",
"\n",
"[3] ๋์๋ณด๋ ๋ฐ์ดํฐ ๊ฐฑ์ 60ํ(๊ตฌ๊ฐ 2๊ฐ ํ์ ๋ฌด์์): ํ๊ท 7ms, ์ต๋ 10ms (๋ชฉํ 2์ด ์ด๋ด: ๋ฌ์ฑ; ํ๋ฉด ๋ ๋๋ง ์๊ฐ์ ์ ์ธ)\n",
"[4] ๊ฒฝ๋ณด ์ ํ๋: 97.0% (๋ชฉํ 95% ์ด์: ๋ฌ์ฑ), ์ค์ ์ง์ฒด 5๋ถ ์ด์ 22๊ฑด ์ค ํ์ง 15๊ฑด(์ฌํ์จ 68%), ์ค๊ฒฝ๋ณด 2๊ฑด\n",
" ๊ฒฝ๋ณด ๊ธฐ์ค์ 3๋ถ์ผ๋ก ๋ฎ์ถ๋ฉด: ์ฌํ์จ 91%, ์ค๊ฒฝ๋ณด 46๊ฑด -> ๋์น์ง ์๋ ์ชฝ์ด ์ค์ํ๋ฉด ๊ธฐ์ค์ ๋ฎ์ถ๋ ์ค๊ฒฝ๋ณด ํผ๋ก๋ฅผ ํจ๊ป ๋ณธ๋ค\n",
"๋ชจ๋ ๋ชจ์๋ ๊ต์ก์ฉ ๊ฐ์ ๋
ธ์ ์ด๋ฉฐ, ์ค์ ๋ด์ยท์ฐํ ๊ฒฐ์ฌ์ ์งํ๊ณํต(์์ ๊ตฐ์๊ด ์น์ธ)์ ๋ฐ๋ฅธ๋ค.\n",
"```\n",
" \n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## ๐ฏ ๋ฏธ์
09. AAR ๊ธฐ๋ฐ ๋ณ๋ชฉ ์์ธก ์ค์ฐจ ๋ถ์ ๋ฐ ๋๋ก ํต๊ณผ ์ฉ๋ ํ๋ผ๋ฏธํฐ ์ฌ๋ณด์ \n",
"**์์์๊ฐ**: ์ฝ 3.0์๊ฐ\n",
"\n",
"### ๐ ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ์ํ ์ง์นจ\n",
"#### ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ๋น์์ฑ\n",
"\n",
"AAR ๊ธฐ๋ฐ ๋ณ๋ชฉ ์์ธก ์ค์ฐจ๋ฅผ ๋ถ์ํ๊ณ ํต๊ณผ ์ฉ๋์ ์ฌ๋ณด์ ํ๋ค. ์ค์ ์ ์ ํ์ฅ์์๋ ๋จ์ ํ์ง์ ๊ทธ์น์ง ์๊ณ ์ ์ํ ์ํ ํ๋จ๊ณผ ์ ๋ขฐ๋ ๋์ ์์ฌ๊ฒฐ์ ์ด ํ์์ ์ด๋ฏ๋ก, **AAR ๊ด์ ์์ ๋ณ๋ชฉ ์์ธก ์ค์ฐจ๋ฅผ ์์ธ๋ณ๋ก ๋ถ์ํ๊ณ ๋๋ก ํต๊ณผ ์ฉ๋ ํ๋ผ๋ฏธํฐ๋ฅผ ์ฌ๋ณด์ ํ๋ฉฐ, ํจ๊ณผ๋ฅผ ROI๋ก ํ์ฐํ๋ค.** ๊ณผ์
์ ์์ํ์ฌ ์คํ์ ๋ฐฉ์งํ๊ณ ์์ ๋์๋ ฅ์ ๊ทน๋ํํด์ผ ํฉ๋๋ค.\n",
"\n",
"์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ณธ ๋ฏธ์
์์๋ **๊ธฐ์ค BPR์ ์ค์ฐจ๋ฅผ ์ ๋น์ผ, ๊ธด๊ธ ๋ถ์ถ์ผ, ์ฐ์ฒ์ผ, ์ ์์ผ๋ก ๋๋์ด ํ๊ท ์ค์ฐจ๋ฅผ ๋น๊ต ์ํํ๊ณ ๊ฒ์ฆํ๋** ์ค์ต์ ์ฒด๊ณ์ ์ผ๋ก ์ํํฉ๋๋ค.\n",
"\n",
"ํ์ต์๋ ์ค์ต๊ฐ์ด๋์ ์ ์๋ ์ด 4๊ฐ ์ธ๋ถ ํ๋(๊ธฐ๋ณธ ๊ธฐ๋ฅ ๊ตฌํ โ ๊ฒฐ๊ณผ ์งํ ๊ฒ์ฆ โ ํ๋ผ๋ฏธํฐ ํ๋ ๋ฐ ๋์ ๊ณผ์ )์ ์์ฐจ์ ์ผ๋ก ์งํํ๋ฉฐ ํต์ฌ ํ์ ๊ธฐ์ค์ ๋ฌ์ฑํฉ๋๋ค.\n",
"\n",
"**์ด๋ฒ ๊ฒฐ๊ณผ๋ฌผ:** `reports/mov01_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 ๊ด์ ์์ BPR ๊ธฐ์ค ๋ชจ๋ธ์ ๋ณ๋ชฉ ์์ธก ์ค์ฐจ๋ฅผ ์์ธ๋ณ๋ก ๋ถ์ํ๊ณ , ์ต์์ ๊ณฑ์ผ๋ก ๊ตฌ๊ฐ๋ณ ์ ํจ ํต๊ณผ ์ฉ๋ ๋ฐฐ์จ๊ณผ ์ง์ฒด ํจ์ ํ๋ผ๋ฏธํฐ๋ฅผ ์ฌ๋ณด์ ํด ๊ฒ์ฆ ๊ตฌ๊ฐ ์ค์ฐจ๋ฅผ ์ค์ธ ๋ค, ํจ๊ณผ๋ฅผ ๋น์ฉ ์ ๊ฐ์กยท์๊ฐ ๋จ์ถ๋ฅ ยท๊ฒฝ๋ณด ํฌ์ฐฉ๋ฅ 3๋ ์งํ๋ก ํ์ฐํ๊ณ ๋ฏผ๊ฐ๋๋ฅผ ๋ถ์ํ๋ค.\"\"\"\n",
"import numpy as np\n",
"import pandas as pd\n",
"import matplotlib.pyplot as plt\n",
"from scipy.optimize import least_squares, linprog\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# [๊ต์ก์ฉ๊ฐ์์๋ฃ] ๊ฐ์ ๋
ธ์ ๋งยท๊ตฌ๊ฐ ํต๊ณผ๋ ์์ฑ๊ธฐ (์ค์ ๋๋กยท์ง๋ช
ยท๋ถ๋ ์์)\n",
"# ๊ฐ์ ์ถ๋ฐ์ง(SRC)์์ ๊ฐ์ ๋ชฉ์ ์ง(DST)๊น์ง 3๊ฐ ์ถ์ (AยทBยทC)๊ณผ ์ฐ๊ฒฐ๋ก 2๊ฐ, ๊ตฌ๊ฐ 10๊ฐ. 150์ผ์น ํผํฌ์๊ฐ ํต๊ณผ๋ยท์ง์ฒด ์๊ฐ์ ๋ง๋ ๋ค.\n",
"# ---------------------------------------------------------------------------\n",
"SEGS = pd.DataFrame([\n",
" (\"A1\", \"SRC\", \"J1\", 12, 130, \"์ผ๋ฐ\"), (\"A2\", \"J1\", \"J2\", 8, 70, \"๊ต๋\"), (\"A3\", \"J2\", \"DST\", 10, 140, \"๊ต์ฐจ๋ก\"),\n",
" (\"B1\", \"SRC\", \"J3\", 15, 100, \"์ผ๋ฐ\"), (\"B2\", \"J3\", \"J2\", 9, 80, \"ํฐ๋\"), (\"B3\", \"J3\", \"DST\", 14, 95, \"์ผ๋ฐ\"),\n",
" (\"C1\", \"SRC\", \"J4\", 18, 70, \"์ผ๋ฐ\"), (\"C2\", \"J4\", \"DST\", 20, 75, \"๊ต๋\"),\n",
" (\"X1\", \"J1\", \"J3\", 7, 50, \"๊ต์ฐจ๋ก\"), (\"X2\", \"J4\", \"J3\", 10, 60, \"์ผ๋ฐ\")],\n",
" columns=[\"seg\", \"u\", \"v\", \"km\", \"cap\", \"kind\"]) # cap = ํผํฌ์๊ฐ ํต๊ณผ ์ฉ๋(๋/์, ๊ต์ก์ฉ ๊ฐ์ ๊ฐ)\n",
"KSPD = {\"์ผ๋ฐ\": 55, \"๊ต๋\": 40, \"ํฐ๋\": 45, \"๊ต์ฐจ๋ก\": 45} # ๊ตฌ๊ฐ ์ ํ๋ณ ์์ ์ฃผํ ์๋(km/h, ๊ฐ์ ๊ฐ)\n",
"SEGS[\"t0\"] = SEGS[\"km\"] / SEGS[\"kind\"].map(KSPD) * 60 # ์์ ์ฃผํ ์๊ฐ(๋ถ)\n",
"UTIL = dict(A1=.78, A2=.85, A3=.66, B1=.70, B2=.72, B3=.64, C1=.55, C2=.58, X1=.45, X2=.40) # ํ์ ์ด์ฉ๋ฅ \n",
"\n",
"\n",
"def bpr(t0, v, cap, a=0.15, b=4.0):\n",
" \"\"\"BPR ์ง์ฒด ํจ์(๊ตํต๊ณตํ์ ํ์คํ): ํต๊ณผ๋/์ฉ๋ ๋น๊ฐ ์ปค์ง์๋ก ์ง์ฒด๊ฐ ๊ธ์ฆํ๋ค. (๋ถ)\"\"\"\n",
" return t0 * a * (v / cap) ** b\n",
"\n",
"\n",
"def make_data(seed=11, days=150):\n",
" \"\"\"๊ตฌ๊ฐ x ์ผ์ ํจ๋. v=์ค์ ํผํฌ ํต๊ณผ๋, plan_v=์์ก๊ณํ ์๋(๊ธด๊ธ ๋ถ์ถ ๋ฏธ๋ฐ์), delay=์ค์ ์ง์ฒด(๋ถ).\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" d_idx = np.arange(days)\n",
" lvl = np.array([1.0, 1.08, 1.04, 1.10, 1.02, 0.82, 0.78])[d_idx % 7] * (1 + 0.0012 * d_idx)\n",
" lvl = lvl * np.where((d_idx >= 100) & (d_idx < 110), 1.30, 1.0) # ์์ก๋ ํญ์ฆ ๊ธฐ๊ฐ(๊ฐ์)\n",
" prev, rows = {s: 0.0 for s in SEGS[\"seg\"]}, []\n",
" for d in d_idx:\n",
" rain = bool(rng.random() < 0.18)\n",
" for r in SEGS.itertuples():\n",
" maint, urgent = bool(rng.random() < 0.06), bool(rng.random() < 0.05) # ๋น๊ณํ ์ ๋น / ๊ธด๊ธ ๋ถ์ถ\n",
" v = r.cap * UTIL[r.seg] * lvl[d] * (1.25 if urgent else 1.0) * rng.normal(1, 0.05)\n",
" cap_eff = r.cap * (0.65 if maint else 1.0) * (0.92 if rain else 1.0)\n",
" delay = max(r.t0 * 0.2 * (v / cap_eff) ** 3.5 + 0.25 * prev[r.seg] + rng.normal(0, 0.4), 0.0)\n",
" prev[r.seg] = delay\n",
" rows.append(dict(day=int(d), dow=int(d % 7), seg=r.seg, kind=r.kind, km=r.km, cap=r.cap, t0=r.t0,\n",
" plan_v=v / (1.25 if urgent else 1.0) * rng.normal(1, 0.04), v=v, maint=int(maint), urgent=int(urgent),\n",
" rain=int(rain), cap_eff=cap_eff, delay=delay))\n",
" df = pd.DataFrame(rows)\n",
" df[\"transit\"] = df[\"t0\"] * (1 + 0.08 * df[\"rain\"]) + df[\"delay\"]\n",
" return df\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",
"# ์ง์ฒด ์์ธก์ฉ ํ์ ํผ์ฒ ์์ฑ(๋น์ผ ์ค์ ์์ ์ ์ ์ ์๋ ์ ๋ณด๋ง ์ฌ์ฉ: ๊ณํ ๋ฌผ๋, ๊ณต์ง ์ ๋น, ๊ธฐ์ ์๋ณด, ์ ์ผยท์ ์ฃผ ์ค์ )\n",
"# ---------------------------------------------------------------------------\n",
"FEATS = [\"plan_v\", \"cap\", \"cap_eff\", \"t0\", \"km\", \"maint\", \"rain\", \"util\", \"util_eff\", \"util_eff4\", \"bpr_base\", \"lag1_delay\", \"lag7_delay\",\n",
" \"roll7_v\", \"surge_ratio\", \"is_weekend\", \"dow_sin\", \"dow_cos\", \"k_๊ต๋\", \"k_ํฐ๋\", \"k_๊ต์ฐจ๋ก\"]\n",
"\n",
"\n",
"def add_features(df):\n",
" d = df.sort_values([\"seg\", \"day\"]).copy()\n",
" g = d.groupby(\"seg\")\n",
" d[\"lag1_delay\"] = g[\"delay\"].shift(1)\n",
" d[\"lag7_delay\"] = g[\"delay\"].shift(7)\n",
" d[\"roll7_v\"] = g[\"v\"].transform(lambda s: s.shift(1).rolling(7).mean())\n",
" d[\"util\"] = d[\"plan_v\"] / d[\"cap\"]\n",
" d[\"util_eff\"] = d[\"plan_v\"] / d[\"cap_eff\"]\n",
" d[\"util_eff4\"] = d[\"util_eff\"] ** 4\n",
" d[\"bpr_base\"] = bpr(d[\"t0\"], d[\"plan_v\"], d[\"cap\"])\n",
" d[\"surge_ratio\"] = d[\"plan_v\"] / d[\"roll7_v\"]\n",
" d[\"is_weekend\"] = (d[\"dow\"] >= 5).astype(int)\n",
" d[\"dow_sin\"], d[\"dow_cos\"] = np.sin(2 * np.pi * d[\"dow\"] / 7), np.cos(2 * np.pi * d[\"dow\"] / 7)\n",
" for k in (\"๊ต๋\", \"ํฐ๋\", \"๊ต์ฐจ๋ก\"):\n",
" d[\"k_\" + k] = (d[\"kind\"] == k).astype(int)\n",
" return d.dropna().sort_values([\"day\", \"seg\"]).reset_index(drop=True)\n",
"# ---------------------------------------------------------------------------\n",
"# ---------------------------------------------------------------------------\n",
"# ๋
ธ์ ๋ง ํ๋ฆ ๋๊ตฌ: ์ต๋์ ๋(Edmonds-Karp), ์ฆ๋ถ ๋ฐฐ์ (ํผ์ก ๋ฐ์ ์ต๋จ์๊ฐ ๊ฒฝ๋ก), ๊ฒฝ๋ก ์ด๊ฑฐ\n",
"# ---------------------------------------------------------------------------\n",
"CAP = dict(zip(SEGS[\"seg\"], SEGS[\"cap\"]))\n",
"T0 = dict(zip(SEGS[\"seg\"], SEGS[\"t0\"]))\n",
"KM = dict(zip(SEGS[\"seg\"], SEGS[\"km\"]))\n",
"ARCS = {r.seg: (r.u, r.v) for r in SEGS.itertuples()}\n",
"SRC, DST = \"SRC\", \"DST\"\n",
"\n",
"\n",
"def seg_time(s, flow, cap=None, a=0.2, b=3.5):\n",
" \"\"\"๊ตฌ๊ฐ ํต๊ณผ์๊ฐ(๋ถ) = ์์ ์ฃผํ + ํผ์ก ์ง์ฒด. cap์ ์ฃผ๋ฉด ์ฉ๋์ ๋์ฒด(์ ๋นยท๊ธฐ์ ๋ฐ์).\"\"\"\n",
" r = flow / (cap or CAP[s])\n",
" return T0[s] * (1 + a * r ** b + 6.0 * max(0.0, r - 1.0)) # ์ฉ๋ ์ด๊ณผ๋ถ์ ๋๊ธฐ์ด๋ก ๊ธ์ฆ\n",
"\n",
"\n",
"def max_flow(closed=(), capmul=None):\n",
" \"\"\"์ต๋์ ๋(๋/์)๊ณผ ์ต์ ์ ๋จ ๊ตฌ๊ฐ(๋ณ๋ชฉ). capmul = {๊ตฌ๊ฐ: ์ฉ๋ ๋ฐฐ์จ}.\"\"\"\n",
" cap = {s: (0 if s in closed else CAP[s] * (capmul or {}).get(s, 1.0)) for s in CAP}\n",
" res = {}\n",
" for s, (u, v) in ARCS.items():\n",
" res[(u, v)] = res.get((u, v), 0) + cap[s]\n",
" res.setdefault((v, u), 0)\n",
" flow = 0.0\n",
" while True:\n",
" prev, q = {SRC: None}, [SRC]\n",
" for u in q:\n",
" for (a, b), c in res.items():\n",
" if a == u and c > 1e-9 and b not in prev:\n",
" prev[b] = u; q.append(b)\n",
" if DST not in prev:\n",
" break\n",
" path, x = [], DST\n",
" while prev[x]:\n",
" path.append((prev[x], x)); x = prev[x]\n",
" f = min(res[e] for e in path)\n",
" for a, b in path:\n",
" res[(a, b)] -= f; res[(b, a)] += f\n",
" flow += f\n",
" reach = set(prev)\n",
" cut = [s for s, (u, v) in ARCS.items() if u in reach and v not in reach and cap[s] > 0]\n",
" return flow, cut\n",
"\n",
"\n",
"def all_paths():\n",
" \"\"\"SRC -> DST ๋จ์ ๊ฒฝ๋ก(๊ตฌ๊ฐ ์ด๋ฆ ํํ) ์ ์ฒด.\"\"\"\n",
" out = []\n",
"\n",
" def dfs(node, segs, seen):\n",
" if node == DST:\n",
" out.append(tuple(segs)); return\n",
" for s, (u, v) in ARCS.items():\n",
" if u == node and v not in seen:\n",
" dfs(v, segs + [s], seen | {v})\n",
" dfs(SRC, [], {SRC})\n",
" return out\n",
"\n",
"\n",
"PATHS = all_paths()\n",
"\n",
"\n",
"def assign(demand, closed=(), capmul=None, steps=40):\n",
" \"\"\"์ฆ๋ถ ๋ฐฐ์ : ์์๋ฅผ steps์กฐ๊ฐ์ผ๋ก ๋๋ , ์ ์ฒด ํต๊ณผ์๊ฐ(๋์ x ์๊ฐ) ์ฆ๊ฐ๊ฐ ๊ฐ์ฅ ์์ ๊ฒฝ๋ก์ ์ฐจ๋ก๋ก ์น๋๋ค(์์คํ
์ต์ ๊ทผ์ฌ).\"\"\"\n",
" cap = {s: CAP[s] * (capmul or {}).get(s, 1.0) for s in CAP}\n",
" flow, pf = {s: 0.0 for s in CAP}, {}\n",
" ok = [p for p in PATHS if not any(s in closed for s in p)]\n",
" if not ok:\n",
" return None\n",
" for _ in range(steps):\n",
" chunk = demand / steps\n",
" best = min(ok, key=lambda p: sum((flow[s] + chunk) * seg_time(s, flow[s] + chunk, cap[s]) - flow[s] * seg_time(s, flow[s], cap[s]) for s in p))\n",
" for s in best:\n",
" flow[s] += chunk\n",
" pf[best] = pf.get(best, 0) + chunk\n",
" tt = {p: sum(seg_time(s, flow[s], cap[s]) for s in p) for p in pf}\n",
" avg = sum(pf[p] * tt[p] for p in pf) / demand\n",
" return {\"flow\": flow, \"paths\": pf, \"path_min\": tt, \"avg_min\": avg, \"util\": {s: flow[s] / cap[s] for s in cap if cap[s] > 0}}\n",
"\n",
"\n",
"FIXED_SHARE = {(\"A1\", \"A2\", \"A3\"): 0.32, (\"B1\", \"B3\"): 0.40, (\"C1\", \"C2\"): 0.28} # ๊ณผ๊ฑฐ ๊ดํ: ์ถ์ ๋ณ ๊ณ ์ ๋ฐฐ๋ถ(๊ฐ์)\n",
"\n",
"\n",
"def fixed_assign(demand, closed=(), capmul=None):\n",
" \"\"\"๊ณผ๊ฑฐ ๊ดํ ๋ฐฐ๋ถ: ์ถ์ ๋ณ ๊ณ ์ ๋น์จ. ๋งํ ์ถ์ ๋ฌผ๋์ ์ด๋ ค ์๋ ์ถ์ ์ ๋น์จ๋๋ก ์ฌ๋ถ๋ฐฐ.\"\"\"\n",
" cap = {s: CAP[s] * (capmul or {}).get(s, 1.0) for s in CAP}\n",
" live = {p: w for p, w in FIXED_SHARE.items() if not any(s in closed for s in p)}\n",
" if not live:\n",
" return None\n",
" tot = sum(live.values())\n",
" flow = {s: 0.0 for s in CAP}\n",
" pf = {p: demand * w / tot for p, w in live.items()}\n",
" for p, f in pf.items():\n",
" for s in p:\n",
" flow[s] += f\n",
" tt = {p: sum(seg_time(s, flow[s], cap[s]) for s in p) for p in pf}\n",
" return {\"flow\": flow, \"paths\": pf, \"path_min\": tt, \"avg_min\": sum(pf[p] * tt[p] for p in pf) / demand,\n",
" \"util\": {s: flow[s] / cap[s] for s in cap if cap[s] > 0}}\n",
"\n",
"\n",
"def lp_assign(demand, closed=(), capmul=None):\n",
" \"\"\"์ ํ๊ณํ(LP): ๊ตฌ๊ฐ ์ฉ๋ ์ํ(์ด์ฉ๋ฅ rho) ์๋์์ ์์ ์ฃผํ์๊ฐ ํฉ์ ์ต์ํํ๋ ๊ฒฝ๋ก๋ณ ๋ฌผ๋. ๋ถ๊ฐ๋ฅํ๋ฉด rho๋ฅผ 1.0๊น์ง ์ํ.\"\"\"\n",
" cap = {s: CAP[s] * (capmul or {}).get(s, 1.0) for s in CAP}\n",
" ok = [p for p in PATHS if not any(s in closed for s in p)]\n",
" if not ok:\n",
" return None\n",
" for rho in (0.85, 0.95, 1.0):\n",
" r = linprog([sum(T0[s] for s in p) for p in ok], A_ub=[[1.0 if s in p else 0.0 for p in ok] for s in CAP],\n",
" b_ub=[rho * cap[s] for s in CAP], A_eq=[[1.0] * len(ok)], b_eq=[demand], bounds=(0, None), method=\"highs\")\n",
" if r.status == 0:\n",
" break\n",
" else:\n",
" return assign(demand, closed, capmul) # ์ฉ๋ ์ด๊ณผ ์์: ์ฆ๋ถ ๋ฐฐ์ ์ผ๋ก ๋์ฒด(๊ณผ๋ถํ ํ์)\n",
" flow = {s: sum(r.x[i] for i, p in enumerate(ok) if s in p) for s in CAP}\n",
" pf = {p: float(r.x[i]) for i, p in enumerate(ok) if r.x[i] > 1e-6}\n",
" tt = {p: sum(seg_time(s, flow[s], cap[s]) for s in p) for p in pf}\n",
" return {\"flow\": flow, \"paths\": pf, \"path_min\": tt, \"avg_min\": sum(pf[p] * tt[p] for p in pf) / demand,\n",
" \"util\": {s: flow[s] / cap[s] for s in cap if cap[s] > 0}, \"rho\": rho}\n",
"\n",
"\n",
"def cost_of(res, tv=1000, fuel_km=450):\n",
" \"\"\"์๊ฐ๋น ๋น์ฉ(์, ๊ต์ก์ฉ ๊ฐ์ ): ๋๋น ์๊ฐ๋น์ฉ tv์/๋ถ + ์ ๋ฅ๋น fuel_km์/km.\"\"\"\n",
" return sum(f * (res[\"path_min\"][p] * tv + sum(KM[s] for s in p) * fuel_km) for p, f in res[\"paths\"].items())\n",
"# ---------------------------------------------------------------------------\n",
"\n",
"df = make_data(); d = add_features(df)\n",
"SEGL = list(SEGS[\"seg\"])\n",
"cal, val = d[d.day < 135], d[d.day >= 135] # AAR ๋์(๋ณด์ ) ๊ตฌ๊ฐ / ๊ฒ์ฆ ๊ตฌ๊ฐ\n",
"\n",
"\n",
"def model(p, x):\n",
" \"\"\"๋ณด์ ๋ชจ๋ธ: a, b, c(์ ์ผ ์ง์ฒด ์ด์), ๊ตฌ๊ฐ๋ณ ์ฉ๋ ๋ฐฐ์จ m_s, ์ ๋น ์ ์ฉ๋ ๋ฐฐ์จ, ์ฐ์ฒ ์ ์ฉ๋ ๋ฐฐ์จ.\"\"\"\n",
" a, b, c, mm, rr = p[0], p[1], p[2], p[3], p[4]\n",
" m = pd.Series(p[5:], index=SEGL)[x[\"seg\"]].values\n",
" eff = x[\"cap\"].values * m * np.where(x[\"maint\"] == 1, mm, 1.0) * np.where(x[\"rain\"] == 1, rr, 1.0)\n",
" return x[\"t0\"].values * a * (x[\"plan_v\"].values / eff) ** b + c * x[\"lag1_delay\"].values\n",
"\n",
"\n",
"p0 = np.r_[0.15, 4.0, 0.0, 1.0, 1.0, np.ones(len(SEGL))]\n",
"lo = np.r_[0.01, 1.0, 0.0, 0.3, 0.5, np.full(len(SEGL), 0.5)]\n",
"hi = np.r_[1.0, 6.0, 0.8, 1.2, 1.2, np.full(len(SEGL), 1.5)]\n",
"fit = least_squares(lambda p: model(p, cal) - cal[\"delay\"].values, p0, bounds=(lo, hi))\n",
"P = fit.x\n",
"\n",
"# ---------------- 1) ์ค์ฐจ ๋ถ์(AAR) ----------------\n",
"base_cal, base_val = cal[\"bpr_base\"].values, val[\"bpr_base\"].values\n",
"new_val = model(P, val)\n",
"print(f\"[1] ๊ฒ์ฆ ๊ตฌ๊ฐ(day 135~149, {len(val)}ํ) ์ง์ฒด MAE: ๊ธฐ์ค {mae(val.delay, base_val):.2f}๋ถ -> ์ฌ๋ณด์ {mae(val.delay, new_val):.2f}๋ถ ({1 - mae(val.delay, new_val) / mae(val.delay, base_val):.0%} ๊ฐ์ )\")\n",
"print(f\" ํต๊ณผ์๊ฐ MAPE: ๊ธฐ์ค {mape(val.transit, val.t0 + base_val):.2f}% -> ์ฌ๋ณด์ {mape(val.transit, val.t0 * (1 + .08 * val.rain) + new_val):.2f}%\")\n",
"err = pd.DataFrame({\"์์ธ\": [\"๋น๊ณํ ์ ๋น์ผ\", \"๊ธด๊ธ ๋ถ์ถ์ผ\", \"์ฐ์ฒ์ผ\", \"์ ์์ผ\"],\n",
" \"๊ฑด์\": [int((cal.maint == 1).sum()), int((cal.urgent == 1).sum()), int((cal.rain == 1).sum()), int(((cal.maint + cal.urgent + cal.rain) == 0).sum())],\n",
" \"๊ธฐ์ค ํ๊ท ์ค์ฐจ\": [(base_cal - cal.delay)[m].mean() for m in (cal.maint == 1, cal.urgent == 1, cal.rain == 1, (cal.maint + cal.urgent + cal.rain) == 0)],\n",
" \"์ฌ๋ณด์ ํ๊ท ์ค์ฐจ\": [(model(P, cal) - cal.delay)[m].mean() for m in (cal.maint == 1, cal.urgent == 1, cal.rain == 1, (cal.maint + cal.urgent + cal.rain) == 0)]}).round(2)\n",
"print(\"[2] AAR ์์ธ๋ณ ํ๊ท ์ค์ฐจ(์์ธก-์ค์ , ๋ถ; ์์ = ๊ณผ์์์ธก)\")\n",
"print(err.to_string(index=False))\n",
"pm = pd.DataFrame({\"ํ๋ผ๋ฏธํฐ\": [\"a(์ง์ฒด ๊ณ์)\", \"b(๋น์ ํ ์ง์)\", \"c(์ ์ผ ์ด์)\", \"์ ๋น ์ ์ฉ๋ ๋ฐฐ์จ\", \"์ฐ์ฒ ์ ์ฉ๋ ๋ฐฐ์จ\"], \"๊ณต์นญ/๊ธฐ์ค\": [0.15, 4.0, 0.0, 1.0, 1.0], \"์ฌ๋ณด์ \": P[:5]}).round(3)\n",
"print(\"[3] ์ฌ๋ณด์ ํ๋ผ๋ฏธํฐ:\", \"; \".join(f\"{r['ํ๋ผ๋ฏธํฐ']} {r['๊ณต์นญ/๊ธฐ์ค']} -> {r['์ฌ๋ณด์ ']}\" for _, r in pm.iterrows()))\n",
"ms = pd.Series(P[5:], index=SEGL).round(2)\n",
"ms_txt = {k: float(v) for k, v in ms.items()}\n",
"print(\" ๊ตฌ๊ฐ๋ณ ์ ํจ ์ฉ๋ ๋ฐฐ์จ:\", ms_txt, \"(1.0 = ๊ณต์นญ ์ฉ๋๊ณผ ์ผ์น)\")\n",
"\n",
"# ---------------- 2) ํจ๊ณผ ํ์ฐ: ๊ฒ์ฆ 15์ผ ๋ฐฑํ
์คํธ ----------------\n",
"TV = 300 # ๋๋น ๋ถ๋น ์๊ฐ๊ฐ์น(์, ๊ฐ์ )\n",
"hours_per_day, APPLY = 2, 0.4 # ์ผ ํผํฌ 2์๊ฐ, ์น์ธยทํ์ฅ ์ ์ฝ์ผ๋ก ์ค์ ์ ์ฉ๋๋ ๋น์จ 40%(๊ฐ์ )\n",
"rec = []\n",
"for day, g in df[df.day >= 135].groupby(\"day\"):\n",
" D = float(g[g.seg.isin([\"A1\", \"B1\", \"C1\"])][\"plan_v\"].sum())\n",
" cm = {r.seg: r.cap_eff / r.cap for r in g.itertuples() if r.cap_eff < r.cap}\n",
" f, l = fixed_assign(D, (), cm), lp_assign(D, (), cm)\n",
" pr_b, pr_n = val[val.day == day], val[val.day == day]\n",
" rec.append((day, cost_of(f, tv=TV), cost_of(l, tv=TV), f[\"avg_min\"], l[\"avg_min\"], D, float(g.delay.max()),\n",
" float((pr_b.bpr_base).max()), float(model(P, pr_n).max())))\n",
"bt = pd.DataFrame(rec, columns=[\"day\", \"c_fix\", \"c_lp\", \"t_fix\", \"t_lp\", \"D\", \"max_delay\", \"pred_base\", \"pred_new\"])\n",
"saving_day = (bt[\"c_fix\"] - bt[\"c_lp\"]) * hours_per_day * APPLY\n",
"ann = saving_day.mean() * 365\n",
"time_cut = 1 - bt[\"t_lp\"].sum() / bt[\"t_fix\"].sum()\n",
"veh_min = float(((bt[\"t_fix\"] - bt[\"t_lp\"]) * bt[\"D\"] * hours_per_day * APPLY).sum())\n",
"alert_true = bt[\"max_delay\"] >= 5\n",
"cap_b = float(((bt[\"pred_base\"] >= 5) & alert_true).sum() / max(alert_true.sum(), 1)); cap_n = float(((bt[\"pred_new\"] >= 5) & alert_true).sum() / max(alert_true.sum(), 1))\n",
"fa_b, fa_n = int(((bt[\"pred_base\"] >= 5) & ~alert_true).sum()), int(((bt[\"pred_new\"] >= 5) & ~alert_true).sum())\n",
"print(f\"\\n[4] 3๋ ์ ๋ ํจ๊ณผ ์งํ(๊ฒ์ฆ 15์ผ, ๊ณ ์ ๋ฐฐ๋ถ ๋๋น LP ์ฐํ ์ต์ ํ, ๊ฐ์ ๋จ๊ฐ ์ฌ์ฉ)\")\n",
"print(f\" (1) ๋น์ฉ ์ ๊ฐ์ก: ์ผ ํ๊ท {saving_day.mean() / 1e4:,.0f}๋ง์, ์ฐ ํ์ฐ {ann / 1e8:,.1f}์ต์(๊ฐ์ : ์ผ ํผํฌ 2์๊ฐ, ์ ์ฉ๋ฅ 40%, 365์ผ)\")\n",
"print(f\" (2) ์๊ฐ ๋จ์ถ๋ฅ : ํ๊ท ํต๊ณผ์๊ฐ {bt['t_fix'].mean():.1f}๋ถ -> {bt['t_lp'].mean():.1f}๋ถ ({time_cut:.1%}), ์ ๊ฐ ์ฐจ๋ยท์๊ฐ {veh_min / 60:,.0f}๋ยท์๊ฐ\")\n",
"print(f\" (3) ์ง์ฒด ๊ฒฝ๋ณด ํฌ์ฐฉ๋ฅ (์ค์ ์ง์ฒด 5๋ถ ์ด์ {int(alert_true.sum())}์ผ ๋๋น): ๊ธฐ์ค {cap_b:.0%} -> ์ฌ๋ณด์ {cap_n:.0%} (์ค๊ฒฝ๋ณด {fa_b} -> {fa_n}์ผ)\")\n",
"\n",
"# ---------------- 3) ROI์ ๋ฏผ๊ฐ๋(๊ฐ์ ๋์
ยท์ด์๋น) ----------------\n",
"COST = 2.0e8 # ๋์
ยท์ด์ ์ฐ ๋น์ฉ 2์ต์(๊ต์ก์ฉ ๊ฐ์ )\n",
"roi = (ann - COST) / COST\n",
"print(f\"\\n[5] ROI = (์ฐ ์ ๊ฐ - ๋น์ฉ) / ๋น์ฉ = {roi:.1%} (์ฐ ๋น์ฉ {COST / 1e8:.1f}์ต์ ๊ฐ์ ), ํ์ ๊ธฐ๊ฐ {COST / ann * 12:.1f}๊ฐ์\")\n",
"sens = pd.DataFrame({\"์๊ฐ๊ฐ์น -30%\": [ann * 0.7], \"๊ธฐ์ค\": [ann], \"์๊ฐ๊ฐ์น +30%\": [ann * 1.3]}).T[0]\n",
"low_eff = ann * 0.5\n",
"print(\" ๋ฏผ๊ฐ๋(์ฐ ์ ๊ฐ, ์ต์):\", {k: round(float(v) / 1e8, 1) for k, v in sens.items()}, f\"| ํจ๊ณผ๊ฐ ์ ๋ฐ์ด๋ฉด ROI {(low_eff - COST) / COST:.1%}\")\n",
"print(\" ๋จ๊ณ๋ณ ๋ก๋๋งต: ๋จ๊ธฐ(1~3๊ฐ์) ๊ฒฝ๋ณด ๊ธฐ์ค ์ ์ฐฉ -> ์ค๊ธฐ(6๊ฐ์) ๊ตฌ๊ฐ๋ณ ์ฉ๋ ๋ฐฐ์จ ์ ๋จ์ ์ฌ๋ณด์ -> ์ฅ๊ธฐ(1๋
) ์์ ์ ๋ณด์ฒด๊ณ ์ฐ๋ ๊ฒํ \")\n",
"print(\" ์ถ์ ์ฐ์์ ๊ฐ์ ๋จ๊ฐ์ ํฌ๊ฒ ์์กดํ๋ฏ๋ก ์ค์ ์ ์ฉ ๋๋ ๊ธฐํยท์ฌ์ ๋ถ์๊ฐ ๋จ๊ฐ๋ฅผ ์ ์ํด์ผ ํ๋ฉฐ ๋ณธ ๊ฐ์ ํ์ต์ฉ์ด๋ค.\")\n",
"\n",
"fig, ax = plt.subplots(1, 3, figsize=(15, 4))\n",
"ax[0].scatter(val[\"delay\"], base_val, s=8, label=\"baseline\"); ax[0].scatter(val[\"delay\"], new_val, s=8, label=\"recalibrated\"); ax[0].plot([0, 15], [0, 15], \"k--\"); ax[0].legend(); ax[0].set_title(\"Actual vs predicted delay (min)\")\n",
"ax[1].bar(ms.index, ms.values); ax[1].axhline(1, c=\"k\", ls=\"--\"); ax[1].set_title(\"Effective capacity multiplier by segment\")\n",
"ax[2].bar([\"-30%\", \"base\", \"+30%\"], [v / 1e8 for v in sens.values]); ax[2].set_title(\"Annual saving sensitivity (100M won)\")\n",
"plt.tight_layout(); plt.show()\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"๐ก [์ฐธ๊ณ ] ๋ฏธ์
09 ์์ ์คํ ๊ฒฐ๊ณผ (Output)
\n",
"\n",
"```text\n",
"[1] ๊ฒ์ฆ ๊ตฌ๊ฐ(day 135~149, 150ํ) ์ง์ฒด MAE: ๊ธฐ์ค 1.69๋ถ -> ์ฌ๋ณด์ 0.54๋ถ (68% ๊ฐ์ )\n",
" ํต๊ณผ์๊ฐ MAPE: ๊ธฐ์ค 10.02% -> ์ฌ๋ณด์ 3.05%\n",
"[2] AAR ์์ธ๋ณ ํ๊ท ์ค์ฐจ(์์ธก-์ค์ , ๋ถ; ์์ = ๊ณผ์์์ธก)\n",
" ์์ธ ๊ฑด์ ๊ธฐ์ค ํ๊ท ์ค์ฐจ ์ฌ๋ณด์ ํ๊ท ์ค์ฐจ\n",
"๋น๊ณํ ์ ๋น์ผ 78 -4.73 -0.03\n",
" ๊ธด๊ธ ๋ถ์ถ์ผ 69 -2.40 -1.33\n",
" ์ฐ์ฒ์ผ 160 -1.55 0.01\n",
" ์ ์์ผ 994 -0.81 0.09\n",
"[3] ์ฌ๋ณด์ ํ๋ผ๋ฏธํฐ: a(์ง์ฒด ๊ณ์) 0.15 -> 0.197; b(๋น์ ํ ์ง์) 4.0 -> 3.091; c(์ ์ผ ์ด์) 0.0 -> 0.258; ์ ๋น ์ ์ฉ๋ ๋ฐฐ์จ 1.0 -> 0.623; ์ฐ์ฒ ์ ์ฉ๋ ๋ฐฐ์จ 1.0 -> 0.922\n",
" ๊ตฌ๊ฐ๋ณ ์ ํจ ์ฉ๋ ๋ฐฐ์จ: {'A1': 0.98, 'A2': 0.98, 'A3': 1.04, 'B1': 1.01, 'B2': 0.98, 'B3': 1.04, 'C1': 1.0, 'C2': 1.0, 'X1': 0.97, 'X2': 1.03} (1.0 = ๊ณต์นญ ์ฉ๋๊ณผ ์ผ์น)\n",
"\n",
"[4] 3๋ ์ ๋ ํจ๊ณผ ์งํ(๊ฒ์ฆ 15์ผ, ๊ณ ์ ๋ฐฐ๋ถ ๋๋น LP ์ฐํ ์ต์ ํ, ๊ฐ์ ๋จ๊ฐ ์ฌ์ฉ)\n",
" (1) ๋น์ฉ ์ ๊ฐ์ก: ์ผ ํ๊ท 90๋ง์, ์ฐ ํ์ฐ 3.3์ต์(๊ฐ์ : ์ผ ํผํฌ 2์๊ฐ, ์ ์ฉ๋ฅ 40%, 365์ผ)\n",
" (2) ์๊ฐ ๋จ์ถ๋ฅ : ํ๊ท ํต๊ณผ์๊ฐ 63.3๋ถ -> 47.7๋ถ (24.6%), ์ ๊ฐ ์ฐจ๋ยท์๊ฐ 816๋ยท์๊ฐ\n",
" (3) ์ง์ฒด ๊ฒฝ๋ณด ํฌ์ฐฉ๋ฅ (์ค์ ์ง์ฒด 5๋ถ ์ด์ 7์ผ ๋๋น): ๊ธฐ์ค 0% -> ์ฌ๋ณด์ 86% (์ค๊ฒฝ๋ณด 0 -> 0์ผ)\n",
"\n",
"[5] ROI = (์ฐ ์ ๊ฐ - ๋น์ฉ) / ๋น์ฉ = 64.9% (์ฐ ๋น์ฉ 2.0์ต์ ๊ฐ์ ), ํ์ ๊ธฐ๊ฐ 7.3๊ฐ์\n",
" ๋ฏผ๊ฐ๋(์ฐ ์ ๊ฐ, ์ต์): {'์๊ฐ๊ฐ์น -30%': 2.3, '๊ธฐ์ค': 3.3, '์๊ฐ๊ฐ์น +30%': 4.3} | ํจ๊ณผ๊ฐ ์ ๋ฐ์ด๋ฉด ROI -17.5%\n",
" ๋จ๊ณ๋ณ ๋ก๋๋งต: ๋จ๊ธฐ(1~3๊ฐ์) ๊ฒฝ๋ณด ๊ธฐ์ค ์ ์ฐฉ -> ์ค๊ธฐ(6๊ฐ์) ๊ตฌ๊ฐ๋ณ ์ฉ๋ ๋ฐฐ์จ ์ ๋จ์ ์ฌ๋ณด์ -> ์ฅ๊ธฐ(1๋
) ์์ ์ ๋ณด์ฒด๊ณ ์ฐ๋ ๊ฒํ \n",
" ์ถ์ ์ฐ์์ ๊ฐ์ ๋จ๊ฐ์ ํฌ๊ฒ ์์กดํ๋ฏ๋ก ์ค์ ์ ์ฉ ๋๋ ๊ธฐํยท์ฌ์ ๋ถ์๊ฐ ๋จ๊ฐ๋ฅผ ์ ์ํด์ผ ํ๋ฉฐ ๋ณธ ๊ฐ์ ํ์ต์ฉ์ด๋ค.\n",
"```\n",
" \n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## ๐ฏ ๋ฏธ์
10. ์ ์ ์์ก ๋ณ๋ชฉ ๊ตฌ๊ฐ ์๋ณ ๋ฐ ์ฐํ ์์ก ์๋ฎฌ๋ ์ด์
๋๊ตฌ ํตํฉ ํจํค์ง ์์ฑ\n",
"**์์์๊ฐ**: ์ฝ 3.5์๊ฐ\n",
"\n",
"### ๐ ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ์ํ ์ง์นจ\n",
"#### ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ๋น์์ฑ\n",
"\n",
"์ ์ ์์ก ๋ณ๋ชฉ ๊ตฌ๊ฐ ์๋ณ ๋ฐ ์ฐํ ์์ก ์๋ฎฌ๋ ์ด์
๋๊ตฌ๋ฅผ ์์ฑํ๋ค. ์ค์ ์ ์ ํ์ฅ์์๋ ๋จ์ ํ์ง์ ๊ทธ์น์ง ์๊ณ ์ ์ํ ์ํ ํ๋จ๊ณผ ์ ๋ขฐ๋ ๋์ ์์ฌ๊ฒฐ์ ์ด ํ์์ ์ด๋ฏ๋ก, **๋ฐ์ดํฐ ๊ฐฑ์ ๋ถํฐ ์ ํฉ์ฑ ๊ฒ์ฆ, ๋๋ฆฌํํธ ์ ๊ฒ, ์๋ ์ฌํ์ต, 7์ผ ์์ ์์ธก, ์ฐํ ๋ฐฐ๋ถ, ์น์ธ๋๊ธฐ ๋ถ๋ฆฌ๊น์ง ํ ๋ฒ์ ์คํ๋๋ ํตํฉ ํ์ดํ๋ผ์ธ์ ์์ฑํ๋ค.** ๊ณผ์
์ ์์ํ์ฌ ์คํ์ ๋ฐฉ์งํ๊ณ ์์ ๋์๋ ฅ์ ๊ทน๋ํํด์ผ ํฉ๋๋ค.\n",
"\n",
"์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ณธ ๋ฏธ์
์์๋ **์ ๊ท 30์ผ ๋ฐ์ดํฐ๋ฅผ ์ถ๊ฐํด ์ ํฉ์ฑ์ ๊ฒ์ฆํ๊ณ , ๊ธฐ์กด ๋ชจ๋ธ์ ์ค์ฐจ๋ก ๋๋ฆฌํํธ๋ฅผ ํ๋จํ ๋ค ์ฑ๋ฆฐ์ ๋ชจ๋ธ์ ํ์ตํด ๊ฐ์ ์ด 3% ์ด์์ผ ๋๋ง ์น๊ฒฉ ์ํํ๊ณ ๊ฒ์ฆํ๋** ์ค์ต์ ์ฒด๊ณ์ ์ผ๋ก ์ํํฉ๋๋ค.\n",
"\n",
"ํ์ต์๋ ์ค์ต๊ฐ์ด๋์ ์ ์๋ ์ด 4๊ฐ ์ธ๋ถ ํ๋(๊ธฐ๋ณธ ๊ธฐ๋ฅ ๊ตฌํ โ ๊ฒฐ๊ณผ ์งํ ๊ฒ์ฆ โ ํ๋ผ๋ฏธํฐ ํ๋ ๋ฐ ๋์ ๊ณผ์ )์ ์์ฐจ์ ์ผ๋ก ์งํํ๋ฉฐ ํต์ฌ ํ์ ๊ธฐ์ค์ ๋ฌ์ฑํฉ๋๋ค.\n",
"\n",
"**์ด๋ฒ ๊ฒฐ๊ณผ๋ฌผ:** `docs/mov01_operations_guide.md ๋ฐ pipeline/mov01_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": [
"\"\"\"๋ฐ์ดํฐ ๊ฐฑ์ , ์ ํฉ์ฑ ๊ฒ์ฆ, ๋๋ฆฌํํธ ์ ๊ฒ, ์๋ ์ฌํ์ต(์ฑํผ์ธ/์ฑ๋ฆฐ์ ), 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 scipy.optimize import linprog\n",
"from sklearn.ensemble import GradientBoostingRegressor\n",
"from sklearn.linear_model import Ridge\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# [๊ต์ก์ฉ๊ฐ์์๋ฃ] ๊ฐ์ ๋
ธ์ ๋งยท๊ตฌ๊ฐ ํต๊ณผ๋ ์์ฑ๊ธฐ (์ค์ ๋๋กยท์ง๋ช
ยท๋ถ๋ ์์)\n",
"# ๊ฐ์ ์ถ๋ฐ์ง(SRC)์์ ๊ฐ์ ๋ชฉ์ ์ง(DST)๊น์ง 3๊ฐ ์ถ์ (AยทBยทC)๊ณผ ์ฐ๊ฒฐ๋ก 2๊ฐ, ๊ตฌ๊ฐ 10๊ฐ. 150์ผ์น ํผํฌ์๊ฐ ํต๊ณผ๋ยท์ง์ฒด ์๊ฐ์ ๋ง๋ ๋ค.\n",
"# ---------------------------------------------------------------------------\n",
"SEGS = pd.DataFrame([\n",
" (\"A1\", \"SRC\", \"J1\", 12, 130, \"์ผ๋ฐ\"), (\"A2\", \"J1\", \"J2\", 8, 70, \"๊ต๋\"), (\"A3\", \"J2\", \"DST\", 10, 140, \"๊ต์ฐจ๋ก\"),\n",
" (\"B1\", \"SRC\", \"J3\", 15, 100, \"์ผ๋ฐ\"), (\"B2\", \"J3\", \"J2\", 9, 80, \"ํฐ๋\"), (\"B3\", \"J3\", \"DST\", 14, 95, \"์ผ๋ฐ\"),\n",
" (\"C1\", \"SRC\", \"J4\", 18, 70, \"์ผ๋ฐ\"), (\"C2\", \"J4\", \"DST\", 20, 75, \"๊ต๋\"),\n",
" (\"X1\", \"J1\", \"J3\", 7, 50, \"๊ต์ฐจ๋ก\"), (\"X2\", \"J4\", \"J3\", 10, 60, \"์ผ๋ฐ\")],\n",
" columns=[\"seg\", \"u\", \"v\", \"km\", \"cap\", \"kind\"]) # cap = ํผํฌ์๊ฐ ํต๊ณผ ์ฉ๋(๋/์, ๊ต์ก์ฉ ๊ฐ์ ๊ฐ)\n",
"KSPD = {\"์ผ๋ฐ\": 55, \"๊ต๋\": 40, \"ํฐ๋\": 45, \"๊ต์ฐจ๋ก\": 45} # ๊ตฌ๊ฐ ์ ํ๋ณ ์์ ์ฃผํ ์๋(km/h, ๊ฐ์ ๊ฐ)\n",
"SEGS[\"t0\"] = SEGS[\"km\"] / SEGS[\"kind\"].map(KSPD) * 60 # ์์ ์ฃผํ ์๊ฐ(๋ถ)\n",
"UTIL = dict(A1=.78, A2=.85, A3=.66, B1=.70, B2=.72, B3=.64, C1=.55, C2=.58, X1=.45, X2=.40) # ํ์ ์ด์ฉ๋ฅ \n",
"\n",
"\n",
"def bpr(t0, v, cap, a=0.15, b=4.0):\n",
" \"\"\"BPR ์ง์ฒด ํจ์(๊ตํต๊ณตํ์ ํ์คํ): ํต๊ณผ๋/์ฉ๋ ๋น๊ฐ ์ปค์ง์๋ก ์ง์ฒด๊ฐ ๊ธ์ฆํ๋ค. (๋ถ)\"\"\"\n",
" return t0 * a * (v / cap) ** b\n",
"\n",
"\n",
"def make_data(seed=11, days=150):\n",
" \"\"\"๊ตฌ๊ฐ x ์ผ์ ํจ๋. v=์ค์ ํผํฌ ํต๊ณผ๋, plan_v=์์ก๊ณํ ์๋(๊ธด๊ธ ๋ถ์ถ ๋ฏธ๋ฐ์), delay=์ค์ ์ง์ฒด(๋ถ).\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" d_idx = np.arange(days)\n",
" lvl = np.array([1.0, 1.08, 1.04, 1.10, 1.02, 0.82, 0.78])[d_idx % 7] * (1 + 0.0012 * d_idx)\n",
" lvl = lvl * np.where((d_idx >= 100) & (d_idx < 110), 1.30, 1.0) # ์์ก๋ ํญ์ฆ ๊ธฐ๊ฐ(๊ฐ์)\n",
" prev, rows = {s: 0.0 for s in SEGS[\"seg\"]}, []\n",
" for d in d_idx:\n",
" rain = bool(rng.random() < 0.18)\n",
" for r in SEGS.itertuples():\n",
" maint, urgent = bool(rng.random() < 0.06), bool(rng.random() < 0.05) # ๋น๊ณํ ์ ๋น / ๊ธด๊ธ ๋ถ์ถ\n",
" v = r.cap * UTIL[r.seg] * lvl[d] * (1.25 if urgent else 1.0) * rng.normal(1, 0.05)\n",
" cap_eff = r.cap * (0.65 if maint else 1.0) * (0.92 if rain else 1.0)\n",
" delay = max(r.t0 * 0.2 * (v / cap_eff) ** 3.5 + 0.25 * prev[r.seg] + rng.normal(0, 0.4), 0.0)\n",
" prev[r.seg] = delay\n",
" rows.append(dict(day=int(d), dow=int(d % 7), seg=r.seg, kind=r.kind, km=r.km, cap=r.cap, t0=r.t0,\n",
" plan_v=v / (1.25 if urgent else 1.0) * rng.normal(1, 0.04), v=v, maint=int(maint), urgent=int(urgent),\n",
" rain=int(rain), cap_eff=cap_eff, delay=delay))\n",
" df = pd.DataFrame(rows)\n",
" df[\"transit\"] = df[\"t0\"] * (1 + 0.08 * df[\"rain\"]) + df[\"delay\"]\n",
" return df\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",
"# ์ง์ฒด ์์ธก์ฉ ํ์ ํผ์ฒ ์์ฑ(๋น์ผ ์ค์ ์์ ์ ์ ์ ์๋ ์ ๋ณด๋ง ์ฌ์ฉ: ๊ณํ ๋ฌผ๋, ๊ณต์ง ์ ๋น, ๊ธฐ์ ์๋ณด, ์ ์ผยท์ ์ฃผ ์ค์ )\n",
"# ---------------------------------------------------------------------------\n",
"FEATS = [\"plan_v\", \"cap\", \"cap_eff\", \"t0\", \"km\", \"maint\", \"rain\", \"util\", \"util_eff\", \"util_eff4\", \"bpr_base\", \"lag1_delay\", \"lag7_delay\",\n",
" \"roll7_v\", \"surge_ratio\", \"is_weekend\", \"dow_sin\", \"dow_cos\", \"k_๊ต๋\", \"k_ํฐ๋\", \"k_๊ต์ฐจ๋ก\"]\n",
"\n",
"\n",
"def add_features(df):\n",
" d = df.sort_values([\"seg\", \"day\"]).copy()\n",
" g = d.groupby(\"seg\")\n",
" d[\"lag1_delay\"] = g[\"delay\"].shift(1)\n",
" d[\"lag7_delay\"] = g[\"delay\"].shift(7)\n",
" d[\"roll7_v\"] = g[\"v\"].transform(lambda s: s.shift(1).rolling(7).mean())\n",
" d[\"util\"] = d[\"plan_v\"] / d[\"cap\"]\n",
" d[\"util_eff\"] = d[\"plan_v\"] / d[\"cap_eff\"]\n",
" d[\"util_eff4\"] = d[\"util_eff\"] ** 4\n",
" d[\"bpr_base\"] = bpr(d[\"t0\"], d[\"plan_v\"], d[\"cap\"])\n",
" d[\"surge_ratio\"] = d[\"plan_v\"] / d[\"roll7_v\"]\n",
" d[\"is_weekend\"] = (d[\"dow\"] >= 5).astype(int)\n",
" d[\"dow_sin\"], d[\"dow_cos\"] = np.sin(2 * np.pi * d[\"dow\"] / 7), np.cos(2 * np.pi * d[\"dow\"] / 7)\n",
" for k in (\"๊ต๋\", \"ํฐ๋\", \"๊ต์ฐจ๋ก\"):\n",
" d[\"k_\" + k] = (d[\"kind\"] == k).astype(int)\n",
" return d.dropna().sort_values([\"day\", \"seg\"]).reset_index(drop=True)\n",
"# ---------------------------------------------------------------------------\n",
"# ---------------------------------------------------------------------------\n",
"# ๋
ธ์ ๋ง ํ๋ฆ ๋๊ตฌ: ์ต๋์ ๋(Edmonds-Karp), ์ฆ๋ถ ๋ฐฐ์ (ํผ์ก ๋ฐ์ ์ต๋จ์๊ฐ ๊ฒฝ๋ก), ๊ฒฝ๋ก ์ด๊ฑฐ\n",
"# ---------------------------------------------------------------------------\n",
"CAP = dict(zip(SEGS[\"seg\"], SEGS[\"cap\"]))\n",
"T0 = dict(zip(SEGS[\"seg\"], SEGS[\"t0\"]))\n",
"KM = dict(zip(SEGS[\"seg\"], SEGS[\"km\"]))\n",
"ARCS = {r.seg: (r.u, r.v) for r in SEGS.itertuples()}\n",
"SRC, DST = \"SRC\", \"DST\"\n",
"\n",
"\n",
"def seg_time(s, flow, cap=None, a=0.2, b=3.5):\n",
" \"\"\"๊ตฌ๊ฐ ํต๊ณผ์๊ฐ(๋ถ) = ์์ ์ฃผํ + ํผ์ก ์ง์ฒด. cap์ ์ฃผ๋ฉด ์ฉ๋์ ๋์ฒด(์ ๋นยท๊ธฐ์ ๋ฐ์).\"\"\"\n",
" r = flow / (cap or CAP[s])\n",
" return T0[s] * (1 + a * r ** b + 6.0 * max(0.0, r - 1.0)) # ์ฉ๋ ์ด๊ณผ๋ถ์ ๋๊ธฐ์ด๋ก ๊ธ์ฆ\n",
"\n",
"\n",
"def max_flow(closed=(), capmul=None):\n",
" \"\"\"์ต๋์ ๋(๋/์)๊ณผ ์ต์ ์ ๋จ ๊ตฌ๊ฐ(๋ณ๋ชฉ). capmul = {๊ตฌ๊ฐ: ์ฉ๋ ๋ฐฐ์จ}.\"\"\"\n",
" cap = {s: (0 if s in closed else CAP[s] * (capmul or {}).get(s, 1.0)) for s in CAP}\n",
" res = {}\n",
" for s, (u, v) in ARCS.items():\n",
" res[(u, v)] = res.get((u, v), 0) + cap[s]\n",
" res.setdefault((v, u), 0)\n",
" flow = 0.0\n",
" while True:\n",
" prev, q = {SRC: None}, [SRC]\n",
" for u in q:\n",
" for (a, b), c in res.items():\n",
" if a == u and c > 1e-9 and b not in prev:\n",
" prev[b] = u; q.append(b)\n",
" if DST not in prev:\n",
" break\n",
" path, x = [], DST\n",
" while prev[x]:\n",
" path.append((prev[x], x)); x = prev[x]\n",
" f = min(res[e] for e in path)\n",
" for a, b in path:\n",
" res[(a, b)] -= f; res[(b, a)] += f\n",
" flow += f\n",
" reach = set(prev)\n",
" cut = [s for s, (u, v) in ARCS.items() if u in reach and v not in reach and cap[s] > 0]\n",
" return flow, cut\n",
"\n",
"\n",
"def all_paths():\n",
" \"\"\"SRC -> DST ๋จ์ ๊ฒฝ๋ก(๊ตฌ๊ฐ ์ด๋ฆ ํํ) ์ ์ฒด.\"\"\"\n",
" out = []\n",
"\n",
" def dfs(node, segs, seen):\n",
" if node == DST:\n",
" out.append(tuple(segs)); return\n",
" for s, (u, v) in ARCS.items():\n",
" if u == node and v not in seen:\n",
" dfs(v, segs + [s], seen | {v})\n",
" dfs(SRC, [], {SRC})\n",
" return out\n",
"\n",
"\n",
"PATHS = all_paths()\n",
"\n",
"\n",
"def assign(demand, closed=(), capmul=None, steps=40):\n",
" \"\"\"์ฆ๋ถ ๋ฐฐ์ : ์์๋ฅผ steps์กฐ๊ฐ์ผ๋ก ๋๋ , ์ ์ฒด ํต๊ณผ์๊ฐ(๋์ x ์๊ฐ) ์ฆ๊ฐ๊ฐ ๊ฐ์ฅ ์์ ๊ฒฝ๋ก์ ์ฐจ๋ก๋ก ์น๋๋ค(์์คํ
์ต์ ๊ทผ์ฌ).\"\"\"\n",
" cap = {s: CAP[s] * (capmul or {}).get(s, 1.0) for s in CAP}\n",
" flow, pf = {s: 0.0 for s in CAP}, {}\n",
" ok = [p for p in PATHS if not any(s in closed for s in p)]\n",
" if not ok:\n",
" return None\n",
" for _ in range(steps):\n",
" chunk = demand / steps\n",
" best = min(ok, key=lambda p: sum((flow[s] + chunk) * seg_time(s, flow[s] + chunk, cap[s]) - flow[s] * seg_time(s, flow[s], cap[s]) for s in p))\n",
" for s in best:\n",
" flow[s] += chunk\n",
" pf[best] = pf.get(best, 0) + chunk\n",
" tt = {p: sum(seg_time(s, flow[s], cap[s]) for s in p) for p in pf}\n",
" avg = sum(pf[p] * tt[p] for p in pf) / demand\n",
" return {\"flow\": flow, \"paths\": pf, \"path_min\": tt, \"avg_min\": avg, \"util\": {s: flow[s] / cap[s] for s in cap if cap[s] > 0}}\n",
"\n",
"\n",
"FIXED_SHARE = {(\"A1\", \"A2\", \"A3\"): 0.32, (\"B1\", \"B3\"): 0.40, (\"C1\", \"C2\"): 0.28} # ๊ณผ๊ฑฐ ๊ดํ: ์ถ์ ๋ณ ๊ณ ์ ๋ฐฐ๋ถ(๊ฐ์)\n",
"\n",
"\n",
"def fixed_assign(demand, closed=(), capmul=None):\n",
" \"\"\"๊ณผ๊ฑฐ ๊ดํ ๋ฐฐ๋ถ: ์ถ์ ๋ณ ๊ณ ์ ๋น์จ. ๋งํ ์ถ์ ๋ฌผ๋์ ์ด๋ ค ์๋ ์ถ์ ์ ๋น์จ๋๋ก ์ฌ๋ถ๋ฐฐ.\"\"\"\n",
" cap = {s: CAP[s] * (capmul or {}).get(s, 1.0) for s in CAP}\n",
" live = {p: w for p, w in FIXED_SHARE.items() if not any(s in closed for s in p)}\n",
" if not live:\n",
" return None\n",
" tot = sum(live.values())\n",
" flow = {s: 0.0 for s in CAP}\n",
" pf = {p: demand * w / tot for p, w in live.items()}\n",
" for p, f in pf.items():\n",
" for s in p:\n",
" flow[s] += f\n",
" tt = {p: sum(seg_time(s, flow[s], cap[s]) for s in p) for p in pf}\n",
" return {\"flow\": flow, \"paths\": pf, \"path_min\": tt, \"avg_min\": sum(pf[p] * tt[p] for p in pf) / demand,\n",
" \"util\": {s: flow[s] / cap[s] for s in cap if cap[s] > 0}}\n",
"\n",
"\n",
"def lp_assign(demand, closed=(), capmul=None):\n",
" \"\"\"์ ํ๊ณํ(LP): ๊ตฌ๊ฐ ์ฉ๋ ์ํ(์ด์ฉ๋ฅ rho) ์๋์์ ์์ ์ฃผํ์๊ฐ ํฉ์ ์ต์ํํ๋ ๊ฒฝ๋ก๋ณ ๋ฌผ๋. ๋ถ๊ฐ๋ฅํ๋ฉด rho๋ฅผ 1.0๊น์ง ์ํ.\"\"\"\n",
" cap = {s: CAP[s] * (capmul or {}).get(s, 1.0) for s in CAP}\n",
" ok = [p for p in PATHS if not any(s in closed for s in p)]\n",
" if not ok:\n",
" return None\n",
" for rho in (0.85, 0.95, 1.0):\n",
" r = linprog([sum(T0[s] for s in p) for p in ok], A_ub=[[1.0 if s in p else 0.0 for p in ok] for s in CAP],\n",
" b_ub=[rho * cap[s] for s in CAP], A_eq=[[1.0] * len(ok)], b_eq=[demand], bounds=(0, None), method=\"highs\")\n",
" if r.status == 0:\n",
" break\n",
" else:\n",
" return assign(demand, closed, capmul) # ์ฉ๋ ์ด๊ณผ ์์: ์ฆ๋ถ ๋ฐฐ์ ์ผ๋ก ๋์ฒด(๊ณผ๋ถํ ํ์)\n",
" flow = {s: sum(r.x[i] for i, p in enumerate(ok) if s in p) for s in CAP}\n",
" pf = {p: float(r.x[i]) for i, p in enumerate(ok) if r.x[i] > 1e-6}\n",
" tt = {p: sum(seg_time(s, flow[s], cap[s]) for s in p) for p in pf}\n",
" return {\"flow\": flow, \"paths\": pf, \"path_min\": tt, \"avg_min\": sum(pf[p] * tt[p] for p in pf) / demand,\n",
" \"util\": {s: flow[s] / cap[s] for s in cap if cap[s] > 0}, \"rho\": rho}\n",
"\n",
"\n",
"def cost_of(res, tv=1000, fuel_km=450):\n",
" \"\"\"์๊ฐ๋น ๋น์ฉ(์, ๊ต์ก์ฉ ๊ฐ์ ): ๋๋น ์๊ฐ๋น์ฉ tv์/๋ถ + ์ ๋ฅ๋น fuel_km์/km.\"\"\"\n",
" return sum(f * (res[\"path_min\"][p] * tv + sum(KM[s] for s in p) * fuel_km) for p, f in res[\"paths\"].items())\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_data(days=180) # day 0~149 ๊ธฐ์กด + day 150~179 ์ ๊ท ์
๋ ฅ๋ถ\n",
"d = add_features(raw)\n",
"chk = {\"(๊ตฌ๊ฐ,์ผ์) ์ ์ผ\": not raw.duplicated([\"seg\", \"day\"]).any(), \"๊ฒฐ์ธก 0\": int(raw.isna().sum().sum()) == 0, \"ํต๊ณผ๋ ๋ฒ์\": bool(((raw.v >= 0) & (raw.v <= 3 * raw.cap)).all()),\n",
" \"ํต๊ณผ์๊ฐ >= ์์ ์ฃผํ\": bool((raw.transit >= raw.t0 - 1e-9).all())}\n",
"print(f\"[1] ์ ๊ท 30์ผ ํฌํจ {len(raw)}ํ ์ ํฉ์ฑ ๊ฒ์ฆ {sum(chk.values())}/{len(chk)} ํต๊ณผ\")\n",
"\n",
"# ---------------- 2) ์๋ ์ฌํ์ต: ๋๋ฆฌํํธ ์ ๊ฒ -> ์ฑ๋ฆฐ์ ํ์ต -> ์น๊ฒฉ ํ๋จ ----------------\n",
"def fit(tr):\n",
" return GradientBoostingRegressor(n_estimators=200, max_depth=3, learning_rate=0.08, random_state=0).fit(tr[FEATS], tr[\"delay\"])\n",
"\n",
"\n",
"def evaluate(m, te):\n",
" p = te[\"t0\"] * (1 + 0.08 * te[\"rain\"]) + m.predict(te[FEATS])\n",
" return mae(te[\"transit\"], p), mape(te[\"transit\"], p)\n",
"\n",
"\n",
"def retrain_pipeline(old_end, new_end, drift_mape=3.0, min_gain=0.03):\n",
" old, new = d[d.day < old_end], d[(d.day >= old_end) & (d.day < new_end)]\n",
" champ = fit(old)\n",
" ch_mae, ch_mape = evaluate(champ, new)\n",
" drift = ch_mape > drift_mape\n",
" chall = fit(d[d.day < new_end - 10]) # ์ต๊ทผ 10์ผ์ ๊ฒ์ฆ์ฉ์ผ๋ก ๋จ๊น\n",
" hold = d[(d.day >= new_end - 10) & (d.day < new_end)]\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\": drift, \"champ_mae\": a, \"chall_mae\": b, \"promote\": promote, \"model\": chall if promote else champ}\n",
"\n",
"\n",
"rt = retrain_pipeline(150, 180)\n",
"print(f\"[2] ์ฌํ์ต ํ์ดํ๋ผ์ธ: ๊ธฐ์กด ๋ชจ๋ธ์ ์ ๊ท 30์ผ MAPE {rt['champ_mape']:.2f}% (๋๋ฆฌํํธ ๊ธฐ์ค 3.0%: {'์ด๊ณผ' if rt['drift'] else '์ด๋ด'})\")\n",
"print(f\" ๊ฒ์ฆ 10์ผ MAE ์ฑํผ์ธ {rt['champ_mae']:.3f}๋ถ vs ์ฑ๋ฆฐ์ {rt['chall_mae']:.3f}๋ถ -> {'์ฑ๋ฆฐ์ ์น๊ฒฉ' if rt['promote'] else '๊ธฐ์กด ๋ชจ๋ธ ์ ์ง'}(๊ฐ์ 3% ์ด์์ผ ๋๋ง ์น๊ฒฉ)\")\n",
"model = rt[\"model\"]\n",
"\n",
"# ---------------- 3) 7์ผ ์์ก ์์ ์์ธก(Ridge ์์ผ+์ถ์ธ vs ์ ์ฃผ ๋์์ผ Baseline) ----------------\n",
"daily = raw[raw.seg.isin([\"A1\", \"B1\", \"C1\"])].groupby(\"day\")[\"plan_v\"].sum()\n",
"\n",
"\n",
"def design(days):\n",
" days = np.asarray(days)\n",
" X = np.zeros((len(days), 9))\n",
" X[np.arange(len(days)), days % 7] = 1\n",
" X[:, 7] = days / 100\n",
" X[:, 8] = ((days >= 100) & (days < 110)).astype(float) # ํญ์ฆ ๊ธฐ๊ฐ(์๋ ค์ง ์ฌ๊ฑด) ๋๋ฏธ\n",
" return X\n",
"\n",
"\n",
"tr_days = daily.index[daily.index < 173]\n",
"rg = Ridge(alpha=0.1, fit_intercept=False).fit(design(tr_days), daily[tr_days])\n",
"test_days = np.arange(173, 180)\n",
"f_ridge = rg.predict(design(test_days)); f_naive = daily[test_days - 7].values\n",
"print(f\"\\n[3] ์์ก ์์ 7์ผ ์์ธก ๊ฒ์ฆ(day 173~179): Ridge MAPE {mape(daily[test_days], f_ridge):.1f}% vs ์ ์ฃผ ๋์์ผ {mape(daily[test_days], f_naive):.1f}%\")\n",
"rg = Ridge(alpha=0.1, fit_intercept=False).fit(design(daily.index), daily)\n",
"nxt = np.arange(180, 187)\n",
"dem = rg.predict(design(nxt))\n",
"\n",
"# ---------------- 4) ์์ -> ๋ณ๋ชฉยท์ฐํ ๋ฐฐ๋ถ -> ์น์ธ๋๊ธฐ ๋ถ๋ฆฌ ----------------\n",
"NOTICE = {183: {\"A2\": 0.65}} # ๊ณต์ง๋ ์ ๋น(๊ฐ์ ): day 183 A2 ๊ต๋ ์ฉ๋ 65%\n",
"out = []\n",
"for day, D in zip(nxt, dem):\n",
" cm = NOTICE.get(int(day))\n",
" f, l = fixed_assign(D, (), cm), lp_assign(D, (), cm)\n",
" extra = l[\"avg_min\"] - lp_assign(D)[\"avg_min\"]\n",
" risky = max(l[\"util\"].values()) > 0.98 or extra >= 3 or day in NOTICE\n",
" out.append({\"day\": int(day), \"์์ธก ์์(๋/์)\": D, \"๊ณ ์ (๋ถ)\": f[\"avg_min\"], \"์ฐํ ๋ฐฐ์ (๋ถ)\": l[\"avg_min\"], \"์ ๊ฐ๋ฅ \": 1 - cost_of(l) / cost_of(f),\n",
" \"์ต๋ ์ด์ฉ๋ฅ \": max(l[\"util\"].values()), \"์ฒ๋ฆฌ\": \"์์ ๊ตฐ์๊ด ์น์ธ๋๊ธฐ(2๋จ๊ณ)\" if risky else \"์ถ์ฒ(๊ฒํ ํ ์งํ)\"})\n",
"pl = pd.DataFrame(out)\n",
"print(\"\\n[4] ํฅํ 7์ผ ์์ก ์์ยท์ฐํ ๋ฐฐ๋ถ ๊ณํ [๊ต์ก์ฉ๊ฐ์์๋ฃ]\")\n",
"print(pl.round(2).to_string(index=False))\n",
"pend = pl[pl[\"์ฒ๋ฆฌ\"].str.contains(\"์น์ธ๋๊ธฐ\")]\n",
"print(f\" ์น์ธ๋๊ธฐ {len(pend)}๊ฑด(day {', '.join(map(str, pend['day']))}) - ์๋ ์งํํ์ง ์๊ณ ์์ ๊ตฐ์๊ด ๊ฒฐ์ฌ(๋ฏธ์
7)๋ก ๋๊ธด๋ค.\")\n",
"\n",
"# ---------------- 5) ์ด์ ๋ฌธ์ ์๋น์จยท์ธํฐํ์ด์ค ๋ช
์ธ ----------------\n",
"docs = {\"๋ฐ์ดํฐ ์ฌ์ ยท์คํค๋ง(M1)\": 29, \"์ ์ ๊ท์น 10์ข
(M2)\": 10, \"BaselineยทEDA(M3)\": 8, \"๋ณ๋ชฉ ์๋ณยท์ฐํ(M4)\": 4, \"์ฐํ ์์ยท์ ๋ฅ ์์ง(M5)\": 4, \"๋์๋ณด๋ยท์ค๋ช
(M6)\": 6,\n",
" \"์น์ธ ์ ์ฐจยท๊ถํ(M7)\": 4, \"์คํธ๋ ์ค ์ํ(M8)\": 4, \"AARยทROI(M9)\": 5, \"์ด์ยท์ฌํ์ต ์ ์ฐจ(M10)\": 6, \"์ฅ์ ๋์\": 5, \"์ธํฐํ์ด์ค ๊ฐ์ด๋\": 4}\n",
"done = sum(v > 0 for v in docs.values())\n",
"iface = {\"input\": {\"daily_plan\": [\"day:int\", \"seg:str\", \"plan_v:float\", \"maint:int\", \"rain:int\"], \"format\": \"json-lines\"},\n",
" \"output\": {\"forecast\": [\"day\", \"demand_vph\", \"paths\", \"avg_min\", \"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.day >= 150]\n",
"fin_mae, fin_mape = evaluate(model, te)\n",
"base_mape = mape(te.transit, te.t0 + te.bpr_base)\n",
"checks = [(\"ํ๊ฒฝ ์ ๊ฒ ํต๊ณผ\", env_ok == len(libs)), (\"๋ฐ์ดํฐ ์ ํฉ์ฑ ์ ํญ๋ชฉ ํต๊ณผ\", all(chk.values())), (\"์ฌํ์ต ํ์ดํ๋ผ์ธ ์ ์ ์คํ(์ฑํผ์ธ/์ฑ๋ฆฐ์ ํ์ ์ถ๋ ฅ)\", rt[\"champ_mae\"] > 0),\n",
" (\"ํต๊ณผ์๊ฐ ์์ธก MAPE 25% ์ดํ\", fin_mape <= 25), (\"๊ธฐ์ค BPR ๋๋น MAE 20% ์ด์ ๊ฐ์ \", fin_mae <= 0.8 * mae(te.transit, te.t0 + te.bpr_base)),\n",
" (\"7์ผ ์์ ์์ธก์ด ์ ์ฃผ ๋์์ผ Baseline๋ณด๋ค ์ ํ\", mape(daily[test_days], f_ridge) < mape(daily[test_days], f_naive)),\n",
" (\"์ํยท์ ๋น ์ฐํ์ ์๋ ์งํ 0๊ฑด(์น์ธ๋๊ธฐ ๋ถ๋ฆฌ)\", len(pend) >= 1 and (pl.loc[pl.day == 183, \"์ฒ๋ฆฌ\"].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\" - ์ ๊ท 30์ผ ๋ฐ์ดํฐ๋ก ์ฌํ์ต ํ๋จ์ ์๋ ์ํํ๋ค(๋๋ฆฌํํธ {'๊ฐ์ง' if rt['drift'] else '๋ฏธ๊ฐ์ง'}, {'์ฑ๋ฆฐ์ ์น๊ฒฉ' if rt['promote'] else '๊ธฐ์กด ๋ชจ๋ธ ์ ์ง'}).\")\n",
"print(f\" - ํต๊ณผ์๊ฐ MAPE {base_mape:.1f}%(BPR) -> {fin_mape:.1f}%, ํฅํ 7์ผ ์์ ์์ธก MAPE {mape(daily[test_days], f_ridge):.1f}%.\")\n",
"print(f\" - ํฅํ 7์ผ ์ค ์ฐํ ์ต์ ํ ํ๊ท ์ ๊ฐ {pl['์ ๊ฐ๋ฅ '].mean():.1%}, ์ ๋น ๊ณต์ง์ผ์ ์น์ธ๋๊ธฐ๋ก ๋ถ๋ฆฌํ๋ค. ์ค์ ์ฐํ ํ๋ฌ์ ์์ ๊ตฐ์๊ด ๊ฒฐ์ฌ์ ์งํ๊ณํต ๊ฒฐ์ฌ์ ๋ฐ๋ฅธ๋ค.\")\n",
"\n",
"fig, ax = plt.subplots(1, 2, figsize=(12, 4))\n",
"ax[0].plot(daily.index, daily.values, label=\"actual\"); ax[0].plot(nxt, dem, \"r--o\", label=\"forecast\"); ax[0].legend(); ax[0].set_title(\"Daily transport demand and 7-day forecast\")\n",
"ax[1].bar(pl[\"day\"] - .2, pl[\"๊ณ ์ (๋ถ)\"], .4, label=\"fixed\"); ax[1].bar(pl[\"day\"] + .2, pl[\"์ฐํ ๋ฐฐ์ (๋ถ)\"], .4, label=\"detour assignment\"); ax[1].legend(); ax[1].set_title(\"Avg transit (min), next 7 days\")\n",
"plt.suptitle(\"MOV01 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] ์ ๊ท 30์ผ ํฌํจ 1800ํ ์ ํฉ์ฑ ๊ฒ์ฆ 4/4 ํต๊ณผ\n",
"[2] ์ฌํ์ต ํ์ดํ๋ผ์ธ: ๊ธฐ์กด ๋ชจ๋ธ์ ์ ๊ท 30์ผ MAPE 2.93% (๋๋ฆฌํํธ ๊ธฐ์ค 3.0%: ์ด๋ด)\n",
" ๊ฒ์ฆ 10์ผ MAE ์ฑํผ์ธ 0.534๋ถ vs ์ฑ๋ฆฐ์ 0.535๋ถ -> ๊ธฐ์กด ๋ชจ๋ธ ์ ์ง(๊ฐ์ 3% ์ด์์ผ ๋๋ง ์น๊ฒฉ)\n",
"\n",
"[3] ์์ก ์์ 7์ผ ์์ธก ๊ฒ์ฆ(day 173~179): Ridge MAPE 3.5% vs ์ ์ฃผ ๋์์ผ 4.7%\n",
"\n",
"[4] ํฅํ 7์ผ ์์ก ์์ยท์ฐํ ๋ฐฐ๋ถ ๊ณํ [๊ต์ก์ฉ๊ฐ์์๋ฃ]\n",
" day ์์ธก ์์(๋/์) ๊ณ ์ (๋ถ) ์ฐํ ๋ฐฐ์ (๋ถ) ์ ๊ฐ๋ฅ ์ต๋ ์ด์ฉ๋ฅ ์ฒ๋ฆฌ\n",
" 180 216.46 42.89 42.43 0.00 0.85 ์ถ์ฒ(๊ฒํ ํ ์งํ)\n",
" 181 204.40 42.15 41.95 0.00 0.85 ์ถ์ฒ(๊ฒํ ํ ์งํ)\n",
" 182 258.65 56.35 44.70 0.16 0.95 ์ถ์ฒ(๊ฒํ ํ ์งํ)\n",
" 183 276.73 91.80 51.32 0.37 1.11 ์์ ๊ตฐ์๊ด ์น์ธ๋๊ธฐ(2๋จ๊ณ)\n",
" 184 266.37 61.40 45.29 0.21 0.95 ์ถ์ฒ(๊ฒํ ํ ์งํ)\n",
" 185 275.57 68.96 45.87 0.27 1.00 ์์ ๊ตฐ์๊ด ์น์ธ๋๊ธฐ(2๋จ๊ณ)\n",
" 186 259.89 57.16 44.79 0.17 0.95 ์ถ์ฒ(๊ฒํ ํ ์งํ)\n",
" ์น์ธ๋๊ธฐ 2๊ฑด(day 183, 185) - ์๋ ์งํํ์ง ์๊ณ ์์ ๊ตฐ์๊ด ๊ฒฐ์ฌ(๋ฏธ์
7)๋ก ๋๊ธด๋ค.\n",
"\n",
"[5] ์ด์ ๋ฌธ์ 12๊ฐ ํญ๋ชฉ ์ค ์์ฑ ์๋ฃ 12๊ฐ (100%) / ์ธํฐํ์ด์ค ๋ช
์ธ(JSON):\n",
" {\"input\": {\"daily_plan\": [\"day:int\", \"seg:str\", \"plan_v:float\", \"maint:int\", \"rain:int\"], \"format\": \"json-lines\"}, \"output\": {\"forecast\": [\"day\", \"demand_vph\", \"paths\", \"avg_min\", \"status\"], \"approval\": \"PENDING_ONLY\"}, \"auth\": \"role-based(๊ฐ์)\", \"note\": \"์์ ๊ตฐ์์ ๋ณด์ฒด๊ณ๋ ๊ฐ์ ์ธํฐํ์ด์ค๋ก๋ง ์ ์\"}\n",
"\n",
"[6] ์์ฒด ์ ๊ฒ\n",
" [PASS] ํ๊ฒฝ ์ ๊ฒ ํต๊ณผ\n",
" [PASS] ๋ฐ์ดํฐ ์ ํฉ์ฑ ์ ํญ๋ชฉ ํต๊ณผ\n",
" [PASS] ์ฌํ์ต ํ์ดํ๋ผ์ธ ์ ์ ์คํ(์ฑํผ์ธ/์ฑ๋ฆฐ์ ํ์ ์ถ๋ ฅ)\n",
" [PASS] ํต๊ณผ์๊ฐ ์์ธก MAPE 25% ์ดํ\n",
" [PASS] ๊ธฐ์ค BPR ๋๋น MAE 20% ์ด์ ๊ฐ์ \n",
" [PASS] 7์ผ ์์ ์์ธก์ด ์ ์ฃผ ๋์์ผ Baseline๋ณด๋ค ์ ํ\n",
" [PASS] ์ํยท์ ๋น ์ฐํ์ ์๋ ์งํ 0๊ฑด(์น์ธ๋๊ธฐ ๋ถ๋ฆฌ)\n",
" [PASS] ์ด์ ๋ฌธ์ 100% ์๋น\n",
"\n",
"[7] ๋ธ๋ฆฌํ ์์ฝ\n",
" - ์ ๊ท 30์ผ ๋ฐ์ดํฐ๋ก ์ฌํ์ต ํ๋จ์ ์๋ ์ํํ๋ค(๋๋ฆฌํํธ ๋ฏธ๊ฐ์ง, ๊ธฐ์กด ๋ชจ๋ธ ์ ์ง).\n",
" - ํต๊ณผ์๊ฐ MAPE 9.6%(BPR) -> 2.9%, ํฅํ 7์ผ ์์ ์์ธก MAPE 3.5%.\n",
" - ํฅํ 7์ผ ์ค ์ฐํ ์ต์ ํ ํ๊ท ์ ๊ฐ 16.9%, ์ ๋น ๊ณต์ง์ผ์ ์น์ธ๋๊ธฐ๋ก ๋ถ๋ฆฌํ๋ค. ์ค์ ์ฐํ ํ๋ฌ์ ์์ ๊ตฐ์๊ด ๊ฒฐ์ฌ์ ์งํ๊ณํต ๊ฒฐ์ฌ์ ๋ฐ๋ฅธ๋ค.\n",
"```\n",
" \n"
]
}
],
"metadata": {
"colab": {
"provenance": []
},
"kernelspec": {
"display_name": "Python 3",
"name": "python3"
},
"language_info": {
"name": "python"
}
},
"nbformat": 4,
"nbformat_minor": 0
}