{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# ๐ฏ [DAT10] ๋ฐ์ดํฐ ๋ง์ด๋ ๊ธฐ๋ฐ ๊ณต๊ธ๋ง ์ฌ๋ ์๋ณ SaaS\n",
"### Google Colab ์ค์ต ํ๊ฒฝ (๋ฐ์ดํฐยท์๋ํยท์
๋ฌดํ๋ซํผ / Level 3)\n",
"\n",
"[](https://colab.research.google.com/github/edunlab/milicontent2/blob/main/uploads/DAT10/DAT10_solution.ipynb)\n",
"\n",
"---\n",
"\n",
"## ๐ 1. ํ๋ก์ ํธ ๊ฐ์ ๋ฐ ๋ฐฐ๊ฒฝ\n",
"\n",
"| ํญ๋ชฉ | ๋ด์ฉ |\n",
"| :--- | :--- |\n",
"| **๊ณผ์ ๋ถ๋ฅ** | ๋ฐ์ดํฐยท์๋ํยท์
๋ฌดํ๋ซํผ (Level 3) |\n",
"| **ํ์ต ์์ ์๊ฐ** | 10๊ฐ ๋ฏธ์
(5์ผ / ์ด 27.5์๊ฐ) |\n",
"| **ํ๊ฒฝ ์ฌ์** | Python 3.10+, numpy, pandas, matplotlib, scipy, scikit-learn |\n",
"\n",
"### ๐ ํ๋ก์ ํธ ๋ชฉํ\n",
"์ ์์๋ ํต์ ๋ง์ด ๋๊ธด ์ฑ ๋ณด๊ธ์๊ฐ ์
๊ณ ์ ๋ถ์ถ์ ๊ฐ์ ๊ธฐ๋กํ๊ณ , ๋ณต๊ตฌ ํ ์ฌ๋ฌ ๊ธฐ๋ก์ ํฉ์น๋ ๊ณผ์ ์์ ์ค๋ณตยท๋๋ฝยท์ถฉ๋ยท์๋ณ์กฐ๊ฐ ์๊ธฐ๋ฉฐ, ์ด๋ฅผ ๋์น๋ฉด ์ฌ๊ณ ํ๋จ๊ณผ ์
์ฒด ๊ณต๊ธ ์ด์ ์งํ ์๋ณ์ด ๋ฆ์ด์ง๋ค. ๋ณธ ํ๋ก์ ํธ๋ ๋ฐฉ์ฐ ๊ณต๊ธ๋ง ํ๋ณต๋ ฅ ์ฐ๊ตฌ(๋ฏธ๊ตญ ๋ฐฉ์์ฐ์
๊ธฐ๋ฐ ์ ์ฑ
์ฐ๊ตฌ)์ ๋ฌธ์ ์์์ ๊ต์ก์ฉ ๊ฐ์ ์๋ฃ๋ก ์ฌ๊ตฌ์ฑํด, ๊ฐ์ ๋ณด๊ธ์ 5๊ณณ์ด ์คํ๋ผ์ธ์ผ๋ก ์ฌ๊ณ ๋ฅผ ๊ธฐ๋กํ๊ณ ๋๊ธฐํํ๋ SaaS๋ฅผ ๋ง๋ค๊ณ ๋๊ธฐํ๋ ์๋ฃ์์ ๊ณต๊ธ๋ง ์ฌ๋ ์งํ(์
์ฒด ๊ณต๊ธ ๊ฐ์, ์ฌ๊ณ ์์ง ์๋ฐ)๋ฅผ ์๋ณํ๋ ๊ณผ์ ์ด๋ค. ์๊ตฌ์ฌํญยท์คํค๋ง, ์คํ๋ผ์ธ ์
๋ ฅ ์ ์ฒ๋ฆฌ, P2P/USB ๊ตํ, CRDT ๋๊ธฐํ์ ์ถฉ๋ ํ์ง, ๋ฌด๊ฒฐ์ฑ ๊ฒ์ฆ, ๋์๋ณด๋, ๊ตฐ์๊ด HITL ๋ณํฉ, 7์ผ ๋จ์ ํ ์ผ๊ด ๋๊ธฐํ ์คํธ๋ ์ค, AAR ์ฌํ๋, ํตํฉ ํจํค์ง๊น์ง ๋จ๊ณ๋ณ๋ก ๊ตฌํํ๋ค. ์ต์ข
๋ชฉํ๋ ์คํ๋ผ์ธ ์ฌ๊ณ ๋๊ธฐํ์ ์ ํ๋์ ๋ฌด๊ฒฐ์ฑ์ ๊ทผ๊ฑฐ์ ํจ๊ป ๋ณด์ฌ ์ฃผ๋, ์ถฉ๋ ๋ณํฉ๊ณผ ๋ณด๊ธ ์กฐ์น๋ ๊ตฐ์๊ดยท์งํ๊ด์ด ํ๋๋ก ์ง์ํ๋ ์์คํ
์ ์์ฑํ๋ ๊ฒ์ด๋ค.\n",
"\n",
"> **โ ๏ธ [๊ต์ก์ฉ ๊ฐ์ ๋ฐ์ดํฐ์
์์น]**\n",
"> - ๋ณธ ๋
ธํธ๋ถ์ ๋ชจ๋ ๋ฐ์ดํฐ(์ ์, ๋ก๊ทธ, ์๋๋ฆฌ์ค ๋ฑ)๋ ๋ณด์ ๊ท์ ์ ์ค์ํ์ฌ **์ฝ๋ ๋ด ๋์ ๊ธฐ๋ฐ ๊ฐ์ ๋ฐ์ดํฐ๋ก ์์ฑ**๋ฉ๋๋ค.\n",
"> - ์ค์ ๋ถ๋ ์์น, ์์ ์ธ๊ฐ, ์ค์ ์ ์ฅ ์ ๋ณด, ๊ตฐ์ฌ ๊ธฐ๋ฐ ๋ฐ์ดํฐ๋ ์ผ์ฒด ํฌํจ๋์ง ์์ต๋๋ค.\n",
"\n",
"---\n",
"\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# ============================================================================\n",
"# [Google Colab ํ๊ฒฝ ์ค์ ] ํ์ ๋ผ์ด๋ธ๋ฌ๋ฆฌ ์ค์น ๋ฐ ์ํฌํธ\n",
"# ============================================================================\n",
"!pip install -q numpy pandas matplotlib scipy scikit-learn\n",
"\n",
"import os\n",
"import sys\n",
"import time\n",
"import math\n",
"import json\n",
"import re\n",
"import numpy as np\n",
"import pandas as pd\n",
"import matplotlib.pyplot as plt\n",
"import warnings\n",
"warnings.filterwarnings('ignore')\n",
"\n",
"# ๊ทธ๋ํ ์๋ฌธ ํฐํธ ๊ธฐ๋ณธ ์ค์ (ํ๊ฒฝ ๋
๋ฆฝ์ฑ ๋ณด์ฅ)\n",
"plt.rcParams['font.sans-serif'] = 'DejaVu Sans'\n",
"plt.rcParams['axes.unicode_minus'] = False\n",
"\n",
"print(f\"์คํ ํ๊ฒฝ ์ค๋น ์๋ฃ (Python {sys.version.split()[0]})\")\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## ๐ฏ ๋ฏธ์
01. ์ ์ ํต์ ๋์ ํ๊ฒฝ์์์ ์ผ์ ๊ตฐ์ ์ฌ๊ณ ๊ด๋ฆฌ ์๊ตฌ์ฌํญ ๋ถ์ ๋ฐ ๋ก์ปฌ DB ์คํค๋ง ์ค๊ณ\n",
"**์์์๊ฐ**: ์ฝ 1.5์๊ฐ\n",
"\n",
"### ๐ ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ์ํ ์ง์นจ\n",
"#### ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ๋น์์ฑ\n",
"\n",
"ํต์ ๋์ ํ๊ฒฝ์์์ ์ผ์ ์ฌ๊ณ ๊ด๋ฆฌ ์๊ตฌ์ฌํญ์ ๋ถ์ํ๊ณ ๋ก์ปฌ DB ์คํค๋ง๋ฅผ ์ค๊ณํ๋ค. ์ค์ ์ ์ ํ์ฅ์์๋ ๋จ์ ํ์ง์ ๊ทธ์น์ง ์๊ณ ์ ์ํ ์ํ ํ๋จ๊ณผ ์ ๋ขฐ๋ ๋์ ์์ฌ๊ฒฐ์ ์ด ํ์์ ์ด๋ฏ๋ก, **ํต์ ์ด ๋๊ธด ํ๊ฒฝ์์ ๋ณด๊ธ์๊ฐ ์
๊ณ ยท๋ถ์ถยท์กฐ์ ์ ๊ธฐ๋กํ๋ ์คํ๋ผ์ธ ์ฌ๊ณ ๊ด๋ฆฌ์ ์๊ตฌ์ฌํญ 12๊ฐ๋ฅผ ์ ๋ฆฌํ๊ณ , ์ด๋ฅผ ๋ง์กฑํ๋ ํธ๋์ญ์
์คํค๋ง(ํ๋ 12๊ฐ)์ ๋ก์ปฌ DB ๊ตฌ์กฐ๋ฅผ ์ค๊ณํ๋ค.** ๊ณผ์
์ ์์ํ์ฌ ์คํ์ ๋ฐฉ์งํ๊ณ ์์ ๋์๋ ฅ์ ๊ทน๋ํํด์ผ ํฉ๋๋ค.\n",
"\n",
"์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ณธ ๋ฏธ์
์์๋ **๊ฐ์ ๋ณด๊ธ์ 5๊ณณ๊ณผ ํ๋ชฉ 12์ข
์ ํธ๋์ญ์
(IDยท๋ณด๊ธ์ยทํ๋ชฉยท๊ตฌ๋ถยท์๋ยท์๊ฐยท์ํฉยท์
์ฒดยท๋
ผ๋ฆฌ ์๊ณยท๋๊ธฐํ ์ํยท์ฒดํฌ์ฌยท๋น๊ณ ) ํ๋๋ฅผ ์๋ฃํยท๋จ์ยทํ์ฉ ๋ฒ์์ ํจ๊ป ์ ์ํ๊ณ , ID๋ ๋
ธ๋ ์ ๋์ด์ ์ผ๋ จ๋ฒํธ๋ก ๋ง๋ค์ด ์ค๋ณต ์
๋ ฅ์ ๋ง๋ ๋ฉฑ๋ฑ ํค๋ก ์ด๋ค ๊ณผ์ ์ ์งํํ๋** ์ค์ต์ ์ฒด๊ณ์ ์ผ๋ก ์ํํฉ๋๋ค.\n",
"\n",
"ํ์ต์๋ ์ค์ต๊ฐ์ด๋์ ์ ์๋ ์ด 4๊ฐ ์ธ๋ถ ํ๋(๊ธฐ๋ณธ ๊ธฐ๋ฅ ๊ตฌํ โ ๊ฒฐ๊ณผ ์งํ ๊ฒ์ฆ โ ํ๋ผ๋ฏธํฐ ํ๋ ๋ฐ ๋์ ๊ณผ์ )์ ์์ฐจ์ ์ผ๋ก ์งํํ๋ฉฐ ํต์ฌ ํ์ ๊ธฐ์ค์ ๋ฌ์ฑํฉ๋๋ค.\n",
"\n",
"**์ด๋ฒ ๊ฒฐ๊ณผ๋ฌผ:** `data/schema/dat10_input_schema.json ๋ฐ docs/dat10_data_spec.md` ํํ์ ๊ฒฐ๊ณผ๋ฌผ์ ์์ฑํ๋ค.\n",
"\n",
"### ๐ ํ๊ฐ ๋ฐ ๊ฒ์ฆ ๊ธฐ์ค (PASS ์กฐ๊ฑด)\n",
"#### ๋ฏธ์
ํ์ \n",
"\n",
"**ํ๊ฐ ๊ตฌ๋ถ:** ํ์\n",
"\n",
"**ํต์ฌ ํ๊ฐ์์ญ:** ๋ฐ์ดํฐยท์๋ํยท์
๋ฌดํ๋ซํผ ์ค๋ฌด ๋ฐ ๊ณต๊ธ๋ง ์ฌ๋ ์๋ณ ๋ฐ์ดํฐ๋ง์ด๋ SaaS(๋ฐฉ์ฐ ๊ณต๊ธ๋ง ํ๋ณต๋ ฅ ์ฐ๊ตฌ) โ ๋ฏธ๊ตญ ๋ฐฉ์์ฐ์
๊ธฐ๋ฐ(Defense Industrial Base) ์ ์ฑ
์ฐ๊ตฌ ์ ์ฅ ํ๊ฒฝ๊ณผ์ ๊ตฐ์ฌ์ ๋ถํฉ์ฑ\n",
"\n",
"#### ๋ฏธ์
PASS ์กฐ๊ฑด\n",
"\n",
"- ์ ๋ ๊ธฐ์ค: ์คํค๋ง ํ๋ ์ ์ ์์ฑ๋ 100%, ์ํ ๋ฐ์ดํฐ ๊ฒ์ฆ ์ค๋ฅ 0๊ฑด\n",
"\n",
"\n",
"- ์งํ ๊ธฐ์ค: ์
๋ ฅ ๋ฐ์ดํฐ ์ ํจ์ฑ ๊ฒ์ฆ ์คํฌ๋ฆฝํธ ํต๊ณผ ๋ฐ ๋ช
์ธ์ ๋ฌธ์ํ ์๋ฃ\n",
"\n",
"\n",
"- ๊ฒ์ฆ ๋ฐฉ๋ฒ: JSON Schema ๊ฒ์ฆ ๋๊ตฌ ๋ฐ ์ํ ๋ฐ์ดํฐ ํ์ฑ ๋จ์ ํ
์คํธ\n",
"\n",
"\n",
"#### ์ฑ์ ์ ํ์ ๋ฉ๋ชจ\n",
"\n",
"> ๋จ์ ์ฝ๋ ์คํ ์ฌ๋ถ๋ฟ๋ง ์๋๋ผ ์ถ๋ ฅ๋ ์์น์ ๊ทผ๊ฑฐ ์๋ฃ๊ฐ ๊ตฐ ์ค๋ฌด ์๊ตฌ์ฌํญ์ ๋ถํฉํ๋์ง ๋ฉด๋ฐํ ํ์ธํ๋ค.\n",
"\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"\"\"\"ํต์ ๋์ ํ๊ฒฝ์ ์ผ์ ๋ณด๊ธ์ ์ฌ๊ณ ยท๊ณต๊ธ๋ง ์๋ฃ ์๊ตฌ์ฌํญ 12๊ฐ๋ฅผ ์ ์ํ๊ณ , ๋ก์ปฌ DB(SQLite ๋ฉ๋ชจ๋ฆฌ DB) ์คํค๋ง์ ํธ๋์ญ์
๋ ์ฝ๋ 12๊ฐ ํ๋์ ๊ฒ์ฆ ๊ท์น์ ๋ง๋ค์ด ์ ์ ํ๋ณธ ๊ฒ์ฆ ์ค๋ฅ 0๊ฑดยท์คํค๋ง ์ ์ ์์ฑ๋ 100%ยท์ ์ฝ ์กฐ๊ฑด(์ค๋ณตยท์์ยท๋
ผ๋ฆฌ) ์ํ์ ํ์ธํ๋ฉฐ ์ค๋ฅ ์ฃผ์
๊ฒ์ถ๋ ฅ์ ์ ๊ฒํ๋ค.\"\"\"\n",
"import hashlib\n",
"import json\n",
"import re\n",
"import sqlite3\n",
"import numpy as np\n",
"import pandas as pd\n",
"import matplotlib.pyplot as plt\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# [๊ต์ก์ฉ๊ฐ์์๋ฃ] ๊ฐ์ ์ผ์ ๋ณด๊ธ์ยท์
์ฒดยทํ๋ชฉ ์๋ฃ ์์ฑ๊ธฐ (์ค์ ๋ถ๋ยท์
์ฒดยทํ๋ชฉยท์ฌ๊ณ ์ ๋ณด ์์)\n",
"# ๊ฐ์ ๋ณด๊ธ์ 5๊ณณ์ด ํ๋ชฉ 12์ข
์ ์
๊ณ ยท๋ถ์ถยท์กฐ์ ํธ๋์ญ์
์ ์คํ๋ผ์ธ์ผ๋ก ๊ธฐ๋กํ๊ณ ๋์ค์ ๋๊ธฐํํ๋ค.\n",
"# ๊ณต๊ธ๋ง ์ฌ๋ ์งํ(์
์ฒด ๋ฉ๊ธฐ ์ง์ฐ, ํ๋ชฉ ์ฌ๊ณ ๊ธ๊ฐ)๋ ๋๊ธฐํ๋ ์๋ฃ๋ฅผ ๋ง์ด๋ํด ์๋ณํ๋ค.\n",
"# ---------------------------------------------------------------------------\n",
"NODES = [\"๋ณด๊ธ์A\", \"๋ณด๊ธ์B\", \"๋ณด๊ธ์C\", \"๋ณด๊ธ์D\", \"๋ณด๊ธ์E\"]\n",
"SUPPLIERS = [\"๊ฐ์์
์ฒด1\", \"๊ฐ์์
์ฒด2\", \"๊ฐ์์
์ฒด3\", \"๊ฐ์์
์ฒด4\", \"๊ฐ์์
์ฒด5\", \"๊ฐ์์
์ฒด6\"]\n",
"CATS = [\"์ผ๋ฐ๋ณด๊ธ\", \"์ ๋น๋ถํ\", \"์๋ฌด์๋ชจ\"]\n",
"ITEMS = [{\"item\": f\"I{i + 1:02d}\", \"cat\": CATS[i % 3], \"supplier\": SUPPLIERS[i % 6], \"lead_days\": 3 + (i * 5) % 12, \"use\": 4 + (i * 7) % 11, \"min_level\": 30 + 10 * (i % 5)} for i in range(12)]\n",
"ITEM_IDS = [x[\"item\"] for x in ITEMS]\n",
"CTXS = [\"ํ์\", \"์ผ๊ฐ\", \"ํต์ ๋ถ๋\"]\n",
"\n",
"\n",
"def make_txns(seed=7, days=7, per_node_day=60, nodes=None):\n",
" \"\"\"๋
ธ๋๋ณ ์คํ๋ผ์ธ ํธ๋์ญ์
๋ชฉ๋ก. id๋ ๋
ธ๋-์ผ๋ จ๋ฒํธ(๋ฉฑ๋ฑ ํค), t๋ ์๊ฐ(์๊ฐ ๋จ์), ctx๋ ์
๋ ฅ ์ํฉ.\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" out = []\n",
" for n in (nodes or NODES):\n",
" seq = 0\n",
" for k in range(days * per_node_day):\n",
" seq += 1\n",
" it = ITEMS[int(rng.integers(len(ITEMS)))]\n",
" t = float(rng.uniform(0, 24 * days))\n",
" ctx = \"์ผ๊ฐ\" if (t % 24 >= 20 or t % 24 < 5) else \"ํ์\"\n",
" if rng.random() < 0.12:\n",
" ctx = \"ํต์ ๋ถ๋\"\n",
" r = rng.random()\n",
" if r < 0.22:\n",
" op, q = \"in\", int(it[\"use\"] * rng.uniform(4, 9))\n",
" elif r < 0.95:\n",
" op, q = \"out\", int(max(1, rng.normal(it[\"use\"] * 1.5, 3)))\n",
" else:\n",
" op, q = \"adjust\", int(rng.integers(-5, 6))\n",
" out.append({\"id\": f\"{n}-{seq:05d}\", \"node\": n, \"item\": it[\"item\"], \"op\": op, \"qty\": q, \"t\": round(t, 2), \"ctx\": ctx, \"supplier\": it[\"supplier\"]})\n",
" return out\n",
"\n",
"\n",
"def stock_of(txns, base=500):\n",
" \"\"\"ํ๋ชฉ๋ณ ์ฌ๊ณ = ๊ธฐ์ด ์ฌ๊ณ + ์
๊ณ - ๋ถ์ถ + ์กฐ์ (id ์ค๋ณต์ ํ ๋ฒ๋ง ๋ฐ์).\"\"\"\n",
" st, seen = {i: base for i in ITEM_IDS}, set()\n",
" for x in txns:\n",
" if x[\"id\"] in seen:\n",
" continue\n",
" seen.add(x[\"id\"])\n",
" st[x[\"item\"]] += x[\"qty\"] if x[\"op\"] in (\"in\", \"adjust\") else -x[\"qty\"]\n",
" return st\n",
"\n",
"# 1) ๋ ์ฝ๋ ์คํค๋ง(ํ๋ 12๊ฐ): ์๋ฃํยทํ์ฉ๊ฐ/๋ฒ์ยท๋จ์ยท์ค๋ช
์ ๋ชจ๋ ์ ์\n",
"SCHEMA = {\n",
" \"id\": {\"type\": \"string\", \"pattern\": r\"^๋ณด๊ธ์[A-E]-\\d{5}$\", \"unit\": \"-\", \"desc\": \"ํธ๋์ญ์
ID(๋
ธ๋-์ผ๋ จ๋ฒํธ, ์ค๋ณต ์
๋ ฅ ๋ฐฉ์ง ํค)\"},\n",
" \"node\": {\"type\": \"string\", \"enum\": NODES, \"unit\": \"-\", \"desc\": \"๊ธฐ๋กํ ๊ฐ์ ๋ณด๊ธ์\"},\n",
" \"item\": {\"type\": \"string\", \"enum\": ITEM_IDS, \"unit\": \"-\", \"desc\": \"ํ๋ชฉ ๋ฒํธ(๊ฐ์)\"},\n",
" \"op\": {\"type\": \"string\", \"enum\": [\"in\", \"out\", \"adjust\"], \"unit\": \"-\", \"desc\": \"์
๊ณ ยท๋ถ์ถยท์กฐ์ \"},\n",
" \"qty\": {\"type\": \"integer\", \"min\": -50, \"max\": 5000, \"unit\": \"๊ฐ\", \"desc\": \"์๋(์กฐ์ ์ ์์ ํ์ฉ)\"},\n",
" \"t\": {\"type\": \"number\", \"min\": 0, \"max\": 24 * 30, \"unit\": \"์๊ฐ\", \"desc\": \"๋ฐ์ ์๊ฐ(๊ธฐ์ค ์์ ๋ถํฐ ๊ฒฝ๊ณผ ์๊ฐ)\"},\n",
" \"ctx\": {\"type\": \"string\", \"enum\": CTXS, \"unit\": \"-\", \"desc\": \"์
๋ ฅ ์ํฉ(ํ์ยท์ผ๊ฐยทํต์ ๋ถ๋)\"},\n",
" \"supplier\": {\"type\": \"string\", \"enum\": SUPPLIERS, \"unit\": \"-\", \"desc\": \"๊ฐ์ ์
์ฒด\"},\n",
" \"lamport\": {\"type\": \"integer\", \"min\": 0, \"max\": 10 ** 7, \"unit\": \"-\", \"desc\": \"๋
ผ๋ฆฌ ์๊ณ(๋
ธ๋๋ณ ์ฆ๊ฐ)\"},\n",
" \"sync_state\": {\"type\": \"string\", \"enum\": [\"local\", \"sent\", \"acked\"], \"unit\": \"-\", \"desc\": \"๋๊ธฐํ ์ํ\"},\n",
" \"checksum\": {\"type\": \"string\", \"pattern\": r\"^[0-9a-f]{8}$\", \"unit\": \"-\", \"desc\": \"์๋ณ์กฐ ์ ๊ฒ์ฉ ํด์ ์ 8์๋ฆฌ\"},\n",
" \"note\": {\"type\": \"string\", \"maxlen\": 40, \"unit\": \"-\", \"desc\": \"๋น๊ณ (์์ ๋ฌธ์ฅ, ๊ฐ์ธ ์ ๋ณด ์
๋ ฅ ๊ธ์ง)\"},\n",
"}\n",
"\n",
"\n",
"def seal(x, lamport):\n",
" body = f\"{x['id']}|{x['node']}|{x['item']}|{x['op']}|{x['qty']}|{x['t']}\"\n",
" return {**x, \"lamport\": lamport, \"sync_state\": \"local\", \"checksum\": hashlib.sha256(body.encode()).hexdigest()[:8], \"note\": \"\"}\n",
"\n",
"\n",
"def check(rec):\n",
" errs = []\n",
" for k, r in SCHEMA.items():\n",
" if k not in rec or rec[k] is None:\n",
" errs.append(f\"{k}: ํ์ ๊ฐ ๋๋ฝ\"); continue\n",
" v = rec[k]\n",
" ok = {\"string\": isinstance(v, str), \"integer\": isinstance(v, (int, np.integer)) and not isinstance(v, bool), \"number\": isinstance(v, (int, float, np.number)) and not isinstance(v, bool)}[r[\"type\"]]\n",
" if not ok:\n",
" errs.append(f\"{k}: ์๋ฃํ ์ค๋ฅ({type(v).__name__})\"); continue\n",
" if \"enum\" in r and v not in r[\"enum\"]:\n",
" errs.append(f\"{k}: ํ์ฉ๋์ง ์์ ๊ฐ '{v}'\")\n",
" if \"min\" in r and v < r[\"min\"]:\n",
" errs.append(f\"{k}: ์ต์๊ฐ {r['min']} ๋ฏธ๋ง\")\n",
" if \"max\" in r and v > r[\"max\"]:\n",
" errs.append(f\"{k}: ์ต๋๊ฐ {r['max']} ์ด๊ณผ\")\n",
" if \"pattern\" in r and not re.match(r[\"pattern\"], v):\n",
" errs.append(f\"{k}: ํ์ ๋ถ์ผ์น '{v}'\")\n",
" if \"maxlen\" in r and len(v) > r[\"maxlen\"]:\n",
" errs.append(f\"{k}: ๊ธธ์ด {r['maxlen']} ์ด๊ณผ\")\n",
" if not errs:\n",
" if rec[\"id\"].split(\"-\")[0] != rec[\"node\"]:\n",
" errs.append(\"id: ๋
ธ๋ ์ ๋์ด์ node ๋ถ์ผ์น\")\n",
" if rec[\"op\"] in (\"in\", \"out\") and rec[\"qty\"] <= 0:\n",
" errs.append(\"qty: ์
๊ณ ยท๋ถ์ถ ์๋์ 1 ์ด์\")\n",
" if re.search(r\"\\d{6}-\\d{7}|๊ตฐ๋ฒ|์ฃผ๋ฏผ\", rec[\"note\"]):\n",
" errs.append(\"note: ๊ฐ์ธ ์๋ณ ์ ๋ณด ์์ฌ ๋ฌธ๊ตฌ\")\n",
" return errs\n",
"\n",
"\n",
"txns = make_txns(7, 7)\n",
"recs = [seal(x, i + 1) for i, x in enumerate(txns)]\n",
"bad = sum(bool(check(r)) for r in recs)\n",
"print(f\"[์คํค๋ง] ํ๋ {len(SCHEMA)}๊ฐ, typeยทunitยทdesc ๋ชจ๋ ์ ์๋ ํ๋ {sum(all(k in v for k in ('type', 'unit', 'desc')) for v in SCHEMA.values())}๊ฐ (์ ์ ์์ฑ๋ 100%)\")\n",
"print(f\"[์ ์ ์๋ฃ ๊ฒ์ฆ] ํธ๋์ญ์
{len(recs)}๊ฑด ์ค ์ค๋ฅ {bad}๊ฑด (๋ชฉํ ์ค๋ฅ 0๊ฑด: {'๋ฌ์ฑ' if bad == 0 else '๋ฏธ๋ฌ'}), ๊ฐ์ธ ์๋ณ ํ๋ 0๊ฐ\")\n",
"\n",
"# 2) ์ค๋ฅ ์ฃผ์
์ํ\n",
"r0 = recs[10]\n",
"inj = {\"์๋ฃํ ์ค๋ฅ(qty ๋ฌธ์)\": dict(r0, qty=\"๋ง์ด\"), \"ํ์ฉ๊ฐ ์ค๋ฅ(ํ๋ชฉ)\": dict(r0, item=\"I99\"), \"ํ์ ์ค๋ฅ(ID)\": dict(r0, id=\"A-1\"), \"๋ฒ์ ์ค๋ฅ(์๊ฐ)\": dict(r0, t=-3.0),\n",
" \"๋
ผ๋ฆฌ ์ค๋ฅ(๋ถ์ถ 0๊ฐ)\": dict(r0, op=\"out\", qty=0), \"๋
ผ๋ฆฌ ์ค๋ฅ(ID ์ ๋์ด)\": dict(r0, node=\"๋ณด๊ธ์B\", id=\"๋ณด๊ธ์A-00011\"), \"ํ๋ ๋๋ฝ\": {k: v for k, v in r0.items() if k != \"checksum\"},\n",
" \"๊ฐ์ธ์ ๋ณด ์์ฌ ๋ฌธ๊ตฌ\": dict(r0, note=\"๊ตฐ๋ฒ ์
๋ ฅ\")}\n",
"print(\"\\n[์ค๋ฅ ์ฃผ์
์ํ]\")\n",
"caught = 0\n",
"for n, r in inj.items():\n",
" e = check(r); caught += bool(e); print(f\" {n:22s}-> {'๊ฒ์ถ' if e else '๋ฏธ๊ฒ์ถ'}: {e[0] if e else ''}\")\n",
"print(f\"์ฃผ์
์ค๋ฅ {len(inj)}์ข
์ค {caught}์ข
๊ฒ์ถ\")\n",
"\n",
"# 3) ๋ก์ปฌ DB ์คํค๋ง(SQLite ๋ฉ๋ชจ๋ฆฌ DB): ์ค๋ณต ํคยท์์ ์ฌ๊ณ ยท๋
ผ๋ฆฌ ๊ท์น์ DB๊ฐ ์ง์ ๋ง๋์ง ์ํ\n",
"DDL = \"\"\"\n",
"CREATE TABLE item (item TEXT PRIMARY KEY, cat TEXT NOT NULL, supplier TEXT NOT NULL, lead_days INTEGER CHECK (lead_days BETWEEN 1 AND 60), min_level INTEGER NOT NULL);\n",
"CREATE TABLE txn (id TEXT PRIMARY KEY, node TEXT NOT NULL, item TEXT NOT NULL REFERENCES item(item), op TEXT NOT NULL CHECK (op IN ('in','out','adjust')),\n",
" qty INTEGER NOT NULL, t REAL NOT NULL CHECK (t >= 0), ctx TEXT NOT NULL, lamport INTEGER NOT NULL, sync_state TEXT NOT NULL DEFAULT 'local',\n",
" checksum TEXT NOT NULL, CHECK (op = 'adjust' OR qty > 0));\n",
"CREATE TABLE meta_edit (id TEXT PRIMARY KEY, item TEXT NOT NULL REFERENCES item(item), field TEXT NOT NULL, value TEXT NOT NULL, node TEXT NOT NULL, lamport INTEGER NOT NULL);\n",
"CREATE TABLE conflict (id INTEGER PRIMARY KEY AUTOINCREMENT, item TEXT NOT NULL, field TEXT NOT NULL, a TEXT, b TEXT, status TEXT NOT NULL DEFAULT 'PENDING');\n",
"CREATE TABLE audit (seq INTEGER PRIMARY KEY AUTOINCREMENT, event TEXT NOT NULL, who TEXT, detail TEXT, prev_hash TEXT, hash TEXT);\n",
"CREATE VIEW stock AS SELECT item, SUM(CASE WHEN op='out' THEN -qty ELSE qty END) AS delta FROM txn GROUP BY item;\n",
"\"\"\"\n",
"db = sqlite3.connect(\":memory:\")\n",
"db.executescript(DDL)\n",
"db.executemany(\"INSERT INTO item VALUES (?,?,?,?,?)\", [(x[\"item\"], x[\"cat\"], x[\"supplier\"], x[\"lead_days\"], x[\"min_level\"]) for x in ITEMS])\n",
"with db:\n",
" db.executemany(\"INSERT INTO txn VALUES (?,?,?,?,?,?,?,?,?,?)\", [(r[\"id\"], r[\"node\"], r[\"item\"], r[\"op\"], r[\"qty\"], r[\"t\"], r[\"ctx\"], r[\"lamport\"], r[\"sync_state\"], r[\"checksum\"]) for r in recs])\n",
"n_rows = db.execute(\"SELECT COUNT(*) FROM txn\").fetchone()[0]\n",
"print(f\"\\n[๋ก์ปฌ DB] ํ
์ด๋ธ 5๊ฐยท๋ทฐ 1๊ฐ ์์ฑ, ํธ๋์ญ์
{n_rows}๊ฑด ์ ์ฌ (์คํค๋ง JSON๊ณผ ํ ์ ์ผ์น: {n_rows == len(recs)})\")\n",
"tests = {\"์ค๋ณต ID ์
๋ ฅ ์ฐจ๋จ(๋ฉฑ๋ฑ ํค)\": (\"INSERT INTO txn VALUES (?,?,?,?,?,?,?,?,?,?)\", (r0[\"id\"], r0[\"node\"], r0[\"item\"], \"in\", 5, 1.0, \"ํ์\", 1, \"local\", \"abcd1234\")),\n",
" \"์กด์ฌํ์ง ์๋ ํ๋ชฉ ์ฐจ๋จ(์ธ๋ ํค)\": (\"INSERT INTO txn VALUES ('๋ณด๊ธ์A-99999','๋ณด๊ธ์A','I99','in',5,1.0,'ํ์',1,'local','abcd1234')\", ()),\n",
" \"๋ถ์ถ 0๊ฐ ์ฐจ๋จ(CHECK)\": (\"INSERT INTO txn VALUES ('๋ณด๊ธ์A-99998','๋ณด๊ธ์A','I01','out',0,1.0,'ํ์',1,'local','abcd1234')\", ()),\n",
" \"์์ ์๊ฐ ์ฐจ๋จ(CHECK)\": (\"INSERT INTO txn VALUES ('๋ณด๊ธ์A-99997','๋ณด๊ธ์A','I01','in',5,-1.0,'ํ์',1,'local','abcd1234')\", ()),\n",
" \"ํ์ฉ๋์ง ์์ op ์ฐจ๋จ(CHECK)\": (\"INSERT INTO txn VALUES ('๋ณด๊ธ์A-99996','๋ณด๊ธ์A','I01','sell',5,1.0,'ํ์',1,'local','abcd1234')\", ())}\n",
"db.execute(\"PRAGMA foreign_keys = ON\")\n",
"blocked = 0\n",
"for n, (sql, args) in tests.items():\n",
" try:\n",
" with db:\n",
" db.execute(sql, args)\n",
" print(f\" {n:28s}-> ํต๊ณผ(๊ฒฐํจ)\")\n",
" except sqlite3.IntegrityError as e:\n",
" blocked += 1; print(f\" {n:28s}-> ์ฐจ๋จ: {str(e)[:48]}\")\n",
"print(f\"์ ์ฝ ์กฐ๊ฑด ์ํ {blocked}/{len(tests)} ์ฐจ๋จ, ๋กค๋ฐฑ ํ ํ ์ {db.execute('SELECT COUNT(*) FROM txn').fetchone()[0]}๊ฑด ์ ์ง\")\n",
"\n",
"# 4) ์๊ตฌ์ฌํญ 12๊ฐ์ ๊ฒ์ฆ ๊ฒฐ๊ณผ(์ฝ๋ ์ํ์ผ๋ก ํ์ธ๋ ํญ๋ชฉ์ PASS)\n",
"names = {r[0] for r in db.execute(\"SELECT name FROM sqlite_master\")}\n",
"cols = {r[1] for r in db.execute(\"PRAGMA table_info(txn)\")}\n",
"REQ = [(\"R01\", \"๊ฐ์ฉ์ฑ\", \"ํต์ ์ด ๋๊ฒจ๋ ๋จ๋ง ๋ก์ปฌ DB์ ์
๊ณ ยท๋ถ์ถ์ ๊ธฐ๋กํ ์ ์๋ค\", \"๋ฉ๋ชจ๋ฆฌ DB ์ ์ฌ\", n_rows == len(recs)), (\"R02\", \"๋ฌด๊ฒฐ์ฑ\", \"๊ฐ์ ์
๋ ฅ์ด ๋ ๋ฒ ๋ค์ด์๋ ํ ๋ฒ๋ง ๋ฐ์ํ๋ค\", \"ID ์ค๋ณต ์ฐจ๋จ\", blocked >= 1),\n",
" (\"R03\", \"๋ฌด๊ฒฐ์ฑ\", \"๋ถ์ถ ์๋ 0ยท์๋ชป๋ ๊ตฌ๋ถ ๊ฐ์ ์ ์ฅ๋์ง ์๋๋ค\", \"CHECK ์ ์ฝ\", blocked >= 3), (\"R04\", \"๋ฌด๊ฒฐ์ฑ\", \"์กด์ฌํ์ง ์๋ ํ๋ชฉ์ ์
๋ ฅํ ์ ์๋ค\", \"์ธ๋ ํค\", blocked >= 2),\n",
" (\"R05\", \"๋ฌด๊ฒฐ์ฑ\", \"๋ ์ฝ๋๋ง๋ค ์๋ณ์กฐ ์ ๊ฒ ํด์๋ฅผ ๋๋ค\", \"checksum ํ๋\", all(re.match(r\"^[0-9a-f]{8}$\", r[\"checksum\"]) for r in recs)), (\"R06\", \"๋๊ธฐํ\", \"๋๊ธฐํ ์ํ(local/sent/acked)๋ฅผ ๋ ์ฝ๋์ ๊ธฐ๋กํ๋ค\", \"sync_state ํ๋\", \"sync_state\" in cols),\n",
" (\"R07\", \"๋๊ธฐํ\", \"๋
ธ๋๋ณ ๋
ผ๋ฆฌ ์๊ณ๋ก ์ ํ ๊ด๊ณ๋ฅผ ๋น๊ตํ๋ค\", \"lamport ํ๋\", all(r[\"lamport\"] > 0 for r in recs)), (\"R08\", \"๋๊ธฐํ\", \"๊ฐ์ ํ๋๋ฅผ ๋์์ ๊ณ ์น ์ถฉ๋์ ๋ณด๋ฅ ๋ชฉ๋ก์ ๋จ๊ธด๋ค\", \"conflict ํ
์ด๋ธ\", \"conflict\" in names),\n",
" (\"R09\", \"๋ณด์\", \"๋ณ๊ฒฝยท๋๊ธฐํยท์น์ธ ํ์๋ ํด์๊ฐ ์ด์ด์ง ๊ฐ์ฌ ๋ก๊ทธ์ ๋จ๊ธด๋ค\", \"audit ํ
์ด๋ธ\", \"audit\" in names), (\"R10\", \"๋ณด์\", \"๊ฐ์ธ ์๋ณ ์ ๋ณด๋ ์
๋ ฅํ์ง ์๋๋ค\", \"note ์ ๊ฒ\", caught == len(inj)),\n",
" (\"R11\", \"์ด์\", \"์
๋ ฅ ์ค๋ฅ๋ ์ฌ์ ์ ํจ๊ป ์ฆ์ ์๋ ค ์ค๋ค\", \"๊ฒ์ฆ ์ค๋ฅ ๋ฉ์์ง\", caught >= 6), (\"R12\", \"์ด์\", \"๋๊ธฐํ ํ ์ฌ๊ณ ๋ฅผ ์
์ฒดยทํ๋ชฉ๋ณ๋ก ๋ง์ด๋ํด ๊ณต๊ธ๋ง ์ฌ๋ ์งํ๋ฅผ ์๋ณํ๋ค\", \"stock ๋ทฐ\", db.execute(\"SELECT COUNT(*) FROM stock\").fetchone()[0] == 12)]\n",
"print(\"\\n[์๊ตฌ์ฌํญ ๊ฒ์ฆํ]\")\n",
"for i, c, d, m, ok in REQ:\n",
" print(f\" {i} [{c}] {d[:34]:36s} {m:12s} {'PASS' if ok else 'FAIL'}\")\n",
"print(f\"์๊ตฌ์ฌํญ {sum(x[4] for x in REQ)}/{len(REQ)} ์ถฉ์กฑ\")\n",
"js = json.dumps({\"title\": \"dat10_input_schema\", \"fields\": {k: {kk: vv for kk, vv in v.items() if kk != 'desc'} for k, v in list(SCHEMA.items())[:2]}}, ensure_ascii=False)\n",
"print(\"์คํค๋ง JSON(์ 2๊ฐ ํ๋):\", js[:140], \"...\")\n",
"print(\"[์
์ถ๋ ฅ] ์
๋ ฅ: ๋ณด๊ธ์ ํธ๋์ญ์
(12ํ๋) / ์ถ๋ ฅ: ๋๊ธฐํ๋ ์ฌ๊ณ , ์ถฉ๋ ๋ชฉ๋ก, ๊ณต๊ธ๋ง ์ฌ๋ ์งํ, ๊ฐ์ฌ ๋ก๊ทธ\")\n",
"print(\"โป ๊ฐ์ ์๋ฃ์ด๋ฉฐ ์ค์ ์ฌ๊ณ ยท์
์ฒด ์ ๋ณด๋ ์ฌ์ฉํ์ง ์๋๋ค. ์ค์ ๋จ๋ง DB๋ ์ํธํ ์ ์ฅ ๋ฑ ๋ณด์ ์๊ฑด์ ๋ณ๋๋ก ์ถฉ์กฑํด์ผ ํ๋ค.\")\n",
"\n",
"st = stock_of(txns)\n",
"plt.figure(figsize=(8, 3.8)); plt.bar(list(st), list(st.values()), color=\"tab:blue\"); plt.title(\"Synthetic stock by item after 7 days (all nodes merged)\"); plt.xlabel(\"item\"); plt.ylabel(\"units\"); plt.tight_layout(); plt.show()\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"๐ก [์ฐธ๊ณ ] ๋ฏธ์
01 ์์ ์คํ ๊ฒฐ๊ณผ (Output)
\n",
"\n",
"```text\n",
"[์คํค๋ง] ํ๋ 12๊ฐ, typeยทunitยทdesc ๋ชจ๋ ์ ์๋ ํ๋ 12๊ฐ (์ ์ ์์ฑ๋ 100%)\n",
"[์ ์ ์๋ฃ ๊ฒ์ฆ] ํธ๋์ญ์
2100๊ฑด ์ค ์ค๋ฅ 0๊ฑด (๋ชฉํ ์ค๋ฅ 0๊ฑด: ๋ฌ์ฑ), ๊ฐ์ธ ์๋ณ ํ๋ 0๊ฐ\n",
"\n",
"[์ค๋ฅ ์ฃผ์
์ํ]\n",
" ์๋ฃํ ์ค๋ฅ(qty ๋ฌธ์) -> ๊ฒ์ถ: qty: ์๋ฃํ ์ค๋ฅ(str)\n",
" ํ์ฉ๊ฐ ์ค๋ฅ(ํ๋ชฉ) -> ๊ฒ์ถ: item: ํ์ฉ๋์ง ์์ ๊ฐ 'I99'\n",
" ํ์ ์ค๋ฅ(ID) -> ๊ฒ์ถ: id: ํ์ ๋ถ์ผ์น 'A-1'\n",
" ๋ฒ์ ์ค๋ฅ(์๊ฐ) -> ๊ฒ์ถ: t: ์ต์๊ฐ 0 ๋ฏธ๋ง\n",
" ๋
ผ๋ฆฌ ์ค๋ฅ(๋ถ์ถ 0๊ฐ) -> ๊ฒ์ถ: qty: ์
๊ณ ยท๋ถ์ถ ์๋์ 1 ์ด์\n",
" ๋
ผ๋ฆฌ ์ค๋ฅ(ID ์ ๋์ด) -> ๊ฒ์ถ: id: ๋
ธ๋ ์ ๋์ด์ node ๋ถ์ผ์น\n",
" ํ๋ ๋๋ฝ -> ๊ฒ์ถ: checksum: ํ์ ๊ฐ ๋๋ฝ\n",
" ๊ฐ์ธ์ ๋ณด ์์ฌ ๋ฌธ๊ตฌ -> ๊ฒ์ถ: note: ๊ฐ์ธ ์๋ณ ์ ๋ณด ์์ฌ ๋ฌธ๊ตฌ\n",
"์ฃผ์
์ค๋ฅ 8์ข
์ค 8์ข
๊ฒ์ถ\n",
"\n",
"[๋ก์ปฌ DB] ํ
์ด๋ธ 5๊ฐยท๋ทฐ 1๊ฐ ์์ฑ, ํธ๋์ญ์
2100๊ฑด ์ ์ฌ (์คํค๋ง JSON๊ณผ ํ ์ ์ผ์น: True)\n",
" ์ค๋ณต ID ์
๋ ฅ ์ฐจ๋จ(๋ฉฑ๋ฑ ํค) -> ์ฐจ๋จ: UNIQUE constraint failed: txn.id\n",
" ์กด์ฌํ์ง ์๋ ํ๋ชฉ ์ฐจ๋จ(์ธ๋ ํค) -> ์ฐจ๋จ: FOREIGN KEY constraint failed\n",
" ๋ถ์ถ 0๊ฐ ์ฐจ๋จ(CHECK) -> ์ฐจ๋จ: CHECK constraint failed: op = 'adjust' OR qty > \n",
" ์์ ์๊ฐ ์ฐจ๋จ(CHECK) -> ์ฐจ๋จ: CHECK constraint failed: t >= 0\n",
" ํ์ฉ๋์ง ์์ op ์ฐจ๋จ(CHECK) -> ์ฐจ๋จ: CHECK constraint failed: op IN ('in','out','adju\n",
"์ ์ฝ ์กฐ๊ฑด ์ํ 5/5 ์ฐจ๋จ, ๋กค๋ฐฑ ํ ํ ์ 2100๊ฑด ์ ์ง\n",
"\n",
"[์๊ตฌ์ฌํญ ๊ฒ์ฆํ]\n",
" R01 [๊ฐ์ฉ์ฑ] ํต์ ์ด ๋๊ฒจ๋ ๋จ๋ง ๋ก์ปฌ DB์ ์
๊ณ ยท๋ถ์ถ์ ๊ธฐ๋กํ ์ ์๋ค ๋ฉ๋ชจ๋ฆฌ DB ์ ์ฌ PASS\n",
" R02 [๋ฌด๊ฒฐ์ฑ] ๊ฐ์ ์
๋ ฅ์ด ๋ ๋ฒ ๋ค์ด์๋ ํ ๋ฒ๋ง ๋ฐ์ํ๋ค ID ์ค๋ณต ์ฐจ๋จ PASS\n",
" R03 [๋ฌด๊ฒฐ์ฑ] ๋ถ์ถ ์๋ 0ยท์๋ชป๋ ๊ตฌ๋ถ ๊ฐ์ ์ ์ฅ๋์ง ์๋๋ค CHECK ์ ์ฝ PASS\n",
" R04 [๋ฌด๊ฒฐ์ฑ] ์กด์ฌํ์ง ์๋ ํ๋ชฉ์ ์
๋ ฅํ ์ ์๋ค ์ธ๋ ํค PASS\n",
" R05 [๋ฌด๊ฒฐ์ฑ] ๋ ์ฝ๋๋ง๋ค ์๋ณ์กฐ ์ ๊ฒ ํด์๋ฅผ ๋๋ค checksum ํ๋ PASS\n",
" R06 [๋๊ธฐํ] ๋๊ธฐํ ์ํ(local/sent/acked)๋ฅผ ๋ ์ฝ๋์ ๊ธฐ๋กํ sync_state ํ๋ PASS\n",
" R07 [๋๊ธฐํ] ๋
ธ๋๋ณ ๋
ผ๋ฆฌ ์๊ณ๋ก ์ ํ ๊ด๊ณ๋ฅผ ๋น๊ตํ๋ค lamport ํ๋ PASS\n",
" R08 [๋๊ธฐํ] ๊ฐ์ ํ๋๋ฅผ ๋์์ ๊ณ ์น ์ถฉ๋์ ๋ณด๋ฅ ๋ชฉ๋ก์ ๋จ๊ธด๋ค conflict ํ
์ด๋ธ PASS\n",
" R09 [๋ณด์] ๋ณ๊ฒฝยท๋๊ธฐํยท์น์ธ ํ์๋ ํด์๊ฐ ์ด์ด์ง ๊ฐ์ฌ ๋ก๊ทธ์ ๋จ๊ธด๋ค audit ํ
์ด๋ธ PASS\n",
" R10 [๋ณด์] ๊ฐ์ธ ์๋ณ ์ ๋ณด๋ ์
๋ ฅํ์ง ์๋๋ค note ์ ๊ฒ PASS\n",
" R11 [์ด์] ์
๋ ฅ ์ค๋ฅ๋ ์ฌ์ ์ ํจ๊ป ์ฆ์ ์๋ ค ์ค๋ค ๊ฒ์ฆ ์ค๋ฅ ๋ฉ์์ง PASS\n",
" R12 [์ด์] ๋๊ธฐํ ํ ์ฌ๊ณ ๋ฅผ ์
์ฒดยทํ๋ชฉ๋ณ๋ก ๋ง์ด๋ํด ๊ณต๊ธ๋ง ์ฌ๋ ์งํ๋ฅผ stock ๋ทฐ PASS\n",
"์๊ตฌ์ฌํญ 12/12 ์ถฉ์กฑ\n",
"์คํค๋ง JSON(์ 2๊ฐ ํ๋): {\"title\": \"dat10_input_schema\", \"fields\": {\"id\": {\"type\": \"string\", \"pattern\": \"^๋ณด๊ธ์[A-E]-\\\\d{5}$\", \"unit\": \"-\"}, \"node\": {\"type\": \"string\", ...\n",
"[์
์ถ๋ ฅ] ์
๋ ฅ: ๋ณด๊ธ์ ํธ๋์ญ์
(12ํ๋) / ์ถ๋ ฅ: ๋๊ธฐํ๋ ์ฌ๊ณ , ์ถฉ๋ ๋ชฉ๋ก, ๊ณต๊ธ๋ง ์ฌ๋ ์งํ, ๊ฐ์ฌ ๋ก๊ทธ\n",
"โป ๊ฐ์ ์๋ฃ์ด๋ฉฐ ์ค์ ์ฌ๊ณ ยท์
์ฒด ์ ๋ณด๋ ์ฌ์ฉํ์ง ์๋๋ค. ์ค์ ๋จ๋ง DB๋ ์ํธํ ์ ์ฅ ๋ฑ ๋ณด์ ์๊ฑด์ ๋ณ๋๋ก ์ถฉ์กฑํด์ผ ํ๋ค.\n",
"```\n",
" \n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## ๐ฏ ๋ฏธ์
02. ์คํ๋ผ์ธ ๋ฐ์ดํฐ ์
๋ ฅ ์ ํจ์ฑ ๊ฒ์ฆ ๋ฐ ๋ก์ปฌ SQLite ํธ๋์ญ์
์ ์ฒ๋ฆฌ ๋ชจ๋ ๊ตฌํ\n",
"**์์์๊ฐ**: ์ฝ 2.0์๊ฐ\n",
"\n",
"### ๐ ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ์ํ ์ง์นจ\n",
"#### ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ๋น์์ฑ\n",
"\n",
"์คํ๋ผ์ธ ๋ฐ์ดํฐ ์
๋ ฅ ์ ํจ์ฑ ๊ฒ์ฆ ๋ฐ ๋ก์ปฌ ํธ๋์ญ์
์ ์ฒ๋ฆฌ ๋ชจ๋์ ๊ตฌํํ๋ค. ์ค์ ์ ์ ํ์ฅ์์๋ ๋จ์ ํ์ง์ ๊ทธ์น์ง ์๊ณ ์ ์ํ ์ํ ํ๋จ๊ณผ ์ ๋ขฐ๋ ๋์ ์์ฌ๊ฒฐ์ ์ด ํ์์ ์ด๋ฏ๋ก, **ํ์์ด ์์ธ ์คํ๋ผ์ธ ์
๋ ฅ ์ค์ ํ์ฑยท๊ฒ์ฆํ๊ณ SQLite ํธ๋์ญ์
์ผ๋ก ์์ ํ๊ฒ ์ ์ฌํ๋ ์ ์ฒ๋ฆฌ ๋ชจ๋์ ๊ตฌํํด ์ฒ๋ฆฌ ์๋ 1,000๊ฑด/์ด ์ด์, ์ ์ฒ๋ฆฌ ์ค๋ฅ์จ 0%๋ฅผ ํ์ธํ๋ค.** ๊ณผ์
์ ์์ํ์ฌ ์คํ์ ๋ฐฉ์งํ๊ณ ์์ ๋์๋ ฅ์ ๊ทน๋ํํด์ผ ํฉ๋๋ค.\n",
"\n",
"์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ณธ ๋ฏธ์
์์๋ **ID\\ ๊ณผ์ ์ ์งํํ๋** ์ค์ต์ ์ฒด๊ณ์ ์ผ๋ก ์ํํฉ๋๋ค.\n",
"\n",
"ํ์ต์๋ ์ค์ต๊ฐ์ด๋์ ์ ์๋ ์ด 4๊ฐ ์ธ๋ถ ํ๋(๊ธฐ๋ณธ ๊ธฐ๋ฅ ๊ตฌํ โ ๊ฒฐ๊ณผ ์งํ ๊ฒ์ฆ โ ํ๋ผ๋ฏธํฐ ํ๋ ๋ฐ ๋์ ๊ณผ์ )์ ์์ฐจ์ ์ผ๋ก ์งํํ๋ฉฐ ํต์ฌ ํ์ ๊ธฐ์ค์ ๋ฌ์ฑํฉ๋๋ค.\n",
"\n",
"**์ด๋ฒ ๊ฒฐ๊ณผ๋ฌผ:** `src/preprocess/dat10_preprocessor.py ๋ฐ ์ ์ฒ๋ฆฌ ์ ํ ๋ฐ์ดํฐ์
` ํํ์ ๊ฒฐ๊ณผ๋ฌผ์ ์์ฑํ๋ค.\n",
"\n",
"### ๐ ํ๊ฐ ๋ฐ ๊ฒ์ฆ ๊ธฐ์ค (PASS ์กฐ๊ฑด)\n",
"#### ๋ฏธ์
ํ์ \n",
"\n",
"**ํ๊ฐ ๊ตฌ๋ถ:** ํ์\n",
"\n",
"**ํต์ฌ ํ๊ฐ์์ญ:** ์ ์ ํ๊ฒฝ ํน์ ์ ๋
ธ์ด์ฆ(์คํธํ, ๋ฐ์ฌํ, ์ผ์ ๋จ์ ๋ฑ) ์ ๊ฑฐ ํ๋น์ฑ\n",
"\n",
"#### ๋ฏธ์
PASS ์กฐ๊ฑด\n",
"\n",
"- ์ ๋ ๊ธฐ์ค: ๋ฐ์ดํฐ ์ ์ ์ฒ๋ฆฌ ์๋ 1,000๊ฑด/์ด ์ด์, ์ ์ฒ๋ฆฌ ์ค๋ฅ์จ 0%\n",
"\n",
"\n",
"- ์งํ ๊ธฐ์ค: ์ ์ ํ์ดํ๋ผ์ธ ์๋ํ ๋ฐ ๋จ์ ํ
์คํธ ์ผ์ด์ค 5์ข
ํต๊ณผ\n",
"\n",
"\n",
"- ๊ฒ์ฆ ๋ฐฉ๋ฒ: pytest ๊ธฐ๋ฐ ์ ์ฒ๋ฆฌ ์ ํฉ์ฑ ํ
์คํธ ๋ฐ ๊ฒฐ์ธก์น ๋ณด์ ์จ ํ์ธ\n",
"\n",
"\n",
"#### ์ฑ์ ์ ํ์ ๋ฉ๋ชจ\n",
"\n",
"> ๋จ์ ์ฝ๋ ์คํ ์ฌ๋ถ๋ฟ๋ง ์๋๋ผ ์ถ๋ ฅ๋ ์์น์ ๊ทผ๊ฑฐ ์๋ฃ๊ฐ ๊ตฐ ์ค๋ฌด ์๊ตฌ์ฌํญ์ ๋ถํฉํ๋์ง ๋ฉด๋ฐํ ํ์ธํ๋ค.\n",
"\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"\"\"\"์์์ด ์๋ก ๋ค๋ฅธ ์คํ๋ผ์ธ ์
๋ ฅ ๋ฌธ์์ด 8,000์ค(๋ฌธ์ฅํยทCSVยทJSONยทํค=๊ฐ 4์ข
)์ ํ์ฑยท๊ฒ์ฆํด SQLite ๋ฉ๋ชจ๋ฆฌ DB์ ๋ฐฐ์น ํธ๋์ญ์
์ผ๋ก ์ ์ฌํ๊ณ , ์
๋ ฅ ๊ตฌ๋ถ(์
๊ณ ยท๋ถ์ถยท์กฐ์ )๋ณ ๋์ ์๊ณ๊ฐ์ผ๋ก ์ด์ ์๋์ ๊ฒฉ๋ฆฌํ๋ฉฐ ์ค๋ณต ์ฌ์ ์ก ๋ฉฑ๋ฑ ์ฒ๋ฆฌยท๋ฐฐ์น ์คํจ ๋กค๋ฐฑยท์ฒ๋ฆฌ ์๋(๊ฑด/์ด)ยท์ ๋ต ๋์กฐ ์ค๋ฅ์จ์ ์ธก์ ํ๋ค.\"\"\"\n",
"import json\n",
"import re\n",
"import sqlite3\n",
"import time\n",
"import numpy as np\n",
"import pandas as pd\n",
"import matplotlib.pyplot as plt\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# [๊ต์ก์ฉ๊ฐ์์๋ฃ] ๊ฐ์ ์ผ์ ๋ณด๊ธ์ยท์
์ฒดยทํ๋ชฉ ์๋ฃ ์์ฑ๊ธฐ (์ค์ ๋ถ๋ยท์
์ฒดยทํ๋ชฉยท์ฌ๊ณ ์ ๋ณด ์์)\n",
"# ๊ฐ์ ๋ณด๊ธ์ 5๊ณณ์ด ํ๋ชฉ 12์ข
์ ์
๊ณ ยท๋ถ์ถยท์กฐ์ ํธ๋์ญ์
์ ์คํ๋ผ์ธ์ผ๋ก ๊ธฐ๋กํ๊ณ ๋์ค์ ๋๊ธฐํํ๋ค.\n",
"# ๊ณต๊ธ๋ง ์ฌ๋ ์งํ(์
์ฒด ๋ฉ๊ธฐ ์ง์ฐ, ํ๋ชฉ ์ฌ๊ณ ๊ธ๊ฐ)๋ ๋๊ธฐํ๋ ์๋ฃ๋ฅผ ๋ง์ด๋ํด ์๋ณํ๋ค.\n",
"# ---------------------------------------------------------------------------\n",
"NODES = [\"๋ณด๊ธ์A\", \"๋ณด๊ธ์B\", \"๋ณด๊ธ์C\", \"๋ณด๊ธ์D\", \"๋ณด๊ธ์E\"]\n",
"SUPPLIERS = [\"๊ฐ์์
์ฒด1\", \"๊ฐ์์
์ฒด2\", \"๊ฐ์์
์ฒด3\", \"๊ฐ์์
์ฒด4\", \"๊ฐ์์
์ฒด5\", \"๊ฐ์์
์ฒด6\"]\n",
"CATS = [\"์ผ๋ฐ๋ณด๊ธ\", \"์ ๋น๋ถํ\", \"์๋ฌด์๋ชจ\"]\n",
"ITEMS = [{\"item\": f\"I{i + 1:02d}\", \"cat\": CATS[i % 3], \"supplier\": SUPPLIERS[i % 6], \"lead_days\": 3 + (i * 5) % 12, \"use\": 4 + (i * 7) % 11, \"min_level\": 30 + 10 * (i % 5)} for i in range(12)]\n",
"ITEM_IDS = [x[\"item\"] for x in ITEMS]\n",
"CTXS = [\"ํ์\", \"์ผ๊ฐ\", \"ํต์ ๋ถ๋\"]\n",
"\n",
"\n",
"def make_txns(seed=7, days=7, per_node_day=60, nodes=None):\n",
" \"\"\"๋
ธ๋๋ณ ์คํ๋ผ์ธ ํธ๋์ญ์
๋ชฉ๋ก. id๋ ๋
ธ๋-์ผ๋ จ๋ฒํธ(๋ฉฑ๋ฑ ํค), t๋ ์๊ฐ(์๊ฐ ๋จ์), ctx๋ ์
๋ ฅ ์ํฉ.\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" out = []\n",
" for n in (nodes or NODES):\n",
" seq = 0\n",
" for k in range(days * per_node_day):\n",
" seq += 1\n",
" it = ITEMS[int(rng.integers(len(ITEMS)))]\n",
" t = float(rng.uniform(0, 24 * days))\n",
" ctx = \"์ผ๊ฐ\" if (t % 24 >= 20 or t % 24 < 5) else \"ํ์\"\n",
" if rng.random() < 0.12:\n",
" ctx = \"ํต์ ๋ถ๋\"\n",
" r = rng.random()\n",
" if r < 0.22:\n",
" op, q = \"in\", int(it[\"use\"] * rng.uniform(4, 9))\n",
" elif r < 0.95:\n",
" op, q = \"out\", int(max(1, rng.normal(it[\"use\"] * 1.5, 3)))\n",
" else:\n",
" op, q = \"adjust\", int(rng.integers(-5, 6))\n",
" out.append({\"id\": f\"{n}-{seq:05d}\", \"node\": n, \"item\": it[\"item\"], \"op\": op, \"qty\": q, \"t\": round(t, 2), \"ctx\": ctx, \"supplier\": it[\"supplier\"]})\n",
" return out\n",
"\n",
"\n",
"def stock_of(txns, base=500):\n",
" \"\"\"ํ๋ชฉ๋ณ ์ฌ๊ณ = ๊ธฐ์ด ์ฌ๊ณ + ์
๊ณ - ๋ถ์ถ + ์กฐ์ (id ์ค๋ณต์ ํ ๋ฒ๋ง ๋ฐ์).\"\"\"\n",
" st, seen = {i: base for i in ITEM_IDS}, set()\n",
" for x in txns:\n",
" if x[\"id\"] in seen:\n",
" continue\n",
" seen.add(x[\"id\"])\n",
" st[x[\"item\"]] += x[\"qty\"] if x[\"op\"] in (\"in\", \"adjust\") else -x[\"qty\"]\n",
" return st\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# [๊ต์ก์ฉ๊ฐ์์๋ฃ] ์คํ๋ผ์ธ ์
๋ ฅ ์๋ฌธ ์์ฑ: ๊ฐ์ ํธ๋์ญ์
์ ์์ 4์ข
์ค ํ๋๋ก ์ ๊ณ , ์ก์(์ด์ ์๋ยทํ์ ๊นจ์งยท์ฌ์ ์ก ์ค๋ณต)์ ์๋๋ค.\n",
"# ---------------------------------------------------------------------------\n",
"rng = np.random.default_rng(21)\n",
"base = make_txns(31, 7, per_node_day=230) # 5๋
ธ๋ x 7์ผ x 230๊ฑด = 8,050๊ฑด\n",
"OPK = {\"in\": \"์
๊ณ \", \"out\": \"๋ถ์ถ\", \"adjust\": \"์กฐ์ \"}\n",
"lines, truth = [], {}\n",
"for i, x in enumerate(base):\n",
" f = i % 4\n",
" if f == 0:\n",
" s = f\"[์
๋ ฅ] {x['node']} ํ๋ชฉ{x['item']} {OPK[x['op']]} {x['qty']}๊ฐ ({x['ctx']}) {x['t']}h #{x['id']}\"\n",
" elif f == 1:\n",
" s = f\"{x['node']},{x['item']},{x['op']},{x['qty']},{x['t']},{x['ctx']},{x['id']}\"\n",
" elif f == 2:\n",
" s = json.dumps({k: x[k] for k in (\"node\", \"item\", \"op\", \"qty\", \"t\", \"ctx\", \"id\")}, ensure_ascii=False)\n",
" else:\n",
" s = f\"ํ๋ชฉ {x['item']} / {OPK[x['op']]} / {x['qty']}๊ฐ / {x['node']} / t={x['t']} / ctx={x['ctx']} / id={x['id']}\"\n",
" lines.append(s); truth[x[\"id\"]] = x\n",
"noise_ids = set()\n",
"for i in rng.choice(len(lines), 120, replace=False): # ์ก์ ์ฃผ์
: ์ด์ ์๋ 40, ๊นจ์ง ์ค 40, ์กด์ฌํ์ง ์๋ ํ๋ชฉ 40\n",
" x = base[i]\n",
" if i % 3 == 0:\n",
" lines[i] = lines[i].replace(str(x[\"qty\"]), \"9999\", 1) if x[\"op\"] != \"adjust\" else lines[i]; noise_ids.add(x[\"id\"]) if \"9999\" in lines[i] else None\n",
" elif i % 3 == 1:\n",
" lines[i] = lines[i][: len(lines[i]) // 2]; noise_ids.add(x[\"id\"])\n",
" else:\n",
" lines[i] = lines[i].replace(x[\"item\"], \"I99\"); noise_ids.add(x[\"id\"])\n",
"dups = [lines[i] for i in rng.choice(len(lines), 200, replace=False)] # ํต์ ๋ณต๊ตฌ ํ ์ฌ์ ์ก๋ ์ค๋ณต ์ค 200๊ฐ\n",
"lines = lines + dups\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# ํ์: ์์๋ณ ์ ๊ท์ -> ํ์ค ๋ ์ฝ๋ (ํด์ ์คํจ๋ ์์ธ)\n",
"# ---------------------------------------------------------------------------\n",
"IV = {v: k for k, v in OPK.items()}\n",
"P1 = re.compile(r\"\\[์
๋ ฅ\\]\\s*(๋ณด๊ธ์[A-E])\\s*ํ๋ชฉ(I\\d{2})\\s*(์
๊ณ |๋ถ์ถ|์กฐ์ )\\s*(-?\\d+)๊ฐ\\s*\\((\\S+)\\)\\s*([\\d.]+)h\\s*#(\\S+)\")\n",
"P4 = re.compile(r\"ํ๋ชฉ\\s*(I\\d{2})\\s*/\\s*(์
๊ณ |๋ถ์ถ|์กฐ์ )\\s*/\\s*(-?\\d+)๊ฐ\\s*/\\s*(๋ณด๊ธ์[A-E])\\s*/\\s*t=([\\d.]+)\\s*/\\s*ctx=(\\S+)\\s*/\\s*id=(\\S+)\")\n",
"\n",
"\n",
"def parse(s):\n",
" s = s.strip()\n",
" if s.startswith(\"[\"):\n",
" m = P1.match(s)\n",
" if m:\n",
" return {\"node\": m[1], \"item\": m[2], \"op\": IV[m[3]], \"qty\": int(m[4]), \"ctx\": m[5], \"t\": float(m[6]), \"id\": m[7]}\n",
" elif s.startswith(\"{\"):\n",
" return json.loads(s)\n",
" elif s.startswith(\"ํ๋ชฉ\"):\n",
" m = P4.match(s)\n",
" if m:\n",
" return {\"item\": m[1], \"op\": IV[m[2]], \"qty\": int(m[3]), \"node\": m[4], \"t\": float(m[5]), \"ctx\": m[6], \"id\": m[7]}\n",
" else:\n",
" p = s.split(\",\")\n",
" if len(p) == 7:\n",
" return {\"node\": p[0], \"item\": p[1], \"op\": p[2], \"qty\": int(p[3]), \"t\": float(p[4]), \"ctx\": p[5], \"id\": p[6]}\n",
" raise ValueError(\"์์ ํด์ ๋ถ๊ฐ\")\n",
"\n",
"\n",
"def valid(r):\n",
" if r[\"item\"] not in ITEM_IDS or r[\"node\"] not in NODES or r[\"op\"] not in OPK or r[\"ctx\"] not in CTXS:\n",
" return \"ํ์ฉ๊ฐ ์ค๋ฅ\"\n",
" if r[\"op\"] != \"adjust\" and r[\"qty\"] <= 0:\n",
" return \"์๋ ์ค๋ฅ\"\n",
" if not re.match(r\"^๋ณด๊ธ์[A-E]-\\d{5}$\", r[\"id\"]) or not r[\"id\"].startswith(r[\"node\"]):\n",
" return \"ID ์ค๋ฅ\"\n",
" return None\n",
"\n",
"\n",
"# 1) ํ์ฑ + ๊ฒ์ฆ + ๋์ ์๊ณ๊ฐ\n",
"t0 = time.perf_counter()\n",
"parsed, rejected = [], []\n",
"for s in lines:\n",
" try:\n",
" r = parse(s); e = valid(r)\n",
" (rejected if e else parsed).append((r, e) if e else r)\n",
" except (ValueError, KeyError, json.JSONDecodeError, IndexError) as ex:\n",
" rejected.append((s, \"ํด์ ์คํจ\"))\n",
"df = pd.DataFrame(parsed)\n",
"before = len(df)\n",
"df = df.drop_duplicates(\"id\")\n",
"th = {}\n",
"for op, g in df[df[\"op\"] != \"adjust\"].groupby(\"op\"):\n",
" med = g[\"qty\"].median(); mad = (g[\"qty\"] - med).abs().median() * 1.4826; th[op] = (max(med - 6 * mad, 1), med + 6 * mad)\n",
"fixed = (df[\"op\"] != \"adjust\") & (df[\"qty\"] > 100)\n",
"dyn = pd.Series([(op != \"adjust\") and not (th[op][0] <= q <= th[op][1]) for op, q in zip(df[\"op\"], df[\"qty\"])], index=df.index)\n",
"susp = dyn.to_numpy()\n",
"clean = df[~dyn].copy()\n",
"dt_parse = time.perf_counter() - t0\n",
"rej_cnt = pd.Series([e for _, e in rejected]).value_counts().to_dict()\n",
"print(f\"[ํ์ฑยท๊ฒ์ฆ] ์
๋ ฅ {len(lines)}์ค(์ฌ์ ์ก ์ค๋ณต 200 ํฌํจ): ํด์ ์คํจยทํ์ฉ๊ฐ ์ค๋ฅ {len(rejected)}์ค {rej_cnt}, ํต๊ณผ {before}์ค, ์ค๋ณต ID {before - len(df)}์ค ์ ๊ฑฐ\")\n",
"print(\"๊ตฌ๋ถ๋ณ ๋์ ์๊ณ๊ฐ(์๋):\", \", \".join(f\"{OPK[k]} {lo:.0f}~{hi:.0f}\" for k, (lo, hi) in th.items()))\n",
"true_bad = df[\"id\"].isin(noise_ids).to_numpy()\n",
"for name, fl in [(\"๊ณ ์ ์๊ณ๊ฐ(100๊ฐ ์ด๊ณผ)\", fixed.to_numpy()), (\"๊ตฌ๋ถ๋ณ ๋์ ์๊ณ๊ฐ\", susp)]:\n",
" print(f\" {name:20s}: ์ด์ ํ์ {int(fl.sum())}๊ฑด, ์ด์ ์๋ {int((fl & true_bad).sum())}/{int(true_bad.sum())} ๊ฒ์ถ, ์ ์ ์
๊ณ ์ธ๋ฐ ์ด์ ํ์ (์คํ) {int((fl & ~true_bad).sum())}๊ฑด\")\n",
"\n",
"# 2) SQLite ๋ฐฐ์น ํธ๋์ญ์
์ ์ฌ(๋ฉฑ๋ฑ: ๊ฐ์ ID๋ ํ ๋ฒ๋ง), ๋ฐฐ์น ์คํจ ์ ๋กค๋ฐฑ ํ ํ ๋จ์๋ก ๊ฒฉ๋ฆฌ\n",
"db = sqlite3.connect(\":memory:\")\n",
"db.execute(\"CREATE TABLE txn (id TEXT PRIMARY KEY, node TEXT NOT NULL, item TEXT NOT NULL, op TEXT NOT NULL CHECK (op IN ('in','out','adjust')), qty INTEGER NOT NULL, t REAL NOT NULL CHECK (t >= 0), ctx TEXT NOT NULL)\")\n",
"db.execute(\"CREATE TABLE quarantine (id TEXT, reason TEXT)\")\n",
"rows = [(r[\"id\"], r[\"node\"], r[\"item\"], r[\"op\"], int(r[\"qty\"]), float(r[\"t\"]), r[\"ctx\"]) for r in clean.to_dict(\"records\")]\n",
"rows[777] = (rows[777][0], rows[777][1], rows[777][2], \"sell\", 5, 1.0, \"ํ์\") # ๋ฐฐ์น ์์ ์ ์ฝ ์๋ฐ 1๊ฑด ์ฃผ์
\n",
"t1 = time.perf_counter(); inserted = rolled = 0\n",
"for b in range(0, len(rows), 500):\n",
" batch = rows[b:b + 500]\n",
" try:\n",
" with db:\n",
" db.executemany(\"INSERT INTO txn VALUES (?,?,?,?,?,?,?) ON CONFLICT(id) DO NOTHING\", batch)\n",
" inserted += len(batch)\n",
" except sqlite3.IntegrityError:\n",
" rolled += 1 # ๋ฐฐ์น ์ ์ฒด ๋กค๋ฐฑ ํ ํ ๋จ์ ์ฌ์๋\n",
" for r in batch:\n",
" try:\n",
" with db:\n",
" db.execute(\"INSERT INTO txn VALUES (?,?,?,?,?,?,?) ON CONFLICT(id) DO NOTHING\", r)\n",
" inserted += 1\n",
" except sqlite3.IntegrityError as e:\n",
" with db:\n",
" db.execute(\"INSERT INTO quarantine VALUES (?,?)\", (r[0], \"์ ์ฝ ์๋ฐ\"))\n",
"dt_db = time.perf_counter() - t1\n",
"n_db = db.execute(\"SELECT COUNT(*) FROM txn\").fetchone()[0]\n",
"print(f\"\\n[SQLite ์ ์ฌ] 500๊ฑด ๋ฐฐ์น {len(rows) // 500 + 1}๊ฐ: ๋กค๋ฐฑ {rolled}ํ(์ ์ฝ ์๋ฐ 1๊ฑด -> ํ ๋จ์ ๊ฒฉ๋ฆฌ), ์ ์ฌ {n_db}๊ฑด, ๊ฒฉ๋ฆฌ ํ
์ด๋ธ {db.execute('SELECT COUNT(*) FROM quarantine').fetchone()[0]}๊ฑด\")\n",
"db.execute(\"INSERT INTO txn VALUES (?,?,?,?,?,?,?) ON CONFLICT(id) DO NOTHING\", rows[0]); n2 = db.execute(\"SELECT COUNT(*) FROM txn\").fetchone()[0]\n",
"print(f\"๊ฐ์ ID๋ฅผ ๋ค์ ๋ฃ์ด๋ ํ ์ {n2}๊ฑด ์ ์ง(๋ฉฑ๋ฑ)\")\n",
"\n",
"# 3) ์ ๋ต ๋์กฐ: ์ ์ฌ๋ ํ์ ํ๋๊ฐ ์๋ณธ ํธ๋์ญ์
๊ณผ ๊ฐ์๊ฐ, ์๋\n",
"errs = 0; chk = 0\n",
"for (i, node, item, op, qty, t, ctx) in db.execute(\"SELECT * FROM txn\"):\n",
" x = truth[i]; chk += 1\n",
" errs += (x[\"node\"], x[\"item\"], x[\"op\"], x[\"qty\"], x[\"ctx\"]) != (node, item, op, qty, ctx) or abs(x[\"t\"] - t) > 1e-6\n",
"total = dt_parse + dt_db\n",
"print(f\"\\n[์ ์ฒ๋ฆฌ ์ ํ๋] ์ ์ฌ {chk}๊ฑด ์ค ์๋ณธ๊ณผ ๋ถ์ผ์น {errs}๊ฑด -> ์ ์ฒ๋ฆฌ ์ค๋ฅ์จ {errs / chk:.2%} (๋ชฉํ 0%: {'๋ฌ์ฑ' if errs == 0 else '๋ฏธ๋ฌ'})\")\n",
"print(f\"์ฒ๋ฆฌ ์๋ {len(lines) / total:,.0f}๊ฑด/์ด (ํ์ฑยท๊ฒ์ฆ {dt_parse:.2f}์ด + DB ์ ์ฌ {dt_db:.2f}์ด, ๋ชฉํ 1,000๊ฑด/์ด ์ด์: {'๋ฌ์ฑ' if len(lines) / total >= 1000 else '๋ฏธ๋ฌ'})\")\n",
"print(f\"์๋ณธ {len(base)}๊ฑด ์ค ์ก์ ์ฃผ์
{len(noise_ids)}๊ฑด๊ณผ ๊ฒฉ๋ฆฌ {int(susp.sum())}๊ฑด์ ์ ์ธํ๊ณ ์ ์ฌ {n_db}๊ฑด -> ๊ฒฉ๋ฆฌ๋ ๊ฑด์ ๋ด๋น์๊ฐ ์๋ณธ ํ์ธ ํ ์ฌ์
๋ ฅ\")\n",
"print(\"โป ์ด์ ํ์ ๋ ์ค์ ์ญ์ ํ์ง ์๊ณ ๊ฒฉ๋ฆฌํ๋ฉฐ, ์๊ณ๊ฐ์ ์
๋ ฅ ๊ตฌ๋ถ๋ณ ์ค์๊ฐยฑ6MAD๋ก ์๋ฃ์์ ์กฐ์ ํ๋ค. ์ค์ ์์์ ๋ง๋ ์ ๊ท์์ ํ์ฅ์์ ๋ณด์ํ๋ค.\")\n",
"\n",
"plt.figure(figsize=(8, 4))\n",
"for op, g in df[df[\"op\"] != \"adjust\"].groupby(\"op\"):\n",
" plt.hist(g[\"qty\"].clip(upper=200), bins=40, alpha=.5, label=op)\n",
"plt.axvline(100, color=\"k\", ls=\"--\"); plt.legend(); plt.xlabel(\"qty\"); plt.title(\"Quantity by operation (dashed: fixed threshold 100)\"); plt.tight_layout(); plt.show()\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"๐ก [์ฐธ๊ณ ] ๋ฏธ์
02 ์์ ์คํ ๊ฒฐ๊ณผ (Output)
\n",
"\n",
"```text\n",
"[ํ์ฑยท๊ฒ์ฆ] ์
๋ ฅ 8250์ค(์ฌ์ ์ก ์ค๋ณต 200 ํฌํจ): ํด์ ์คํจยทํ์ฉ๊ฐ ์ค๋ฅ 84์ค {'ํ์ฉ๊ฐ ์ค๋ฅ': 44, 'ํด์ ์คํจ': 40}, ํต๊ณผ 8166์ค, ์ค๋ณต ID 198์ค ์ ๊ฑฐ\n",
"๊ตฌ๋ถ๋ณ ๋์ ์๊ณ๊ฐ(์๋): ์
๊ณ 1~221, ๋ถ์ถ 1~56\n",
" ๊ณ ์ ์๊ณ๊ฐ(100๊ฐ ์ด๊ณผ) : ์ด์ ํ์ 139๊ฑด, ์ด์ ์๋ 34/34 ๊ฒ์ถ, ์ ์ ์
๊ณ ์ธ๋ฐ ์ด์ ํ์ (์คํ) 105๊ฑด\n",
" ๊ตฌ๋ถ๋ณ ๋์ ์๊ณ๊ฐ : ์ด์ ํ์ 34๊ฑด, ์ด์ ์๋ 34/34 ๊ฒ์ถ, ์ ์ ์
๊ณ ์ธ๋ฐ ์ด์ ํ์ (์คํ) 0๊ฑด\n",
"\n",
"[SQLite ์ ์ฌ] 500๊ฑด ๋ฐฐ์น 16๊ฐ: ๋กค๋ฐฑ 1ํ(์ ์ฝ ์๋ฐ 1๊ฑด -> ํ ๋จ์ ๊ฒฉ๋ฆฌ), ์ ์ฌ 7933๊ฑด, ๊ฒฉ๋ฆฌ ํ
์ด๋ธ 1๊ฑด\n",
"๊ฐ์ ID๋ฅผ ๋ค์ ๋ฃ์ด๋ ํ ์ 7933๊ฑด ์ ์ง(๋ฉฑ๋ฑ)\n",
"\n",
"[์ ์ฒ๋ฆฌ ์ ํ๋] ์ ์ฌ 7933๊ฑด ์ค ์๋ณธ๊ณผ ๋ถ์ผ์น 0๊ฑด -> ์ ์ฒ๋ฆฌ ์ค๋ฅ์จ 0.00% (๋ชฉํ 0%: ๋ฌ์ฑ)\n",
"์ฒ๋ฆฌ ์๋ 48,683๊ฑด/์ด (ํ์ฑยท๊ฒ์ฆ 0.13์ด + DB ์ ์ฌ 0.04์ด, ๋ชฉํ 1,000๊ฑด/์ด ์ด์: ๋ฌ์ฑ)\n",
"์๋ณธ 8050๊ฑด ์ค ์ก์ ์ฃผ์
116๊ฑด๊ณผ ๊ฒฉ๋ฆฌ 34๊ฑด์ ์ ์ธํ๊ณ ์ ์ฌ 7933๊ฑด -> ๊ฒฉ๋ฆฌ๋ ๊ฑด์ ๋ด๋น์๊ฐ ์๋ณธ ํ์ธ ํ ์ฌ์
๋ ฅ\n",
"โป ์ด์ ํ์ ๋ ์ค์ ์ญ์ ํ์ง ์๊ณ ๊ฒฉ๋ฆฌํ๋ฉฐ, ์๊ณ๊ฐ์ ์
๋ ฅ ๊ตฌ๋ถ๋ณ ์ค์๊ฐยฑ6MAD๋ก ์๋ฃ์์ ์กฐ์ ํ๋ค. ์ค์ ์์์ ๋ง๋ ์ ๊ท์์ ํ์ฅ์์ ๋ณด์ํ๋ค.\n",
"```\n",
" \n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## ๐ฏ ๋ฏธ์
03. ์คํ๋ผ์ธ ๋จ๋ง ๊ฐ 1์ฐจ ํผ์ดํฌํผ์ด(P2P)/USB ์ฌ๊ณ ๋ฐ์ดํฐ ๊ตํ Baseline ๊ธฐ๋ฅ ๊ตฌํ\n",
"**์์์๊ฐ**: ์ฝ 2.5์๊ฐ\n",
"\n",
"### ๐ ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ์ํ ์ง์นจ\n",
"#### ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ๋น์์ฑ\n",
"\n",
"์คํ๋ผ์ธ ๋จ๋ง ๊ฐ ํผ์ดํฌํผ์ด(P2P) ์ฌ๊ณ ๋ฐ์ดํฐ ๊ตํ Baseline ๊ธฐ๋ฅ์ ๊ตฌํํ๋ค. ์ค์ ์ ์ ํ์ฅ์์๋ ๋จ์ ํ์ง์ ๊ทธ์น์ง ์๊ณ ์ ์ํ ์ํ ํ๋จ๊ณผ ์ ๋ขฐ๋ ๋์ ์์ฌ๊ฒฐ์ ์ด ํ์์ ์ด๋ฏ๋ก, **๋ณด๊ธ์ ๋จ๋ง 5๊ณณ์ด ์ ์ดํ ๋ USBยทP2P๋ก ์ฌ๊ณ ํธ๋์ญ์
์ ์ฃผ๊ณ ๋ฐ๋ Baseline ๊ตํ ๊ธฐ๋ฅ์ ๋ง๋ค๊ณ 7์ผ ํ ์ ๋ณด ๊ณต์ ์ ํ๋ 75% ์ด์์ ํ์ธํ๋ค.** ๊ณผ์
์ ์์ํ์ฌ ์คํ์ ๋ฐฉ์งํ๊ณ ์์ ๋์๋ ฅ์ ๊ทน๋ํํด์ผ ํฉ๋๋ค.\n",
"\n",
"์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ณธ ๋ฏธ์
์์๋ **7์ผ๊ฐ ์ ์ด 56ํ(์ ์ด๋น ๋ฐฉํฅ๋ณ ์ต๋ 80๊ฑด, ์์ค 5%)์ ๊ฐ์ ์ผ์ ์์ ์๋ ์ํ๋ฅผ ๋ชจ๋ฅด๋ ์ ์ฒด ์ ์ก, ID ์ฐจ์งํฉ ๋ธํ ์ ์ก, ๋ธํ์ ์ฐ์ ์์(์ค๋๋ ๊ฒ ๋จผ์ )๋ฅผ ๋ํ ๋ฐฉ์์ ๋น๊ต ์ํํ๊ณ ๊ฒ์ฆํ๋** ์ค์ต์ ์ฒด๊ณ์ ์ผ๋ก ์ํํฉ๋๋ค.\n",
"\n",
"ํ์ต์๋ ์ค์ต๊ฐ์ด๋์ ์ ์๋ ์ด 4๊ฐ ์ธ๋ถ ํ๋(๊ธฐ๋ณธ ๊ธฐ๋ฅ ๊ตฌํ โ ๊ฒฐ๊ณผ ์งํ ๊ฒ์ฆ โ ํ๋ผ๋ฏธํฐ ํ๋ ๋ฐ ๋์ ๊ณผ์ )์ ์์ฐจ์ ์ผ๋ก ์งํํ๋ฉฐ ํต์ฌ ํ์ ๊ธฐ์ค์ ๋ฌ์ฑํฉ๋๋ค.\n",
"\n",
"**์ด๋ฒ ๊ฒฐ๊ณผ๋ฌผ:** `src/models/dat10_baseline_model.py ๋ฐ ๊ธฐ๋ณธ ์ถ๋ก ๊ฒฐ๊ณผ ๋ณด๊ณ ์` ํํ์ ๊ฒฐ๊ณผ๋ฌผ์ ์์ฑํ๋ค.\n",
"\n",
"### ๐ ํ๊ฐ ๋ฐ ๊ฒ์ฆ ๊ธฐ์ค (PASS ์กฐ๊ฑด)\n",
"#### ๋ฏธ์
ํ์ \n",
"\n",
"**ํ๊ฐ ๊ตฌ๋ถ:** ํ์\n",
"\n",
"**ํต์ฌ ํ๊ฐ์์ญ:** ์๊ณ ๋ฆฌ์ฆ์ ๋
ผ๋ฆฌ์ ํ๋ฆ ๋ฐ ๊ตฐ์ฌ์ ํ๋จ ๊ธฐ์ค์ ํฌ๋ช
์ฑ\n",
"\n",
"#### ๋ฏธ์
PASS ์กฐ๊ฑด\n",
"\n",
"- ์ ๋ ๊ธฐ์ค: ๊ธฐ๋ณธ ๋ฐ์ดํฐ์
๋์ ํ์ง/์ฒ๋ฆฌ ์ ํ๋ 75% ์ด์ ๋ฌ์ฑ\n",
"\n",
"\n",
"- ์งํ ๊ธฐ์ค: ๋จ์ ๊ธฐ๋ฅ ๋์ ํ์ธ ๋ฐ ๊ธฐ๋ณธ ์๋๋ฆฌ์ค ์คํ ๋ก๊ทธ ์์ฑ\n",
"\n",
"\n",
"- ๊ฒ์ฆ ๋ฐฉ๋ฒ: ๊ธฐ์ค ๋ฒค์น๋งํฌ ๋ฐ์ดํฐ์
๋์ ์ถ๋ก ๊ฒฐ๊ณผ ์ค์ฐจ์จ ๋น๊ต\n",
"\n",
"\n",
"#### ์ฑ์ ์ ํ์ ๋ฉ๋ชจ\n",
"\n",
"> ๋จ์ ์ฝ๋ ์คํ ์ฌ๋ถ๋ฟ๋ง ์๋๋ผ ์ถ๋ ฅ๋ ์์น์ ๊ทผ๊ฑฐ ์๋ฃ๊ฐ ๊ตฐ ์ค๋ฌด ์๊ตฌ์ฌํญ์ ๋ถํฉํ๋์ง ๋ฉด๋ฐํ ํ์ธํ๋ค.\n",
"\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"\"\"\"์คํ๋ผ์ธ ๋ณด๊ธ์ 5๊ณณ์ด ๋๊ธด ํต์ ๋์ ์งง์ P2P ์ ์ดยทUSB ์ธํธ์ผ๋ก ํธ๋์ญ์
์ ๊ตํํ๋ Baseline์ ๊ตฌํํด, ์ ์ฒด ์ ์กยทID ์ฐจ์งํฉ(๋ธํ) ์ ์กยท๋ธํ+์ฐ์ ์์ ํด๋ฆฌ์คํฑ(๋ด ๊ธฐ๋ก ์ฐ์ ยท์ค๋๋ ๊ฒ ์ฐ์ )์ 7์ผ ํ ๋๊ธฐํ ์์ ์ฑยท์ ์ก ํจ์จยท์๋ ด ์์ ์ ๋น๊ตํ๊ณ ์ฐ์ ์์ ๊ฐ์ค์น๋ฅผ 1์ฐจ ํ๋ํ๋ค.\"\"\"\n",
"import itertools\n",
"import numpy as np\n",
"import pandas as pd\n",
"import matplotlib.pyplot as plt\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# [๊ต์ก์ฉ๊ฐ์์๋ฃ] ๊ฐ์ ์ผ์ ๋ณด๊ธ์ยท์
์ฒดยทํ๋ชฉ ์๋ฃ ์์ฑ๊ธฐ (์ค์ ๋ถ๋ยท์
์ฒดยทํ๋ชฉยท์ฌ๊ณ ์ ๋ณด ์์)\n",
"# ๊ฐ์ ๋ณด๊ธ์ 5๊ณณ์ด ํ๋ชฉ 12์ข
์ ์
๊ณ ยท๋ถ์ถยท์กฐ์ ํธ๋์ญ์
์ ์คํ๋ผ์ธ์ผ๋ก ๊ธฐ๋กํ๊ณ ๋์ค์ ๋๊ธฐํํ๋ค.\n",
"# ๊ณต๊ธ๋ง ์ฌ๋ ์งํ(์
์ฒด ๋ฉ๊ธฐ ์ง์ฐ, ํ๋ชฉ ์ฌ๊ณ ๊ธ๊ฐ)๋ ๋๊ธฐํ๋ ์๋ฃ๋ฅผ ๋ง์ด๋ํด ์๋ณํ๋ค.\n",
"# ---------------------------------------------------------------------------\n",
"NODES = [\"๋ณด๊ธ์A\", \"๋ณด๊ธ์B\", \"๋ณด๊ธ์C\", \"๋ณด๊ธ์D\", \"๋ณด๊ธ์E\"]\n",
"SUPPLIERS = [\"๊ฐ์์
์ฒด1\", \"๊ฐ์์
์ฒด2\", \"๊ฐ์์
์ฒด3\", \"๊ฐ์์
์ฒด4\", \"๊ฐ์์
์ฒด5\", \"๊ฐ์์
์ฒด6\"]\n",
"CATS = [\"์ผ๋ฐ๋ณด๊ธ\", \"์ ๋น๋ถํ\", \"์๋ฌด์๋ชจ\"]\n",
"ITEMS = [{\"item\": f\"I{i + 1:02d}\", \"cat\": CATS[i % 3], \"supplier\": SUPPLIERS[i % 6], \"lead_days\": 3 + (i * 5) % 12, \"use\": 4 + (i * 7) % 11, \"min_level\": 30 + 10 * (i % 5)} for i in range(12)]\n",
"ITEM_IDS = [x[\"item\"] for x in ITEMS]\n",
"CTXS = [\"ํ์\", \"์ผ๊ฐ\", \"ํต์ ๋ถ๋\"]\n",
"\n",
"\n",
"def make_txns(seed=7, days=7, per_node_day=60, nodes=None):\n",
" \"\"\"๋
ธ๋๋ณ ์คํ๋ผ์ธ ํธ๋์ญ์
๋ชฉ๋ก. id๋ ๋
ธ๋-์ผ๋ จ๋ฒํธ(๋ฉฑ๋ฑ ํค), t๋ ์๊ฐ(์๊ฐ ๋จ์), ctx๋ ์
๋ ฅ ์ํฉ.\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" out = []\n",
" for n in (nodes or NODES):\n",
" seq = 0\n",
" for k in range(days * per_node_day):\n",
" seq += 1\n",
" it = ITEMS[int(rng.integers(len(ITEMS)))]\n",
" t = float(rng.uniform(0, 24 * days))\n",
" ctx = \"์ผ๊ฐ\" if (t % 24 >= 20 or t % 24 < 5) else \"ํ์\"\n",
" if rng.random() < 0.12:\n",
" ctx = \"ํต์ ๋ถ๋\"\n",
" r = rng.random()\n",
" if r < 0.22:\n",
" op, q = \"in\", int(it[\"use\"] * rng.uniform(4, 9))\n",
" elif r < 0.95:\n",
" op, q = \"out\", int(max(1, rng.normal(it[\"use\"] * 1.5, 3)))\n",
" else:\n",
" op, q = \"adjust\", int(rng.integers(-5, 6))\n",
" out.append({\"id\": f\"{n}-{seq:05d}\", \"node\": n, \"item\": it[\"item\"], \"op\": op, \"qty\": q, \"t\": round(t, 2), \"ctx\": ctx, \"supplier\": it[\"supplier\"]})\n",
" return out\n",
"\n",
"\n",
"def stock_of(txns, base=500):\n",
" \"\"\"ํ๋ชฉ๋ณ ์ฌ๊ณ = ๊ธฐ์ด ์ฌ๊ณ + ์
๊ณ - ๋ถ์ถ + ์กฐ์ (id ์ค๋ณต์ ํ ๋ฒ๋ง ๋ฐ์).\"\"\"\n",
" st, seen = {i: base for i in ITEM_IDS}, set()\n",
" for x in txns:\n",
" if x[\"id\"] in seen:\n",
" continue\n",
" seen.add(x[\"id\"])\n",
" st[x[\"item\"]] += x[\"qty\"] if x[\"op\"] in (\"in\", \"adjust\") else -x[\"qty\"]\n",
" return st\n",
"\n",
"TX = make_txns(7, 7, per_node_day=60) # 5๋
ธ๋ x 7์ผ x 60๊ฑด = 2,100๊ฑด\n",
"N = len(TX); IDX = {x[\"id\"]: i for i, x in enumerate(TX)}\n",
"ORIGIN = np.array([NODES.index(x[\"node\"]) for x in TX]); T = np.array([x[\"t\"] for x in TX])\n",
"CONTACTS_PER_DAY, BW, LOSS = 8, 80, 0.05 # ํ๋ฃจ ์ ์ด 8ํ, ์ ์ด๋น ๋ฐฉํฅ๋ณ ์ต๋ 60๊ฑด, ์ ์ก ์์ค 5%\n",
"\n",
"\n",
"def contact_schedule(seed=3, days=7):\n",
" r = np.random.default_rng(seed); out = []\n",
" for d in range(days):\n",
" for _ in range(CONTACTS_PER_DAY):\n",
" a, b = r.choice(5, 2, replace=False); out.append((d * 24 + float(r.uniform(0, 24)), int(a), int(b), r.random()))\n",
" return sorted(out)\n",
"\n",
"\n",
"def simulate(method, w_own=0.0, w_age=0.0, seed=3, trace=False):\n",
" r = np.random.default_rng(seed + 100)\n",
" known = [set() for _ in range(5)]; sent = useful = 0; tr = []\n",
" prod = [np.where(ORIGIN == n)[0] for n in range(5)]\n",
" for tm, a, b, _ in contact_schedule(seed):\n",
" for n in (a, b): # ์ ์ด ์์ ๊น์ง ์๊ธฐ ๋
ธ๋๊ฐ ๊ธฐ๋กํ ํธ๋์ญ์
\n",
" known[n] |= set(prod[n][T[prod[n]] <= tm].tolist())\n",
" for src, dst in ((a, b), (b, a), ):\n",
" if method == \"full\":\n",
" cand = sorted(known[src], key=lambda i: T[i]) # ์๋๊ฐ ๋ญ ๊ฐ์ก๋์ง ๋ชจ๋ฅด๊ณ ์ ๋ถ ๋ณด๋(์์์๋ถํฐ BW๊ฑด)\n",
" else:\n",
" cand = [i for i in known[src] if i not in known[dst]] # ์๋๊ฐ ์๋ ๊ฒ๋ง(ID ์์ฝ ๋น๊ต)\n",
" if method == \"prio\":\n",
" cand.sort(key=lambda i: -(w_own * (ORIGIN[i] == src) + w_age * (tm - T[i]) / 168))\n",
" else:\n",
" cand.sort(key=lambda i: T[i])\n",
" for i in cand[:BW]:\n",
" sent += 1\n",
" if r.random() > LOSS:\n",
" useful += i not in known[dst]; known[dst].add(i)\n",
" if trace:\n",
" tr.append((tm, np.mean([len(k) for k in known]) / N))\n",
" comp = float(np.mean([len(k) for k in known]) / N)\n",
" return {\"comp\": comp, \"sent\": sent, \"useful\": useful, \"eff\": useful / max(sent, 1), \"full_nodes\": sum(len(k) == N for k in known), \"trace\": tr}\n",
"\n",
"\n",
"# 1) ๋ฐฉ์ ๋น๊ต\n",
"res = {\"์ ์ฒด ์ ์ก(์๋ ์ํ ๋ชจ๋ฆ)\": simulate(\"full\"), \"ID ์ฐจ์งํฉ ๋ธํ(์ค๋๋ ๊ฒ ๋จผ์ )\": simulate(\"delta\"), \"๋ธํ + ์ฐ์ ์์(๊ธฐ๋ณธ ๊ฐ์ค own 1, age 0)\": simulate(\"prio\", 1, 0)}\n",
"print(f\"[์กฐ๊ฑด] ๋ณด๊ธ์ 5๊ณณ, ํธ๋์ญ์
{N}๊ฑด, 7์ผ๊ฐ ์ ์ด {CONTACTS_PER_DAY * 7}ํ(P2PยทUSB), ์ ์ด๋น ๋ฐฉํฅ๋ณ ์ต๋ {BW}๊ฑด, ์์ค {LOSS:.0%}\")\n",
"print(f\"{'๋ฐฉ์':32s} {'์์ ์ฑ':>7s} {'์ ์ก':>6s} {'์ ํจ ์ ์ก':>8s} {'ํจ์จ':>6s}\")\n",
"for k, v in res.items():\n",
" print(f\"{k:32s} {v['comp']:7.1%} {v['sent']:6d} {v['useful']:8d} {v['eff']:6.1%}\")\n",
"\n",
"# 2) 1์ฐจ ํ๋: ์ฐ์ ์์ ๊ฐ์ค์น ๊ฒฉ์ ํ์(์ค์ผ์ค ์๋ 3๊ฐ ํ๊ท , ์ํ์ ๋ค๋ฅธ ์๋ 3๊ฐ)\n",
"grid = {}\n",
"for wo, wa in itertools.product((0, 1, 2, 4), (-1, 0, 1)):\n",
" grid[(wo, wa)] = np.mean([simulate(\"prio\", wo, wa, seed=s)[\"comp\"] for s in (1, 2, 4)])\n",
"best = max(grid, key=grid.get)\n",
"print(f\"\\n[์ฐ์ ์์ ๊ฐ์ค์น ํ์(ํ๋ ์๋ 3๊ฐ ํ๊ท ์์ ์ฑ)] ๋ด ๊ธฐ๋ก ๊ฐ์ค x ์ค๋๋จ ๊ฐ์ค\")\n",
"print(\" \" + \" \".join(f\"own{wo}/age{wa}:{v:.1%}\" for (wo, wa), v in list(grid.items())[:6]) + \" ...\")\n",
"print(f\"์ฑํ: own {best[0]}, age {best[1]} (ํ๋ ํ๊ท {grid[best]:.1%})\")\n",
"test = {m: np.mean([simulate(m, *( (best if m == 'prio' else (0, 0)) ), seed=s)[\"comp\"] for s in (5, 6, 7)]) for m in (\"full\", \"delta\", \"prio\")}\n",
"print(f\"์ํ ์๋ 3๊ฐ ํ๊ท ์์ ์ฑ: ์ ์ฒด ์ ์ก {test['full']:.1%}, ๋ธํ {test['delta']:.1%}, ๋ธํ+ํ๋ ์ฐ์ ์์ {test['prio']:.1%}\")\n",
"acc = test[\"prio\"]\n",
"print(f\"๊ธฐ๋ณธ ์ ํ๋(7์ผ ํ ๋ชจ๋ ๋
ธ๋๊ฐ ์ ์ฒด ํธ๋์ญ์
์ค ์๊ณ ์๋ ๋น์จ) {acc:.1%} (๋ชฉํ 75% ์ด์: {'๋ฌ์ฑ' if acc >= 0.75 else '๋ฏธ๋ฌ'})\")\n",
"\n",
"# 3) ์๋ ด ์ถ์ ๊ณผ ํ๋จ ์ง์ฐ\n",
"import time\n",
"t0 = time.perf_counter(); r = simulate(\"prio\", *best, trace=True); dt = time.perf_counter() - t0\n",
"tr = r[\"trace\"]; day = next((t / 24 for t, c in tr if c >= 0.9), None)\n",
"print(f\"\\n[์๋ ด] 7์ผ ์๋ฎฌ๋ ์ด์
{dt * 1000:.0f}ms (์ ์ด 1ํ๋น {dt / len(tr) * 1000:.1f}ms), ์์ ์ฑ 90% ๋๋ฌ ์์ {('%.1f์ผ' % day) if day else '7์ผ ๋ด ๋ฏธ๋๋ฌ'}, ๋ชจ๋ ํธ๋์ญ์
์ ๊ฐ์ง ๋
ธ๋ {r['full_nodes']}/5\")\n",
"print(\"์ ์ด์ด ๋ถ์กฑํ๋ฉด ์์ ์ฑ์ด 100%์ ๋ชป ๋ฏธ์น๋ฏ๋ก, ๋๊ธฐํ ์ ์ฌ๊ณ ๋ 'ํ์ธ๋ ๋ฒ์' ํ์๊ฐ ํ์ํ๋ค(M6).\")\n",
"print(\"โป ๊ฐ์ ์ ์ด ์ผ์ ๊ณผ ๋์ญํญ ๊ฐ์ ์ ๋ฐ๋ฅธ ๊ฒฐ๊ณผ์ด๋ฉฐ, ์ค์ ์ ์ด ๋น๋ยทUSB ์ด๋์ ํ์ฅ ์กฐ๊ฑด์ ๋ฐ๋ผ ๋ค๋ฅด๋ค. ๋ณํฉ ์ถฉ๋ ํ๋จ์ M4ยทM7์์ ๋ค๋ฃฌ๋ค.\")\n",
"\n",
"plt.figure(figsize=(8, 4))\n",
"for m, lab in ((\"full\", \"full dump\"), (\"delta\", \"delta\"), (\"prio\", \"delta + priority\")):\n",
" rr = simulate(m, *(best if m == \"prio\" else (0, 0)), trace=True)[\"trace\"]; plt.plot([t / 24 for t, _ in rr], [c * 100 for _, c in rr], label=lab)\n",
"plt.xlabel(\"days\"); plt.ylabel(\"mean completeness (%)\"); plt.title(\"P2P/USB exchange: completeness over time\"); plt.legend(); plt.tight_layout(); plt.show()\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"๐ก [์ฐธ๊ณ ] ๋ฏธ์
03 ์์ ์คํ ๊ฒฐ๊ณผ (Output)
\n",
"\n",
"```text\n",
"[์กฐ๊ฑด] ๋ณด๊ธ์ 5๊ณณ, ํธ๋์ญ์
2100๊ฑด, 7์ผ๊ฐ ์ ์ด 56ํ(P2PยทUSB), ์ ์ด๋น ๋ฐฉํฅ๋ณ ์ต๋ 80๊ฑด, ์์ค 5%\n",
"๋ฐฉ์ ์์ ์ฑ ์ ์ก ์ ํจ ์ ์ก ํจ์จ\n",
"์ ์ฒด ์ ์ก(์๋ ์ํ ๋ชจ๋ฆ) 22.8% 8470 398 4.7%\n",
"ID ์ฐจ์งํฉ ๋ธํ(์ค๋๋ ๊ฒ ๋จผ์ ) 82.5% 6999 6657 95.1%\n",
"๋ธํ + ์ฐ์ ์์(๊ธฐ๋ณธ ๊ฐ์ค own 1, age 0) 83.7% 7143 6791 95.1%\n",
"\n",
"[์ฐ์ ์์ ๊ฐ์ค์น ํ์(ํ๋ ์๋ 3๊ฐ ํ๊ท ์์ ์ฑ)] ๋ด ๊ธฐ๋ก ๊ฐ์ค x ์ค๋๋จ ๊ฐ์ค\n",
" own0/age-1:84.8% own0/age0:83.7% own0/age1:82.7% own1/age-1:84.7% own1/age0:84.4% own1/age1:84.0% ...\n",
"์ฑํ: own 0, age -1 (ํ๋ ํ๊ท 84.8%)\n",
"์ํ ์๋ 3๊ฐ ํ๊ท ์์ ์ฑ: ์ ์ฒด ์ ์ก 24.0%, ๋ธํ 82.1%, ๋ธํ+ํ๋ ์ฐ์ ์์ 84.6%\n",
"๊ธฐ๋ณธ ์ ํ๋(7์ผ ํ ๋ชจ๋ ๋
ธ๋๊ฐ ์ ์ฒด ํธ๋์ญ์
์ค ์๊ณ ์๋ ๋น์จ) 84.6% (๋ชฉํ 75% ์ด์: ๋ฌ์ฑ)\n",
"\n",
"[์๋ ด] 7์ผ ์๋ฎฌ๋ ์ด์
58ms (์ ์ด 1ํ๋น 1.0ms), ์์ ์ฑ 90% ๋๋ฌ ์์ 7์ผ ๋ด ๋ฏธ๋๋ฌ, ๋ชจ๋ ํธ๋์ญ์
์ ๊ฐ์ง ๋
ธ๋ 0/5\n",
"์ ์ด์ด ๋ถ์กฑํ๋ฉด ์์ ์ฑ์ด 100%์ ๋ชป ๋ฏธ์น๋ฏ๋ก, ๋๊ธฐํ ์ ์ฌ๊ณ ๋ 'ํ์ธ๋ ๋ฒ์' ํ์๊ฐ ํ์ํ๋ค(M6).\n",
"โป ๊ฐ์ ์ ์ด ์ผ์ ๊ณผ ๋์ญํญ ๊ฐ์ ์ ๋ฐ๋ฅธ ๊ฒฐ๊ณผ์ด๋ฉฐ, ์ค์ ์ ์ด ๋น๋ยทUSB ์ด๋์ ํ์ฅ ์กฐ๊ฑด์ ๋ฐ๋ผ ๋ค๋ฅด๋ค. ๋ณํฉ ์ถฉ๋ ํ๋จ์ M4ยทM7์์ ๋ค๋ฃฌ๋ค.\n",
"```\n",
" \n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## ๐ฏ ๋ฏธ์
04. ๋คํธ์ํฌ ๋ณต๊ตฌ ์ ํด๋ผ์ฐ๋ ์ค์ ์๋ฒ์์ ์๋ฐฉํฅ ๋ฐ์ดํฐ ์ถฉ๋ ํด๊ฒฐ(CRDT) ๋๊ธฐํ ๊ณ ๋ํ\n",
"**์์์๊ฐ**: ์ฝ 3.0์๊ฐ\n",
"\n",
"### ๐ ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ์ํ ์ง์นจ\n",
"#### ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ๋น์์ฑ\n",
"\n",
"๋คํธ์ํฌ ๋ณต๊ตฌ ์ ์ค์ ์๋ฒ์์ ์๋ฐฉํฅ ๋ฐ์ดํฐ ์ถฉ๋ ํด๊ฒฐ(CRDT) ๋๊ธฐํ๋ฅผ ๊ณ ๋ํํ๋ค. ์ค์ ์ ์ ํ์ฅ์์๋ ๋จ์ ํ์ง์ ๊ทธ์น์ง ์๊ณ ์ ์ํ ์ํ ํ๋จ๊ณผ ์ ๋ขฐ๋ ๋์ ์์ฌ๊ฒฐ์ ์ด ํ์์ ์ด๋ฏ๋ก, **ํต์ ๋ณต๊ตฌ ํ ๋
ธ๋์ ์ค์ ์๋ฒ๊ฐ ์๋ฐฉํฅ์ผ๋ก ๋ณํฉํ๋ CRDT ๋๊ธฐํ๋ฅผ ๊ตฌํํ๊ณ , ๊ฐ์ ํ๋ชฉ ์ ๋ณด๋ฅผ ์๋ก ๋ชจ๋ฅด๊ฒ ๊ณ ์น ์ถฉ๋์ ํ์ง ์ฌํ์จ 85% ์ด์๊ณผ ์ฒ๋ฆฌ ์ง์ฐ 200ms ์ดํ๋ฅผ ํ์ธํ๋ค.** ๊ณผ์
์ ์์ํ์ฌ ์คํ์ ๋ฐฉ์งํ๊ณ ์์ ๋์๋ ฅ์ ๊ทน๋ํํด์ผ ํฉ๋๋ค.\n",
"\n",
"์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ณธ ๋ฏธ์
์์๋ **์ฌ๊ณ ์๋์ ํ๋ชฉ๋ณ PN-์นด์ดํฐ(๋
ธ๋๋ณ ์ฆ๊ฐยท๊ฐ์ ๋์ , ๋ณํฉ์ ์์๋ณ ์ต๋๊ฐ)๋ก ๋ง๋ค์ด ๋
ธ๋ 5๊ณณ๊ณผ ์๋ฒ๋ฅผ 2๋จ๊ณ๋ก ๋ณํฉํ๊ณ , ๋ง์ง๋ง ๊ฐ์ด ์ด๊ธฐ๋ ๋ฐฉ์(LWW)๊ณผ ๊ฐฑ์ ์ ์ค์ ๋น๊ต ์ํํ๊ณ ๊ฒ์ฆํ๋** ์ค์ต์ ์ฒด๊ณ์ ์ผ๋ก ์ํํฉ๋๋ค.\n",
"\n",
"ํ์ต์๋ ์ค์ต๊ฐ์ด๋์ ์ ์๋ ์ด 4๊ฐ ์ธ๋ถ ํ๋(๊ธฐ๋ณธ ๊ธฐ๋ฅ ๊ตฌํ โ ๊ฒฐ๊ณผ ์งํ ๊ฒ์ฆ โ ํ๋ผ๋ฏธํฐ ํ๋ ๋ฐ ๋์ ๊ณผ์ )์ ์์ฐจ์ ์ผ๋ก ์งํํ๋ฉฐ ํต์ฌ ํ์ ๊ธฐ์ค์ ๋ฌ์ฑํฉ๋๋ค.\n",
"\n",
"**์ด๋ฒ ๊ฒฐ๊ณผ๋ฌผ:** `src/models/dat10_advanced_detector.py ๋ฐ ์์ธ ํ์ง ํ
์คํธ ๊ฒฐ๊ณผ` ํํ์ ๊ฒฐ๊ณผ๋ฌผ์ ์์ฑํ๋ค.\n",
"\n",
"### ๐ ํ๊ฐ ๋ฐ ๊ฒ์ฆ ๊ธฐ์ค (PASS ์กฐ๊ฑด)\n",
"#### ๋ฏธ์
ํ์ \n",
"\n",
"**ํ๊ฐ ๊ตฌ๋ถ:** ํ์\n",
"\n",
"**ํต์ฌ ํ๊ฐ์์ญ:** ๊ธฐ๋ง/๊ต๋ ๊ณต๊ฒฉ์ ๋ํ ๋ฐฉ์ด ๋ก์ง์ ํ์ค์ฑ ๋ฐ ์ ๋ขฐ์ฑ\n",
"\n",
"#### ๋ฏธ์
PASS ์กฐ๊ฑด\n",
"\n",
"- ์ ๋ ๊ธฐ์ค: ๋ณตํฉ ์ํ ํ์ง ์ฌํ์จ(Recall) 85% ์ด์, ์ฒ๋ฆฌ ์ง์ฐ 200ms ์ดํ\n",
"\n",
"\n",
"- ์งํ ๊ธฐ์ค: ๋ณตํฉ ์ํ ์๋๋ฆฌ์ค 10์ข
์ ๋ํ ํ์ง ํ
์คํธ ํต๊ณผ\n",
"\n",
"\n",
"- ๊ฒ์ฆ ๋ฐฉ๋ฒ: ํฉ์ฑ ์ํ ๋ฐ์ดํฐ์
์ฃผ์
์ ์ด์์น ํ์ง ์ ํ๋ ๊ฒ์ฆ\n",
"\n",
"\n",
"#### ์ฑ์ ์ ํ์ ๋ฉ๋ชจ\n",
"\n",
"> ๋จ์ ์ฝ๋ ์คํ ์ฌ๋ถ๋ฟ๋ง ์๋๋ผ ์ถ๋ ฅ๋ ์์น์ ๊ทผ๊ฑฐ ์๋ฃ๊ฐ ๊ตฐ ์ค๋ฌด ์๊ตฌ์ฌํญ์ ๋ถํฉํ๋์ง ๋ฉด๋ฐํ ํ์ธํ๋ค.\n",
"\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"\"\"\"ํต์ ๋ณต๊ตฌ ํ ๋ณด๊ธ์ 5๊ณณ๊ณผ ์ค์ ์๋ฒ๊ฐ ์๋ฐฉํฅ์ผ๋ก ๋ณํฉํ๋ CRDT ๋๊ธฐํ(์ฌ๊ณ ๋ PN-์นด์ดํฐ, ๋ฉํ ์ ๋ณด๋ ๋ฒ์ ๋ฒกํฐ ๊ธฐ๋ฐ ์ถฉ๋ ๊ฐ์ง)๋ฅผ ๊ตฌํํด, ์๋ ดยท๊ฐฑ์ ์ ์ค 0๊ฑดยท๋ฉฑ๋ฑ/๊ตํ/๊ฒฐํฉ ์ฑ์งยท์ค๋ณต ์ฌ์ ์กยท์ค๋๋ ์ํ ์ฌ์ ์ก ๋ฐฉ์ด๋ฅผ ๊ฒ์ฆํ๊ณ , ์ถฉ๋ ํ์ง ์ฌํ์จ(์๊ฐ ๋น๊ต ๊ท์น ๋ ๋ฒ์ ๋ฒกํฐ ๊ท์น)๊ณผ ์ค๊ฒฝ๋ณด ํจํดยท๋๊ธฐํ ์ง์ฐ์ ์ธก์ ํ๋ค.\"\"\"\n",
"import copy\n",
"import itertools\n",
"import time\n",
"import numpy as np\n",
"import pandas as pd\n",
"import matplotlib.pyplot as plt\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# [๊ต์ก์ฉ๊ฐ์์๋ฃ] ๊ฐ์ ์ผ์ ๋ณด๊ธ์ยท์
์ฒดยทํ๋ชฉ ์๋ฃ ์์ฑ๊ธฐ (์ค์ ๋ถ๋ยท์
์ฒดยทํ๋ชฉยท์ฌ๊ณ ์ ๋ณด ์์)\n",
"# ๊ฐ์ ๋ณด๊ธ์ 5๊ณณ์ด ํ๋ชฉ 12์ข
์ ์
๊ณ ยท๋ถ์ถยท์กฐ์ ํธ๋์ญ์
์ ์คํ๋ผ์ธ์ผ๋ก ๊ธฐ๋กํ๊ณ ๋์ค์ ๋๊ธฐํํ๋ค.\n",
"# ๊ณต๊ธ๋ง ์ฌ๋ ์งํ(์
์ฒด ๋ฉ๊ธฐ ์ง์ฐ, ํ๋ชฉ ์ฌ๊ณ ๊ธ๊ฐ)๋ ๋๊ธฐํ๋ ์๋ฃ๋ฅผ ๋ง์ด๋ํด ์๋ณํ๋ค.\n",
"# ---------------------------------------------------------------------------\n",
"NODES = [\"๋ณด๊ธ์A\", \"๋ณด๊ธ์B\", \"๋ณด๊ธ์C\", \"๋ณด๊ธ์D\", \"๋ณด๊ธ์E\"]\n",
"SUPPLIERS = [\"๊ฐ์์
์ฒด1\", \"๊ฐ์์
์ฒด2\", \"๊ฐ์์
์ฒด3\", \"๊ฐ์์
์ฒด4\", \"๊ฐ์์
์ฒด5\", \"๊ฐ์์
์ฒด6\"]\n",
"CATS = [\"์ผ๋ฐ๋ณด๊ธ\", \"์ ๋น๋ถํ\", \"์๋ฌด์๋ชจ\"]\n",
"ITEMS = [{\"item\": f\"I{i + 1:02d}\", \"cat\": CATS[i % 3], \"supplier\": SUPPLIERS[i % 6], \"lead_days\": 3 + (i * 5) % 12, \"use\": 4 + (i * 7) % 11, \"min_level\": 30 + 10 * (i % 5)} for i in range(12)]\n",
"ITEM_IDS = [x[\"item\"] for x in ITEMS]\n",
"CTXS = [\"ํ์\", \"์ผ๊ฐ\", \"ํต์ ๋ถ๋\"]\n",
"\n",
"\n",
"def make_txns(seed=7, days=7, per_node_day=60, nodes=None):\n",
" \"\"\"๋
ธ๋๋ณ ์คํ๋ผ์ธ ํธ๋์ญ์
๋ชฉ๋ก. id๋ ๋
ธ๋-์ผ๋ จ๋ฒํธ(๋ฉฑ๋ฑ ํค), t๋ ์๊ฐ(์๊ฐ ๋จ์), ctx๋ ์
๋ ฅ ์ํฉ.\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" out = []\n",
" for n in (nodes or NODES):\n",
" seq = 0\n",
" for k in range(days * per_node_day):\n",
" seq += 1\n",
" it = ITEMS[int(rng.integers(len(ITEMS)))]\n",
" t = float(rng.uniform(0, 24 * days))\n",
" ctx = \"์ผ๊ฐ\" if (t % 24 >= 20 or t % 24 < 5) else \"ํ์\"\n",
" if rng.random() < 0.12:\n",
" ctx = \"ํต์ ๋ถ๋\"\n",
" r = rng.random()\n",
" if r < 0.22:\n",
" op, q = \"in\", int(it[\"use\"] * rng.uniform(4, 9))\n",
" elif r < 0.95:\n",
" op, q = \"out\", int(max(1, rng.normal(it[\"use\"] * 1.5, 3)))\n",
" else:\n",
" op, q = \"adjust\", int(rng.integers(-5, 6))\n",
" out.append({\"id\": f\"{n}-{seq:05d}\", \"node\": n, \"item\": it[\"item\"], \"op\": op, \"qty\": q, \"t\": round(t, 2), \"ctx\": ctx, \"supplier\": it[\"supplier\"]})\n",
" return out\n",
"\n",
"\n",
"def stock_of(txns, base=500):\n",
" \"\"\"ํ๋ชฉ๋ณ ์ฌ๊ณ = ๊ธฐ์ด ์ฌ๊ณ + ์
๊ณ - ๋ถ์ถ + ์กฐ์ (id ์ค๋ณต์ ํ ๋ฒ๋ง ๋ฐ์).\"\"\"\n",
" st, seen = {i: base for i in ITEM_IDS}, set()\n",
" for x in txns:\n",
" if x[\"id\"] in seen:\n",
" continue\n",
" seen.add(x[\"id\"])\n",
" st[x[\"item\"]] += x[\"qty\"] if x[\"op\"] in (\"in\", \"adjust\") else -x[\"qty\"]\n",
" return st\n",
"\n",
"\n",
"class Replica:\n",
" \"\"\"์ฌ๊ณ ๋ ํ๋ชฉ๋ณ PN-์นด์ดํฐ(๋
ธ๋๋ณ ์ฆ๊ฐ๋ P, ๊ฐ์๋ N), ๊ฐ์ ๋
ธ๋์ ๊ธฐ๋ก์ ์๋ก ๋ฎ์ด์ฐ์ง ์๋๋ค. ๋ณํฉ์ ์์๋ณ ์ต๋๊ฐ.\"\"\"\n",
" def __init__(self, name):\n",
" self.name = name; self.P = {i: {} for i in ITEM_IDS}; self.N = {i: {} for i in ITEM_IDS}\n",
"\n",
" def apply(self, x):\n",
" d = self.P if x[\"op\"] == \"in\" or (x[\"op\"] == \"adjust\" and x[\"qty\"] > 0) else self.N\n",
" d[x[\"item\"]][x[\"node\"]] = d[x[\"item\"]].get(x[\"node\"], 0) + abs(x[\"qty\"])\n",
"\n",
" def merge(self, other):\n",
" for it in ITEM_IDS:\n",
" for src, dst in ((other.P, self.P), (other.N, self.N)):\n",
" for n, v in src[it].items():\n",
" dst[it][n] = max(dst[it].get(n, 0), v)\n",
"\n",
" def stock(self, base=500):\n",
" return {i: base + sum(self.P[i].values()) - sum(self.N[i].values()) for i in ITEM_IDS}\n",
"\n",
" def state(self):\n",
" return (tuple(tuple(sorted(self.P[i].items())) for i in ITEM_IDS), tuple(tuple(sorted(self.N[i].items())) for i in ITEM_IDS))\n",
"\n",
"\n",
"TX = make_txns(7, 7, per_node_day=60)\n",
"truth = stock_of(TX)\n",
"reps = {n: Replica(n) for n in NODES}\n",
"for x in TX:\n",
" reps[x[\"node\"]].apply(x) # 7์ผ๊ฐ ๊ฐ ๋
ธ๋๋ ์๊ธฐ ๊ธฐ๋ก๋ง ๋ฐ์(์คํ๋ผ์ธ)\n",
"server = Replica(\"์๋ฒ\")\n",
"\n",
"# 1) ํต์ ๋ณต๊ตฌ: ๋
ธ๋ -> ์๋ฒ ๋ณํฉ ํ ์๋ฒ -> ๋
ธ๋ ๋ณํฉ(2๋จ๊ณ). ์ง์ฐ ์ธก์ \n",
"lat = []\n",
"t0 = time.perf_counter()\n",
"for n in NODES:\n",
" t1 = time.perf_counter(); server.merge(reps[n]); lat.append((time.perf_counter() - t1) * 1000)\n",
"for n in NODES:\n",
" t1 = time.perf_counter(); reps[n].merge(server); lat.append((time.perf_counter() - t1) * 1000)\n",
"dt = time.perf_counter() - t0\n",
"conv = len({r.state() for r in reps.values()} | {server.state()}) == 1\n",
"lost = sum(abs(server.stock()[i] - truth[i]) for i in ITEM_IDS)\n",
"print(f\"[CRDT ๋๊ธฐํ] ๋
ธ๋ 5๊ณณ + ์๋ฒ, ํธ๋์ญ์
{len(TX)}๊ฑด: ์๋ ด {'์ฑ๊ณต' if conv else '์คํจ'}, ์๋ฒ ์ฌ๊ณ ์ ์ ๋ต(์ ์ฒด ํฉ์ฐ) ์ฐจ์ด ํฉ๊ณ {lost}๊ฐ -> ๊ฐฑ์ ์ ์ค {'0๊ฑด' if lost == 0 else '์์'}\")\n",
"print(f\"๋ณํฉ 1ํ ํ๊ท {np.mean(lat):.3f}ms, ์ต๋ {max(lat):.3f}ms, ์ ์ฒด ๋๊ธฐํ {dt * 1000:.2f}ms (๋ชฉํ 200ms ์ดํ: {'๋ฌ์ฑ' if max(lat) <= 200 else '๋ฏธ๋ฌ'})\")\n",
"\n",
"# 2) ๋น๊ต: ์ฌ๊ณ ์ซ์๋ฅผ '๋ง์ง๋ง ๊ธฐ๋ก์ด ์ด๊น(LWW)'์ผ๋ก ๋ฎ์ด์ฐ๋ฉด ์คํ๋ผ์ธ ๊ฐฑ์ ์ด ์ฌ๋ผ์ง๋ค\n",
"last = {}\n",
"for n in NODES: # ๊ฐ ๋
ธ๋์ ์ต์ข
์ฌ๊ณ ๊ฐ(๊ธฐ์ด ์ฌ๊ณ + ์๊ธฐ ๊ธฐ๋ก๋ง ๋ฐ์)\n",
" loc = {i: 500 for i in ITEM_IDS}\n",
" for x in TX:\n",
" if x[\"node\"] == n:\n",
" loc[x[\"item\"]] += x[\"qty\"] if x[\"op\"] in (\"in\", \"adjust\") else -x[\"qty\"]\n",
" last[n] = (max(x[\"t\"] for x in TX if x[\"node\"] == n), loc)\n",
"lww = max(last.values())[1]\n",
"print(f\"์ซ์ ๋ฎ์ด์ฐ๊ธฐ(LWW) ๋ณํฉ: ์ ๋ต๊ณผ ์ฐจ์ด ํฉ๊ณ {sum(abs(lww[i] - truth[i]) for i in ITEM_IDS)}๊ฐ (๊ฐฑ์ ์ ์ค) / PN-์นด์ดํฐ {lost}๊ฐ\")\n",
"\n",
"# 3) CRDT ์ฑ์ง ์ํ: ๋ฉฑ๋ฑยท๊ตํยท๊ฒฐํฉยท์ค๋ณตยท์ค๋๋ ์ํ ์ฌ์ ์ก\n",
"rng = np.random.default_rng(5); ok = {\"๋ฉฑ๋ฑ(๋ ๋ฒ ๋ณํฉ)\": 0, \"๊ตํ(A+B = B+A)\": 0, \"๊ฒฐํฉ((A+B)+C = A+(B+C))\": 0, \"์ค๋๋ ์ํ ์ฌ์ ์ก\": 0}\n",
"trials = 200\n",
"for _ in range(trials):\n",
" a, b, c = (reps[n] for n in rng.choice(NODES, 3, replace=False))\n",
" parts = []\n",
" for r in (a, b, c):\n",
" q = Replica(\"t\")\n",
" for i in ITEM_IDS:\n",
" for n in NODES:\n",
" if rng.random() < .5:\n",
" q.P[i][n] = int(rng.integers(0, 100)); q.N[i][n] = int(rng.integers(0, 100))\n",
" parts.append(q)\n",
" x, y, z = parts\n",
" m = copy.deepcopy(x); m.merge(y); s1 = m.state(); m.merge(y); ok[\"๋ฉฑ๋ฑ(๋ ๋ฒ ๋ณํฉ)\"] += s1 == m.state()\n",
" m1 = copy.deepcopy(x); m1.merge(y); m2 = copy.deepcopy(y); m2.merge(x); ok[\"๊ตํ(A+B = B+A)\"] += m1.state() == m2.state()\n",
" l = copy.deepcopy(x); l.merge(y); l.merge(z); rr = copy.deepcopy(y); rr.merge(z); r2 = copy.deepcopy(x); r2.merge(rr); ok[\"๊ฒฐํฉ((A+B)+C = A+(B+C))\"] += l.state() == r2.state()\n",
" new = copy.deepcopy(x); new.merge(y); old = copy.deepcopy(x); before = new.state(); new.merge(old); ok[\"์ค๋๋ ์ํ ์ฌ์ ์ก\"] += before == new.state()\n",
"print(\"\\n[CRDT ์ฑ์ง ์ํ(๋ฌด์์ ์ํ 200์)]\", \", \".join(f\"{k} {v}/{trials}\" for k, v in ok.items()))\n",
"\n",
"# 4) ๋ฉํ ์ ๋ณด(์ต์ ๋ณด์ ๋ยท์์นยท์ํ) ๋์ ์์ ์ถฉ๋ ํ์ง: ์๊ฐ ๋น๊ต ๊ท์น vs ๋ฒ์ ๋ฒกํฐ ๊ท์น\n",
"def dominated(a, b):\n",
" return all(a.get(k, 0) <= b.get(k, 0) for k in set(a) | set(b)) and a != b\n",
"\n",
"\n",
"def make_pair(kind):\n",
" base = float(rng.uniform(0, 160)); va, vb = int(rng.integers(30, 80)), int(rng.integers(30, 80))\n",
" if va == vb:\n",
" vb += 7\n",
" A = {\"field\": \"min_level\", \"value\": va, \"t\": base, \"vv\": {\"A\": 3, \"B\": 1}}\n",
" if kind == \"concurrent\":\n",
" B = {\"field\": \"min_level\", \"value\": vb, \"t\": base + float(rng.uniform(0.5, 120)), \"vv\": {\"A\": 2, \"B\": 4}}\n",
" elif kind == \"causal\": # B๊ฐ A์ ์์ ์ ๋ฐ์ ๋ค ๊ณ ์นจ(์ ์ ํ์ ์์ ), ์๊ฐ์ ๊ฐ๊น์\n",
" B = {\"field\": \"min_level\", \"value\": vb, \"t\": base + float(rng.uniform(0.01, 0.5)), \"vv\": {\"A\": 3, \"B\": 2}}\n",
" elif kind == \"same_value\":\n",
" B = {\"field\": \"min_level\", \"value\": va, \"t\": base + float(rng.uniform(0.5, 120)), \"vv\": {\"A\": 2, \"B\": 4}}\n",
" elif kind == \"other_field\":\n",
" B = {\"field\": \"location\", \"value\": vb, \"t\": base + float(rng.uniform(0.5, 120)), \"vv\": {\"A\": 2, \"B\": 4}}\n",
" else: # ์๊ณ ์ด๊ธ๋จ: ์ค์ ๋ก๋ ๋์ ์์ ์ด์ง๋ง B ๋จ๋ง ์๊ณ๊ฐ ๋๋ ค ์๊ฐ์ด A๋ณด๋ค ์์ ๊ฒ์ฒ๋ผ ๊ธฐ๋ก๋จ\n",
" B = {\"field\": \"min_level\", \"value\": vb, \"t\": base - float(rng.uniform(0.5, 3)), \"vv\": {\"A\": 2, \"B\": 4}}\n",
" return A, B\n",
"\n",
"\n",
"truth_conf = {\"concurrent\": True, \"causal\": False, \"same_value\": False, \"other_field\": False, \"skew\": True}\n",
"pairs = [(k, *make_pair(k)) for k in rng.choice(list(truth_conf), 400, p=[.40, .25, .10, .10, .15])]\n",
"rule_time = lambda A, B: A[\"field\"] == B[\"field\"] and A[\"value\"] != B[\"value\"] and abs(A[\"t\"] - B[\"t\"]) <= 1.0 # 1์๊ฐ ์ด๋ด ๊ฐ์ ํ๋ ๋ค๋ฅธ ๊ฐ\n",
"rule_val = lambda A, B: A[\"field\"] == B[\"field\"] and A[\"value\"] != B[\"value\"] # ๊ฐ์ ํ๋ ๋ค๋ฅธ ๊ฐ์ด๋ฉด ๋ชจ๋ ์ถฉ๋(์ธ๊ณผ ๋ฌด์)\n",
"rule_vv1 = lambda A, B: A[\"field\"] == B[\"field\"] and not dominated(A[\"vv\"], B[\"vv\"]) and not dominated(B[\"vv\"], A[\"vv\"]) # ๋์์ด๋ฉด ์ถฉ๋(๊ฐ ๋น๊ต ์์)\n",
"rule_vv2 = lambda A, B: rule_vv1(A, B) and A[\"value\"] != B[\"value\"] # ์ฌ์ค๊ณ: ๊ฐ์ด ๋ค๋ฅผ ๋๋ง\n",
"print(\"\\n[์ถฉ๋ ํ์ง] ์ ๋ต ์ถฉ๋ %d๊ฑด / ์ %d๊ฑด\" % (sum(truth_conf[k] for k, _, _ in pairs), len(pairs)))\n",
"for name, rule in ((\"์๊ฐ ๊ทผ์ ๊ท์น(1์๊ฐ ์ด๋ด)\", rule_time), (\"๊ฐ ๋น๊ต ๊ท์น(๋ค๋ฅด๋ฉด ์ถฉ๋)\", rule_val), (\"๋ฒ์ ๋ฒกํฐ ๊ท์น v1(๋์์ด๋ฉด ์ถฉ๋)\", rule_vv1), (\"๋ฒ์ ๋ฒกํฐ + ๊ฐ ๋น๊ต v2(์ฌ์ค๊ณ)\", rule_vv2)):\n",
" pred = np.array([rule(a, b) for _, a, b in pairs]); tr = np.array([truth_conf[k] for k, _, _ in pairs])\n",
" tp, fp, fn = int((pred & tr).sum()), int((pred & ~tr).sum()), int((~pred & tr).sum())\n",
" kinds = {str(k): int(v) for k, v in pd.Series([k for (k, _, _), p, t in zip(pairs, pred, tr) if p != t]).value_counts().items()}\n",
" print(f\" {name:30s} ์ฌํ์จ {tp / (tp + fn):5.1%}, ์ ๋ฐ๋ {tp / max(tp + fp, 1):5.1%}, ์ค๊ฒฝ๋ณด {fp}๊ฑด, ๋์นจ {fn}๊ฑด {kinds}\")\n",
"print(\"์ค๊ฒฝ๋ณดยท๋ฏธํ ํจํด: ์๊ฐ ๊ท์น์ ์คํ๋ผ์ธ ๋์ ๋ฉ๋ฆฌ ๋จ์ด์ง ๋์ ์์ (concurrent)ยท์๊ณ ์ด๊ธ๋จ(skew)์ ๋์น๊ณ , ๊ฐ ๋น๊ต ๊ท์น์ 'ํ์ ์ ์ ์์ '(causal)์ ์ถฉ๋๋ก ์ฌ๋ฆผ -> ๋ฒ์ ๋ฒกํฐ๋ก ์ธ๊ณผ ๊ด๊ณ๋ฅผ ๋ณด๊ณ ๊ฐ์ ๊ฐ์ด๋ฉด ์ ์ธ(v2)\")\n",
"print(\"โป ์ถฉ๋๋ก ํ์๋ ๊ฑด์ ์๋ ๋ณํฉํ์ง ์๊ณ ๊ตฐ์๊ด์ด ์๋ ๋ณํฉํ๋ค(M7). ์ฌ๊ณ ์๋์ CRDT๋ก ์๋ ํฉ์ฐ๋๋ฏ๋ก ์ฌ๋์ด ํ๋จํ ๋์์ ๋ฉํ ์ ๋ณด ์ถฉ๋์ด๋ค.\")\n",
"\n",
"plt.figure(figsize=(6, 3.5)); x = np.arange(len(ITEM_IDS))\n",
"plt.bar(x - .2, [truth[i] for i in ITEM_IDS], .4, label=\"truth / CRDT\"); plt.bar(x + .2, [lww[i] for i in ITEM_IDS], .4, label=\"LWW overwrite\", color=\"tab:red\")\n",
"plt.xticks(x, [f\"{i + 1}\" for i in range(12)]); plt.xlabel(\"item\"); plt.ylabel(\"stock\"); plt.legend(); plt.title(\"Merged stock: CRDT vs last-write-wins\"); plt.tight_layout(); plt.show()\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"๐ก [์ฐธ๊ณ ] ๋ฏธ์
04 ์์ ์คํ ๊ฒฐ๊ณผ (Output)
\n",
"\n",
"```text\n",
"[CRDT ๋๊ธฐํ] ๋
ธ๋ 5๊ณณ + ์๋ฒ, ํธ๋์ญ์
2100๊ฑด: ์๋ ด ์ฑ๊ณต, ์๋ฒ ์ฌ๊ณ ์ ์ ๋ต(์ ์ฒด ํฉ์ฐ) ์ฐจ์ด ํฉ๊ณ 0๊ฐ -> ๊ฐฑ์ ์ ์ค 0๊ฑด\n",
"๋ณํฉ 1ํ ํ๊ท 0.022ms, ์ต๋ 0.035ms, ์ ์ฒด ๋๊ธฐํ 0.22ms (๋ชฉํ 200ms ์ดํ: ๋ฌ์ฑ)\n",
"์ซ์ ๋ฎ์ด์ฐ๊ธฐ(LWW) ๋ณํฉ: ์ ๋ต๊ณผ ์ฐจ์ด ํฉ๊ณ 5142๊ฐ (๊ฐฑ์ ์ ์ค) / PN-์นด์ดํฐ 0๊ฐ\n",
"\n",
"[CRDT ์ฑ์ง ์ํ(๋ฌด์์ ์ํ 200์)] ๋ฉฑ๋ฑ(๋ ๋ฒ ๋ณํฉ) 200/200, ๊ตํ(A+B = B+A) 200/200, ๊ฒฐํฉ((A+B)+C = A+(B+C)) 200/200, ์ค๋๋ ์ํ ์ฌ์ ์ก 200/200\n",
"\n",
"[์ถฉ๋ ํ์ง] ์ ๋ต ์ถฉ๋ 226๊ฑด / ์ 400๊ฑด\n",
" ์๊ฐ ๊ทผ์ ๊ท์น(1์๊ฐ ์ด๋ด) ์ฌํ์จ 4.9%, ์ ๋ฐ๋ 9.3%, ์ค๊ฒฝ๋ณด 107๊ฑด, ๋์นจ 215๊ฑด {'concurrent': 163, 'causal': 107, 'skew': 52}\n",
" ๊ฐ ๋น๊ต ๊ท์น(๋ค๋ฅด๋ฉด ์ถฉ๋) ์ฌํ์จ 100.0%, ์ ๋ฐ๋ 67.9%, ์ค๊ฒฝ๋ณด 107๊ฑด, ๋์นจ 0๊ฑด {'causal': 107}\n",
" ๋ฒ์ ๋ฒกํฐ ๊ท์น v1(๋์์ด๋ฉด ์ถฉ๋) ์ฌํ์จ 100.0%, ์ ๋ฐ๋ 88.3%, ์ค๊ฒฝ๋ณด 30๊ฑด, ๋์นจ 0๊ฑด {'same_value': 30}\n",
" ๋ฒ์ ๋ฒกํฐ + ๊ฐ ๋น๊ต v2(์ฌ์ค๊ณ) ์ฌํ์จ 100.0%, ์ ๋ฐ๋ 100.0%, ์ค๊ฒฝ๋ณด 0๊ฑด, ๋์นจ 0๊ฑด {}\n",
"์ค๊ฒฝ๋ณดยท๋ฏธํ ํจํด: ์๊ฐ ๊ท์น์ ์คํ๋ผ์ธ ๋์ ๋ฉ๋ฆฌ ๋จ์ด์ง ๋์ ์์ (concurrent)ยท์๊ณ ์ด๊ธ๋จ(skew)์ ๋์น๊ณ , ๊ฐ ๋น๊ต ๊ท์น์ 'ํ์ ์ ์ ์์ '(causal)์ ์ถฉ๋๋ก ์ฌ๋ฆผ -> ๋ฒ์ ๋ฒกํฐ๋ก ์ธ๊ณผ ๊ด๊ณ๋ฅผ ๋ณด๊ณ ๊ฐ์ ๊ฐ์ด๋ฉด ์ ์ธ(v2)\n",
"โป ์ถฉ๋๋ก ํ์๋ ๊ฑด์ ์๋ ๋ณํฉํ์ง ์๊ณ ๊ตฐ์๊ด์ด ์๋ ๋ณํฉํ๋ค(M7). ์ฌ๊ณ ์๋์ CRDT๋ก ์๋ ํฉ์ฐ๋๋ฏ๋ก ์ฌ๋์ด ํ๋จํ ๋์์ ๋ฉํ ์ ๋ณด ์ถฉ๋์ด๋ค.\n",
"```\n",
" \n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## ๐ฏ ๋ฏธ์
05. ์คํ๋ผ์ธ ์๋ณ์กฐ ๋ฐฉ์ง ์ํธํ ํด์ ๋ฐ ๋ฐ์ดํฐ ๋ฌด๊ฒฐ์ฑ ๊ฒ์ฆ ์์ง ๊ฐ๋ฐ\n",
"**์์์๊ฐ**: ์ฝ 3.0์๊ฐ\n",
"\n",
"### ๐ ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ์ํ ์ง์นจ\n",
"#### ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ๋น์์ฑ\n",
"\n",
"์คํ๋ผ์ธ ์๋ณ์กฐ ๋ฐฉ์ง ์ํธํ ํด์ ๋ฐ ๋ฐ์ดํฐ ๋ฌด๊ฒฐ์ฑ ๊ฒ์ฆ ์์ง์ ๊ฐ๋ฐํ๋ค. ์ค์ ์ ์ ํ์ฅ์์๋ ๋จ์ ํ์ง์ ๊ทธ์น์ง ์๊ณ ์ ์ํ ์ํ ํ๋จ๊ณผ ์ ๋ขฐ๋ ๋์ ์์ฌ๊ฒฐ์ ์ด ํ์์ ์ด๋ฏ๋ก, **๋ณด๊ธ์ ๋ก๊ทธ์ ํด์ ์ฒด์ธ๊ณผ ๋ ์ฝ๋๋ณ HMAC ์๋ช
์ผ๋ก ์๋ณ์กฐ๋ฅผ ์ฐพ๋ ์คํ๋ผ์ธ ๋ฌด๊ฒฐ์ฑ ๊ฒ์ฆ ์์ง์ ๋ง๋ค๊ณ , ๋จ์ ์ ์ฒด ํด์ ๋น๊ต ๋๋น ์ค๊ฒฝ๋ณด์จ์ 40% ์ด์ ์ค์ด๊ณ ์ ๋ขฐ๋ ๋ฑ๊ธ ์ฐ์ถ์ ์ผ๊ด์ฑ 90% ์ด์์ ํ์ธํ๋ค.** ๊ณผ์
์ ์์ํ์ฌ ์คํ์ ๋ฐฉ์งํ๊ณ ์์ ๋์๋ ฅ์ ๊ทน๋ํํด์ผ ํฉ๋๋ค.\n",
"\n",
"์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ณธ ๋ฏธ์
์์๋ **์๋ ์์ ยท๊ธฐ๋ก ์ญ์ ยท์์ ๋ณ๊ฒฝยท์์กฐ ์ฝ์
ยท์ฌ์ ์ก ๋ณ์กฐ(๊ฐ์ ID, ๋ค๋ฅธ ์๋)ยท๊ผฌ๋ฆฌ ์ ๋จ 6์ข
์ ์
์ฑ ๋ณํ๊ณผ ์ค๋ณต ์ ์กยท์์ ๋ค๋ฐ๋ยท๊ณต๋ฐฑ ์ฐจ์ด 3์ข
์ ์ ์ ๋ณํ์ ๋ณด๊ธ์ 5๊ณณ์ 6ํ์ฉ ์ฃผ์
ํ์ฌ ์ฒ๋ฆฌ ์ง์ฐ ๋ฐ ๋ณต์๋ ฅ์ ์ํํ๋** ์ค์ต์ ์ฒด๊ณ์ ์ผ๋ก ์ํํฉ๋๋ค.\n",
"\n",
"ํ์ต์๋ ์ค์ต๊ฐ์ด๋์ ์ ์๋ ์ด 4๊ฐ ์ธ๋ถ ํ๋(๊ธฐ๋ณธ ๊ธฐ๋ฅ ๊ตฌํ โ ๊ฒฐ๊ณผ ์งํ ๊ฒ์ฆ โ ํ๋ผ๋ฏธํฐ ํ๋ ๋ฐ ๋์ ๊ณผ์ )์ ์์ฐจ์ ์ผ๋ก ์งํํ๋ฉฐ ํต์ฌ ํ์ ๊ธฐ์ค์ ๋ฌ์ฑํฉ๋๋ค.\n",
"\n",
"**์ด๋ฒ ๊ฒฐ๊ณผ๋ฌผ:** `src/verification/dat10_confidence_engine.py` ํํ์ ๊ฒฐ๊ณผ๋ฌผ์ ์์ฑํ๋ค.\n",
"\n",
"### ๐ ํ๊ฐ ๋ฐ ๊ฒ์ฆ ๊ธฐ์ค (PASS ์กฐ๊ฑด)\n",
"#### ๋ฏธ์
ํ์ \n",
"\n",
"**ํ๊ฐ ๊ตฌ๋ถ:** ํ์\n",
"\n",
"**ํต์ฌ ํ๊ฐ์์ญ:** ์ ๋ขฐ๋ ๋ฑ๊ธ(High/Med/Low) ๋ถ์ฌ ๊ธฐ์ค์ ๊ตฐ์ฌ์ ํ๋น์ฑ\n",
"\n",
"#### ๋ฏธ์
PASS ์กฐ๊ฑด\n",
"\n",
"- ์ ๋ ๊ธฐ์ค: ์ค๊ฒฝ๋ณด์จ(FAR) 40% ์ด์ ๊ฐ์ถ, ์ ๋ขฐ๋ ์ค์ฝ์ด ์ฐ์ถ ์ผ๊ด์ฑ 90% ์ด์\n",
"\n",
"\n",
"- ์งํ ๊ธฐ์ค: ์ค๊ฒฝ๋ณด ํํฐ๋ง ์ ํ ๋น๊ต ์งํ ์ฐ์ถ ๋ฐ ํตํฉ ํ
์คํธ ์๋ฃ\n",
"\n",
"\n",
"- ๊ฒ์ฆ ๋ฐฉ๋ฒ: ROC-AUC ๊ณก์ ๋ถ์ ๋ฐ ์๊ณ๊ฐ ๋ฏผ๊ฐ๋ ํ
์คํธ\n",
"\n",
"\n",
"#### ์ฑ์ ์ ํ์ ๋ฉ๋ชจ\n",
"\n",
"> ๋จ์ ์ฝ๋ ์คํ ์ฌ๋ถ๋ฟ๋ง ์๋๋ผ ์ถ๋ ฅ๋ ์์น์ ๊ทผ๊ฑฐ ์๋ฃ๊ฐ ๊ตฐ ์ค๋ฌด ์๊ตฌ์ฌํญ์ ๋ถํฉํ๋์ง ๋ฉด๋ฐํ ํ์ธํ๋ค.\n",
"\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"\"\"\"๋ณด๊ธ์ ๋ก๊ทธ์ ํด์ ์ฒด์ธยท๋ ์ฝ๋๋ณ HMAC ์๋ช
์ผ๋ก ์๋ณ์กฐ(์๋ ์์ ยท์ญ์ ยท์์ ๋ณ๊ฒฝยท์์กฐ ์ฝ์
ยท์ฌ์ ์ก ๋ณ์กฐยท๊ผฌ๋ฆฌ ์ ๋จ)๋ฅผ ํ์งํ๋ ์คํ๋ผ์ธ ๋ฌด๊ฒฐ์ฑ ๊ฒ์ฆ ์์ง๊ณผ, ์ ๊ฒ ํญ๋ชฉ ๊ฐ์คํฉ ์ ๋ขฐ๋(High/Med/Low)๋ฅผ ๊ตฌํํด ๋จ์ ์ ์ฒด ํด์ ๋น๊ต ๋๋น ์ค๊ฒฝ๋ณด์จ ๊ฐ์ถยท์ ๋ขฐ๋ ์ฐ์ถ ์ผ๊ด์ฑยท๋ถํ์ค์ฑ ํ์๋ฅผ ๊ฒ์ฆํ๋ค.\"\"\"\n",
"import hashlib\n",
"import hmac\n",
"import numpy as np\n",
"import pandas as pd\n",
"import matplotlib.pyplot as plt\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# [๊ต์ก์ฉ๊ฐ์์๋ฃ] ๊ฐ์ ์ผ์ ๋ณด๊ธ์ยท์
์ฒดยทํ๋ชฉ ์๋ฃ ์์ฑ๊ธฐ (์ค์ ๋ถ๋ยท์
์ฒดยทํ๋ชฉยท์ฌ๊ณ ์ ๋ณด ์์)\n",
"# ๊ฐ์ ๋ณด๊ธ์ 5๊ณณ์ด ํ๋ชฉ 12์ข
์ ์
๊ณ ยท๋ถ์ถยท์กฐ์ ํธ๋์ญ์
์ ์คํ๋ผ์ธ์ผ๋ก ๊ธฐ๋กํ๊ณ ๋์ค์ ๋๊ธฐํํ๋ค.\n",
"# ๊ณต๊ธ๋ง ์ฌ๋ ์งํ(์
์ฒด ๋ฉ๊ธฐ ์ง์ฐ, ํ๋ชฉ ์ฌ๊ณ ๊ธ๊ฐ)๋ ๋๊ธฐํ๋ ์๋ฃ๋ฅผ ๋ง์ด๋ํด ์๋ณํ๋ค.\n",
"# ---------------------------------------------------------------------------\n",
"NODES = [\"๋ณด๊ธ์A\", \"๋ณด๊ธ์B\", \"๋ณด๊ธ์C\", \"๋ณด๊ธ์D\", \"๋ณด๊ธ์E\"]\n",
"SUPPLIERS = [\"๊ฐ์์
์ฒด1\", \"๊ฐ์์
์ฒด2\", \"๊ฐ์์
์ฒด3\", \"๊ฐ์์
์ฒด4\", \"๊ฐ์์
์ฒด5\", \"๊ฐ์์
์ฒด6\"]\n",
"CATS = [\"์ผ๋ฐ๋ณด๊ธ\", \"์ ๋น๋ถํ\", \"์๋ฌด์๋ชจ\"]\n",
"ITEMS = [{\"item\": f\"I{i + 1:02d}\", \"cat\": CATS[i % 3], \"supplier\": SUPPLIERS[i % 6], \"lead_days\": 3 + (i * 5) % 12, \"use\": 4 + (i * 7) % 11, \"min_level\": 30 + 10 * (i % 5)} for i in range(12)]\n",
"ITEM_IDS = [x[\"item\"] for x in ITEMS]\n",
"CTXS = [\"ํ์\", \"์ผ๊ฐ\", \"ํต์ ๋ถ๋\"]\n",
"\n",
"\n",
"def make_txns(seed=7, days=7, per_node_day=60, nodes=None):\n",
" \"\"\"๋
ธ๋๋ณ ์คํ๋ผ์ธ ํธ๋์ญ์
๋ชฉ๋ก. id๋ ๋
ธ๋-์ผ๋ จ๋ฒํธ(๋ฉฑ๋ฑ ํค), t๋ ์๊ฐ(์๊ฐ ๋จ์), ctx๋ ์
๋ ฅ ์ํฉ.\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" out = []\n",
" for n in (nodes or NODES):\n",
" seq = 0\n",
" for k in range(days * per_node_day):\n",
" seq += 1\n",
" it = ITEMS[int(rng.integers(len(ITEMS)))]\n",
" t = float(rng.uniform(0, 24 * days))\n",
" ctx = \"์ผ๊ฐ\" if (t % 24 >= 20 or t % 24 < 5) else \"ํ์\"\n",
" if rng.random() < 0.12:\n",
" ctx = \"ํต์ ๋ถ๋\"\n",
" r = rng.random()\n",
" if r < 0.22:\n",
" op, q = \"in\", int(it[\"use\"] * rng.uniform(4, 9))\n",
" elif r < 0.95:\n",
" op, q = \"out\", int(max(1, rng.normal(it[\"use\"] * 1.5, 3)))\n",
" else:\n",
" op, q = \"adjust\", int(rng.integers(-5, 6))\n",
" out.append({\"id\": f\"{n}-{seq:05d}\", \"node\": n, \"item\": it[\"item\"], \"op\": op, \"qty\": q, \"t\": round(t, 2), \"ctx\": ctx, \"supplier\": it[\"supplier\"]})\n",
" return out\n",
"\n",
"\n",
"def stock_of(txns, base=500):\n",
" \"\"\"ํ๋ชฉ๋ณ ์ฌ๊ณ = ๊ธฐ์ด ์ฌ๊ณ + ์
๊ณ - ๋ถ์ถ + ์กฐ์ (id ์ค๋ณต์ ํ ๋ฒ๋ง ๋ฐ์).\"\"\"\n",
" st, seen = {i: base for i in ITEM_IDS}, set()\n",
" for x in txns:\n",
" if x[\"id\"] in seen:\n",
" continue\n",
" seen.add(x[\"id\"])\n",
" st[x[\"item\"]] += x[\"qty\"] if x[\"op\"] in (\"in\", \"adjust\") else -x[\"qty\"]\n",
" return st\n",
"\n",
"KEYS = {n: hashlib.sha256(f\"๊ต์ก์ฉ-๊ฐ์-ํค-{n}\".encode()).digest() for n in NODES} # ๋
ธ๋๋ณ ์๋ช
ํค(๊ฐ์, ์ค์ ํค ๊ด๋ฆฌ๋ ๋ณ๋ ์ฒด๊ณ)\n",
"TX = make_txns(7, 7, per_node_day=60)\n",
"\n",
"\n",
"def rec_str(x):\n",
" return f\"{x['id']}|{x['node']}|{x['item']}|{x['op']}|{x['qty']}|{x['t']}\"\n",
"\n",
"\n",
"def sign_log(node):\n",
" \"\"\"๋
ธ๋ ๋ก๊ทธ๋ฅผ ์์๋๋ก ํด์ ์ฒด์ธ์ผ๋ก ๋ฌถ๊ณ ๋ ์ฝ๋๋ณ HMAC ์๋ช
์ ๋ถ์ธ๋ค. ๋ง์ง๋ง ํด์(๋จธ๋ฆฌ)๋ ๋๋ฃ ๋
ธ๋๊ฐ ๋ฐ๋ก ๋ณด๊ดํ๋ค.\"\"\"\n",
" log, prev = [], \"GENESIS\"\n",
" for x in [y for y in TX if y[\"node\"] == node]:\n",
" h = hashlib.sha256((prev + rec_str(x)).encode()).hexdigest()\n",
" log.append({**x, \"prev\": prev, \"hash\": h, \"sig\": hmac.new(KEYS[node], rec_str(x).encode(), \"sha256\").hexdigest()[:16], \"note\": \"\"}); prev = h\n",
" return log, prev\n",
"\n",
"\n",
"LOGS = {n: sign_log(n) for n in NODES}\n",
"HEADS = {n: LOGS[n][1] for n in NODES}\n",
"\n",
"\n",
"def canonical(log):\n",
" \"\"\"๋ฐฐ๋ฌ ๊ณผ์ ์ ์ ์ ๋ณํ(์ค๋ณต ์ ์กยท์์ ๋ค๋ฐ๋ยท๊ณต๋ฐฑ ์ฐจ์ด)์ ์ ๋ฆฌ: ๊ฐ์ ID๋ ๋ด์ฉ์ด ๊ฐ์ผ๋ฉด ํ๋๋ง, ์ผ๋ จ๋ฒํธ ์ ์ ๋ ฌ, note ๊ณต๋ฐฑ ์ ๊ฑฐ.\"\"\"\n",
" seen, out, conflicts = {}, [], 0\n",
" for r in log:\n",
" if r[\"id\"] in seen:\n",
" conflicts += (rec_str(r) != rec_str(seen[r[\"id\"]])); continue\n",
" seen[r[\"id\"]] = r; out.append(r)\n",
" return sorted(out, key=lambda r: r[\"id\"]), conflicts\n",
"\n",
"\n",
"def verify(node, log, head=None):\n",
" \"\"\"๊ฒ์ฆ ํญ๋ชฉ 6๊ฐ์ ๊ฒฐ๊ณผ dict: ์ฒด์ธ ์ฐ์, ์๋ช
, ๊ฐ์ ID ๋ด์ฉ ์ถฉ๋, ์ผ๋ จ๋ฒํธ ์ฐ์, ์๊ฐ ์ด์, ๊ผฌ๋ฆฌ ์ ๋จ.\"\"\"\n",
" log, conf = canonical(log)\n",
" chain_ok = sig_ok = True; prev = \"GENESIS\"; bad_sig = 0\n",
" for r in log:\n",
" if r[\"prev\"] != prev or hashlib.sha256((prev + rec_str(r)).encode()).hexdigest() != r[\"hash\"]:\n",
" chain_ok = False\n",
" if not hmac.compare_digest(r[\"sig\"], hmac.new(KEYS[node], rec_str(r).encode(), \"sha256\").hexdigest()[:16]):\n",
" sig_ok = False; bad_sig += 1\n",
" prev = r[\"hash\"]\n",
" seqs = [int(r[\"id\"].split(\"-\")[1]) for r in log]\n",
" gap = seqs != list(range(1, len(seqs) + 1)) if seqs else True\n",
" ts = np.array([r[\"t\"] for r in log]); skew = bool(len(ts) and (ts < 0).any())\n",
" tail = bool(head is not None and prev != head)\n",
" return {\"chain\": chain_ok, \"sig\": sig_ok, \"conflict\": conf == 0, \"seq\": not gap, \"time\": not skew, \"tail\": not tail, \"bad_sig\": bad_sig}\n",
"\n",
"\n",
"W = {\"chain\": 0.30, \"sig\": 0.30, \"conflict\": 0.15, \"seq\": 0.10, \"time\": 0.05, \"tail\": 0.10} # ์ ๋ขฐ๋ ๊ฐ์ค์น ๋งคํธ๋ฆญ์ค(ํฉ 1)\n",
"\n",
"\n",
"def confidence(v, sample_frac=1.0, rng=None):\n",
" \"\"\"์ ๊ฒ ํต๊ณผ ๊ฐ์คํฉ. ์ฒด์ธยท์๋ช
ยท์ถฉ๋ยท๊ผฌ๋ฆฌ ์คํจ๋ ์ฆ์ Low. ์๋ช
์ sample_frac ๋น์จ์ ํ๋ณธ ์ ๊ฒ๋ง ํ์ ๋ ๋ถํ์ค์ฑ์ด ์๊ธด๋ค.\"\"\"\n",
" s = sum(W[k] for k in W if v[k])\n",
" lvl = \"Low\" if not (v[\"chain\"] and v[\"sig\"] and v[\"conflict\"] and v[\"tail\"]) else \"High\" if s >= 0.95 else \"Med\" if s >= 0.75 else \"Low\"\n",
" return s, lvl\n",
"\n",
"\n",
"# ์ํ 1: ์
์ฑ ๋ณ์กฐ 6์ข
+ ์ ์ ์ ์ก ๋ณํ 3์ข
, ๊ฐ ๋
ธ๋ x 6ํ\n",
"import copy\n",
"rng = np.random.default_rng(11)\n",
"\n",
"\n",
"def tamper(kind, log):\n",
" log = copy.deepcopy(log); i = int(rng.integers(5, len(log) - 5))\n",
" if kind == \"์๋ ์์ \":\n",
" log[i][\"qty\"] += 40\n",
" elif kind == \"๊ธฐ๋ก ์ญ์ \":\n",
" del log[i]\n",
" elif kind == \"์์ ๋ฐ๊ฟ+์๊ฐ ์์ \":\n",
" log[i], log[i + 1] = log[i + 1], log[i]; log[i][\"t\"] = round(log[i][\"t\"] + 3, 2)\n",
" elif kind == \"์์กฐ ์ฝ์
(์๋ช
์์)\":\n",
" f = dict(log[i]); f[\"id\"] = log[i][\"id\"].split(\"-\")[0] + \"-\" + f\"{len(log) + 1:05d}\"; f[\"qty\"] = 99; f[\"sig\"] = \"0\" * 16; log.append(f)\n",
" elif kind == \"์ฌ์ ์ก ๋ณ์กฐ(๊ฐ์ ID, ๋ค๋ฅธ ์๋)\":\n",
" f = dict(log[i]); f[\"qty\"] += 7; log.append(f)\n",
" elif kind == \"๊ผฌ๋ฆฌ ์ ๋จ\":\n",
" log = log[: -int(rng.integers(3, 20))]\n",
" elif kind == \"์ค๋ณต ์ ์ก\":\n",
" log += copy.deepcopy(log[i:i + 5])\n",
" elif kind == \"์์ ๋ค๋ฐ๋ ์ ์ก\":\n",
" rng.shuffle(log)\n",
" elif kind == \"๊ณต๋ฐฑ ์ฐจ์ด(note)\":\n",
" for r in log[i:i + 3]:\n",
" r[\"note\"] = \" \"\n",
" return log\n",
"\n",
"\n",
"MAL = [\"์๋ ์์ \", \"๊ธฐ๋ก ์ญ์ \", \"์์ ๋ฐ๊ฟ+์๊ฐ ์์ \", \"์์กฐ ์ฝ์
(์๋ช
์์)\", \"์ฌ์ ์ก ๋ณ์กฐ(๊ฐ์ ID, ๋ค๋ฅธ ์๋)\", \"๊ผฌ๋ฆฌ ์ ๋จ\"]\n",
"BEN = [\"์ค๋ณต ์ ์ก\", \"์์ ๋ค๋ฐ๋ ์ ์ก\", \"๊ณต๋ฐฑ ์ฐจ์ด(note)\"]\n",
"rows = []\n",
"for n in NODES:\n",
" for kind in MAL + BEN:\n",
" for _ in range(6):\n",
" lg = tamper(kind, LOGS[n][0])\n",
" naive = hashlib.sha256(\"\".join(rec_str(r) + r[\"hash\"] + r[\"note\"] for r in lg).encode()).hexdigest() != hashlib.sha256(\"\".join(rec_str(r) + r[\"hash\"] + r[\"note\"] for r in LOGS[n][0]).encode()).hexdigest()\n",
" v = verify(n, lg, HEADS[n]); s, lvl = confidence(v)\n",
" rows.append({\"node\": n, \"kind\": kind, \"mal\": kind in MAL, \"naive\": naive, \"alarm\": lvl == \"Low\", \"score\": s, \"lvl\": lvl})\n",
"R = pd.DataFrame(rows)\n",
"det = R[R[\"mal\"]].groupby(\"kind\").agg(naive=(\"naive\", \"mean\"), layered=(\"alarm\", \"mean\"))\n",
"print(f\"[๋ณ์กฐ ํ์ง] ์
์ฑ {len(MAL)}์ข
x ๋
ธ๋ 5 x 6ํ = {int(R['mal'].sum())}๊ฑด, ์ ์ ์ ์ก ๋ณํ {len(BEN)}์ข
= {int((~R['mal']).sum())}๊ฑด\")\n",
"print(\"์
์ฑ ์ข
๋ฅ๋ณ ํ์ง์จ: ์ ์ฒด ํด์ ๋น๊ต / ๊ณ์ธต ๊ฒ์ฆ(์ฒด์ธ+์๋ช
+์ถฉ๋+์๋ฒ+๊ผฌ๋ฆฌ)\")\n",
"for k, r in det.iterrows():\n",
" print(f\" {k:28s} {r['naive']:5.0%} / {r['layered']:5.0%}\")\n",
"tp_l, fp_l = int((R[\"alarm\"] & R[\"mal\"]).sum()), int((R[\"alarm\"] & ~R[\"mal\"]).sum())\n",
"tp_n, fp_n = int((R[\"naive\"] & R[\"mal\"]).sum()), int((R[\"naive\"] & ~R[\"mal\"]).sum())\n",
"far_n, far_l = fp_n / (tp_n + fp_n), fp_l / max(tp_l + fp_l, 1)\n",
"print(f\"\\n[์ค๊ฒฝ๋ณด] ์ ์ฒด ํด์ ๋น๊ต: ๊ฒฝ๋ณด {tp_n + fp_n}๊ฑด ์ค ์ ์ ๋ณํ {fp_n}๊ฑด(์ค๊ฒฝ๋ณด์จ {far_n:.1%}), ํ์ง์จ {tp_n / R['mal'].sum():.0%}\")\n",
"print(f\" ๊ณ์ธต ๊ฒ์ฆ(์ ๊ทํ ํ): ๊ฒฝ๋ณด {tp_l + fp_l}๊ฑด ์ค ์ ์ ๋ณํ {fp_l}๊ฑด(์ค๊ฒฝ๋ณด์จ {far_l:.1%}), ํ์ง์จ {tp_l / R['mal'].sum():.0%}\")\n",
"print(f\"-> ์ค๊ฒฝ๋ณด์จ {far_n:.1%} -> {far_l:.1%} ({(far_n - far_l) / far_n:.0%} ๊ฐ์ถ, ๋ชฉํ 40% ์ด์: {'๋ฌ์ฑ' if (far_n - far_l) / far_n >= 0.4 else '๋ฏธ๋ฌ'})\")\n",
"miss = R[R[\"mal\"] & ~R[\"alarm\"]][\"kind\"].value_counts().to_dict()\n",
"print(f\"๊ณ์ธต ๊ฒ์ฆ ๋ฏธํ: {miss or '์์'} (๊ผฌ๋ฆฌ ์ ๋จ์ ๋๋ฃ๊ฐ ๋ณด๊ดํ ๋จธ๋ฆฌ ํด์๊ฐ ์์ด์ผ ํ์ง)\")\n",
"\n",
"# ์ํ 2: ์ ๋ขฐ๋ ์ ์์ ์ผ๊ด์ฑ(๊ฐ์ ๋ก๊ทธ๋ฅผ ์๋ช
ํ๋ณธ ์ ๊ฒ ๋น์จ์ ๋ฐ๊ฟ๊ฐ๋ฉฐ 20ํ ์ ๊ฒํ์ ๋ ๋ฑ๊ธ์ด ๊ฐ์๊ฐ)\n",
"print(\"\\n[์ ๋ขฐ๋ ๊ฐ์ค์น ๋งคํธ๋ฆญ์ค]\", W, \"/ High 0.95 ์ด์, Med 0.75 ์ด์, ์ฒด์ธยท์๋ช
ยท์ถฉ๋ยท๊ผฌ๋ฆฌ ์คํจ๋ ์ฆ์ Low\")\n",
"cons = []\n",
"for n in NODES:\n",
" grades, scores = [], []\n",
" for k in range(20):\n",
" lg = LOGS[n][0] if k % 2 == 0 else tamper(\"์ค๋ณต ์ ์ก\", LOGS[n][0]) if k % 4 == 1 else tamper(\"์์ ๋ค๋ฐ๋ ์ ์ก\", LOGS[n][0])\n",
" s, lvl = confidence(verify(n, lg, HEADS[n])); grades.append(lvl); scores.append(s)\n",
" cons.append((n, pd.Series(grades).value_counts().iloc[0] / 20, np.mean(scores), np.std(scores)))\n",
"cm = np.mean([c[1] for c in cons])\n",
"print(\"์ ์ ๋ก๊ทธ 20ํ ๋ฐ๋ณต(์ ์ ์ ์ก ๋ณํ ํฌํจ) ๋ฑ๊ธ ์ผ๊ด์ฑ: \" + \", \".join(f\"{n[-1]} {c:.0%}\" for n, c, _, _ in cons) + f\" -> ํ๊ท {cm:.0%} (๋ชฉํ 90% ์ด์: {'๋ฌ์ฑ' if cm >= .9 else '๋ฏธ๋ฌ'})\")\n",
"lv = pd.crosstab(R[\"kind\"], R[\"lvl\"]).reindex(columns=[\"High\", \"Med\", \"Low\"], fill_value=0)\n",
"print(\"\\n[๋ณํ ์ข
๋ฅ๋ณ ์ ๋ขฐ๋ ๋ฑ๊ธ ๋ถํฌ]\"); print(lv.to_string())\n",
"bad = verify(\"๋ณด๊ธ์A\", tamper(\"์๋ ์์ \", LOGS[\"๋ณด๊ธ์A\"][0]), HEADS[\"๋ณด๊ธ์A\"])\n",
"print(f\"๋ถํ์ค์ฑ ํ์ ์: ์๋ ์์ ๋ก๊ทธ์ ์๋ช
๋ถ์ผ์น ๋ ์ฝ๋ {bad['bad_sig']}๊ฑด -> ํด๋น ๋ ์ฝ๋ ๋ฒํธ๋ฅผ ๊ตฐ์๊ด์๊ฒ ์ ์(์ ์ฒด ํ๊ธฐ ์๋)\")\n",
"print(\"โป ์๋ช
ํค ๋ฐฐํฌยท๋ณด๊ด, ๋จ๋ง ๋ถ์ค ์ ํ๊ธฐ๋ ๋ณ๋ ๋ณด์ ์ฒด๊ณ๊ฐ ํ์ํ๋ค. ์ ๋ขฐ๋ ๋ฑ๊ธ์ ์ ๊ฒ ํญ๋ชฉ ๊ฐ์คํฉ์ด๋ฉฐ ํต๊ณ์ ํ๋ฅ ์ด ์๋๋ค. ์ต์ข
ํ๋จ์ ๊ตฐ์๊ดยท๋ณด์ ๋ด๋น์ด ํ๋ค.\")\n",
"\n",
"plt.figure(figsize=(8, 3.8)); x = np.arange(len(det))\n",
"plt.bar(x - .2, det[\"naive\"] * 100, .4, label=\"whole-hash compare\", color=\"tab:gray\"); plt.bar(x + .2, det[\"layered\"] * 100, .4, label=\"layered verify\", color=\"tab:blue\")\n",
"plt.xticks(x, [f\"M{i + 1}\" for i in range(len(det))]); plt.ylabel(\"detection rate (%)\"); plt.title(\"Tamper detection by type (M1-M6)\"); plt.legend(); plt.tight_layout(); plt.show()\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"๐ก [์ฐธ๊ณ ] ๋ฏธ์
05 ์์ ์คํ ๊ฒฐ๊ณผ (Output)
\n",
"\n",
"```text\n",
"[๋ณ์กฐ ํ์ง] ์
์ฑ 6์ข
x ๋
ธ๋ 5 x 6ํ = 180๊ฑด, ์ ์ ์ ์ก ๋ณํ 3์ข
= 90๊ฑด\n",
"์
์ฑ ์ข
๋ฅ๋ณ ํ์ง์จ: ์ ์ฒด ํด์ ๋น๊ต / ๊ณ์ธต ๊ฒ์ฆ(์ฒด์ธ+์๋ช
+์ถฉ๋+์๋ฒ+๊ผฌ๋ฆฌ)\n",
" ๊ธฐ๋ก ์ญ์ 100% / 100%\n",
" ๊ผฌ๋ฆฌ ์ ๋จ 100% / 100%\n",
" ์๋ ์์ 100% / 100%\n",
" ์์ ๋ฐ๊ฟ+์๊ฐ ์์ 100% / 100%\n",
" ์์กฐ ์ฝ์
(์๋ช
์์) 100% / 100%\n",
" ์ฌ์ ์ก ๋ณ์กฐ(๊ฐ์ ID, ๋ค๋ฅธ ์๋) 100% / 100%\n",
"\n",
"[์ค๊ฒฝ๋ณด] ์ ์ฒด ํด์ ๋น๊ต: ๊ฒฝ๋ณด 270๊ฑด ์ค ์ ์ ๋ณํ 90๊ฑด(์ค๊ฒฝ๋ณด์จ 33.3%), ํ์ง์จ 100%\n",
" ๊ณ์ธต ๊ฒ์ฆ(์ ๊ทํ ํ): ๊ฒฝ๋ณด 180๊ฑด ์ค ์ ์ ๋ณํ 0๊ฑด(์ค๊ฒฝ๋ณด์จ 0.0%), ํ์ง์จ 100%\n",
"-> ์ค๊ฒฝ๋ณด์จ 33.3% -> 0.0% (100% ๊ฐ์ถ, ๋ชฉํ 40% ์ด์: ๋ฌ์ฑ)\n",
"๊ณ์ธต ๊ฒ์ฆ ๋ฏธํ: ์์ (๊ผฌ๋ฆฌ ์ ๋จ์ ๋๋ฃ๊ฐ ๋ณด๊ดํ ๋จธ๋ฆฌ ํด์๊ฐ ์์ด์ผ ํ์ง)\n",
"\n",
"[์ ๋ขฐ๋ ๊ฐ์ค์น ๋งคํธ๋ฆญ์ค] {'chain': 0.3, 'sig': 0.3, 'conflict': 0.15, 'seq': 0.1, 'time': 0.05, 'tail': 0.1} / High 0.95 ์ด์, Med 0.75 ์ด์, ์ฒด์ธยท์๋ช
ยท์ถฉ๋ยท๊ผฌ๋ฆฌ ์คํจ๋ ์ฆ์ Low\n",
"์ ์ ๋ก๊ทธ 20ํ ๋ฐ๋ณต(์ ์ ์ ์ก ๋ณํ ํฌํจ) ๋ฑ๊ธ ์ผ๊ด์ฑ: A 100%, B 100%, C 100%, D 100%, E 100% -> ํ๊ท 100% (๋ชฉํ 90% ์ด์: ๋ฌ์ฑ)\n",
"\n",
"[๋ณํ ์ข
๋ฅ๋ณ ์ ๋ขฐ๋ ๋ฑ๊ธ ๋ถํฌ]\n",
"lvl High Med Low\n",
"kind \n",
"๊ณต๋ฐฑ ์ฐจ์ด(note) 30 0 0\n",
"๊ธฐ๋ก ์ญ์ 0 0 30\n",
"๊ผฌ๋ฆฌ ์ ๋จ 0 0 30\n",
"์๋ ์์ 0 0 30\n",
"์์ ๋ค๋ฐ๋ ์ ์ก 30 0 0\n",
"์์ ๋ฐ๊ฟ+์๊ฐ ์์ 0 0 30\n",
"์์กฐ ์ฝ์
(์๋ช
์์) 0 0 30\n",
"์ฌ์ ์ก ๋ณ์กฐ(๊ฐ์ ID, ๋ค๋ฅธ ์๋) 0 0 30\n",
"์ค๋ณต ์ ์ก 30 0 0\n",
"๋ถํ์ค์ฑ ํ์ ์: ์๋ ์์ ๋ก๊ทธ์ ์๋ช
๋ถ์ผ์น ๋ ์ฝ๋ 1๊ฑด -> ํด๋น ๋ ์ฝ๋ ๋ฒํธ๋ฅผ ๊ตฐ์๊ด์๊ฒ ์ ์(์ ์ฒด ํ๊ธฐ ์๋)\n",
"โป ์๋ช
ํค ๋ฐฐํฌยท๋ณด๊ด, ๋จ๋ง ๋ถ์ค ์ ํ๊ธฐ๋ ๋ณ๋ ๋ณด์ ์ฒด๊ณ๊ฐ ํ์ํ๋ค. ์ ๋ขฐ๋ ๋ฑ๊ธ์ ์ ๊ฒ ํญ๋ชฉ ๊ฐ์คํฉ์ด๋ฉฐ ํต๊ณ์ ํ๋ฅ ์ด ์๋๋ค. ์ต์ข
ํ๋จ์ ๊ตฐ์๊ดยท๋ณด์ ๋ด๋น์ด ํ๋ค.\n",
"```\n",
" \n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## ๐ฏ ๋ฏธ์
06. ์คํ๋ผ์ธ/์จ๋ผ์ธ ์ ํ ์ํ ๋ฐ ์ฌ๊ณ ๋ณ๋ ๋ด์ญ ์ค์๊ฐ ๋์๋ณด๋ UI ๊ตฌํ\n",
"**์์์๊ฐ**: ์ฝ 2.5์๊ฐ\n",
"\n",
"### ๐ ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ์ํ ์ง์นจ\n",
"#### ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ๋น์์ฑ\n",
"\n",
"์คํ๋ผ์ธ/์จ๋ผ์ธ ์ํ ๋ฐ ์ฌ๊ณ ๋ณ๋ ๋ด์ญ ์ค์๊ฐ ๋์๋ณด๋ UI๋ฅผ ๊ตฌํํ๋ค. ์ค์ ์ ์ ํ์ฅ์์๋ ๋จ์ ํ์ง์ ๊ทธ์น์ง ์๊ณ ์ ์ํ ์ํ ํ๋จ๊ณผ ์ ๋ขฐ๋ ๋์ ์์ฌ๊ฒฐ์ ์ด ํ์์ ์ด๋ฏ๋ก, **๋ณด๊ธ์๋ณ ์คํ๋ผ์ธ/์จ๋ผ์ธ ์ํ์ ํ๋ชฉ๋ณ ์ฌ๊ณ ๋ณ๋ ๋ด์ญ, ๊ณต๊ธ ๊ฒฝ๋ณด๋ฅผ ๋ณด์ฌ ์ฃผ๋ ์ค์๊ฐ ๋์๋ณด๋ ํ๋ฉด ๋ชจ๋ธ์ ๊ตฌํํ๊ณ ๊ฐฑ์ 1์ด ์ด๋ดยท์กฐ์ ์๋ต 100ms ์ด๋ด๋ฅผ ํ์ธํ๋ค.** ๊ณผ์
์ ์์ํ์ฌ ์คํ์ ๋ฐฉ์งํ๊ณ ์์ ๋์๋ ฅ์ ๊ทน๋ํํด์ผ ํฉ๋๋ค.\n",
"\n",
"์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ณธ ๋ฏธ์
์์๋ **7์ผ์น ํธ๋์ญ์
2,100๊ฑด์ 1์๊ฐ ๋จ์ 168ํ ๋ฐ์ดํฐ ์คํธ๋ฆผ์ผ๋ก ํ๋ ค ๋ณด๊ธ์ ์ํํ(์จ๋ผ์ธยท์คํ๋ผ์ธยท๋ฏธ๋๊ธฐ ๊ฑด์ยท์ง์ฐ ์๊ฐ), ํ๋ชฉ๋ณ ์ฌ๊ณ ๊ฒ์ด์ง(์์ง ์์ ์ผ์์ ๋ฉ๊ธฐ ๋น๊ต), ๋ณ๋ ๋ด์ญ, ๊ฒฝ๋ณด ๋ชฉ๋ก 4๊ฐ ํจ๋์ ํ๋ฉด ๋ชจ๋ธ์ ๊ฐฑ์ ์ํํ๊ณ ๊ฒ์ฆํ๋** ์ค์ต์ ์ฒด๊ณ์ ์ผ๋ก ์ํํฉ๋๋ค.\n",
"\n",
"ํ์ต์๋ ์ค์ต๊ฐ์ด๋์ ์ ์๋ ์ด 4๊ฐ ์ธ๋ถ ํ๋(๊ธฐ๋ณธ ๊ธฐ๋ฅ ๊ตฌํ โ ๊ฒฐ๊ณผ ์งํ ๊ฒ์ฆ โ ํ๋ผ๋ฏธํฐ ํ๋ ๋ฐ ๋์ ๊ณผ์ )์ ์์ฐจ์ ์ผ๋ก ์งํํ๋ฉฐ ํต์ฌ ํ์ ๊ธฐ์ค์ ๋ฌ์ฑํฉ๋๋ค.\n",
"\n",
"**์ด๋ฒ ๊ฒฐ๊ณผ๋ฌผ:** `src/ui/dat10_dashboard.py (๋๋ ์น UI ์ปดํฌ๋ํธ)` ํํ์ ๊ฒฐ๊ณผ๋ฌผ์ ์์ฑํ๋ค.\n",
"\n",
"### ๐ ํ๊ฐ ๋ฐ ๊ฒ์ฆ ๊ธฐ์ค (PASS ์กฐ๊ฑด)\n",
"#### ๋ฏธ์
ํ์ \n",
"\n",
"**ํ๊ฐ ๊ตฌ๋ถ:** ํ์\n",
"\n",
"**ํต์ฌ ํ๊ฐ์์ญ:** ์ ์ ์์ ์ํฉํ์ผ๋ก์์ ์ง๊ด์ฑ ๋ฐ ์ธ์ง ๋ถํ ๊ฐ์๋\n",
"\n",
"#### ๋ฏธ์
PASS ์กฐ๊ฑด\n",
"\n",
"- ์ ๋ ๊ธฐ์ค: ํ๋ฉด ๊ฐฑ์ ์ฃผ๊ธฐ 1์ด ์ด๋ด, UI ์๋ต์ฑ 100ms ์ด๋ด\n",
"\n",
"\n",
"- ์งํ ๊ธฐ์ค: ์ฃผ์ ์งํ ์๊ฐํ ์ปดํฌ๋ํธ 4์ข
์ด์ ๋ ๋๋ง ๋ฐ ์ค์๊ฐ ๋ฐ์ดํฐ ์ฐ๋\n",
"\n",
"\n",
"- ๊ฒ์ฆ ๋ฐฉ๋ฒ: ์ฌ์ฉ์ ์ธํฐ๋์
ํ
์คํธ ๋ฐ ๋ชจ์ ๋ฐ์ดํฐ ์คํธ๋ฆผ ๋ ๋๋ง ๊ฒ์ฆ\n",
"\n",
"\n",
"#### ์ฑ์ ์ ํ์ ๋ฉ๋ชจ\n",
"\n",
"> ๋จ์ ์ฝ๋ ์คํ ์ฌ๋ถ๋ฟ๋ง ์๋๋ผ ์ถ๋ ฅ๋ ์์น์ ๊ทผ๊ฑฐ ์๋ฃ๊ฐ ๊ตฐ ์ค๋ฌด ์๊ตฌ์ฌํญ์ ๋ถํฉํ๋์ง ๋ฉด๋ฐํ ํ์ธํ๋ค.\n",
"\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"\"\"\"5๊ฐ ๋ณด๊ธ์์ ์คํ๋ผ์ธ/์จ๋ผ์ธ ์ ํ ์ํ์ ํ๋ชฉ๋ณ ์ฌ๊ณ ๋ณ๋ ๋ด์ญ์ ์๊ฐ ๋จ์ ๋ชจ์ ๋ฐ์ดํฐ ์คํธ๋ฆผ์ผ๋ก ๊ฐฑ์ ํ๋ ์ค์๊ฐ ๋์๋ณด๋ ํ๋ฉด ๋ชจ๋ธ(์ํํยท์ฌ๊ณ ๊ฒ์ด์งยท๋ณ๋ ๋ด์ญยท๊ฒฝ๋ณด ํจ๋ 4์ข
)๊ณผ ์ผ๊ฐ ๋ชจ๋ยท๊ธด๊ธ ๊ฒฝ๋ณด ๊ฐ์กฐยท์กฐ์ ์๋ต ์๊ฐ์ ๊ตฌํํด ๊ฐฑ์ ์ฃผ๊ธฐ 1์ด ์ด๋ดยทUI ์๋ต 100ms ์ด๋ด๋ฅผ ์ธก์ ํ๋ค.\"\"\"\n",
"import time\n",
"import numpy as np\n",
"import pandas as pd\n",
"import matplotlib.pyplot as plt\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# [๊ต์ก์ฉ๊ฐ์์๋ฃ] ๊ฐ์ ์ผ์ ๋ณด๊ธ์ยท์
์ฒดยทํ๋ชฉ ์๋ฃ ์์ฑ๊ธฐ (์ค์ ๋ถ๋ยท์
์ฒดยทํ๋ชฉยท์ฌ๊ณ ์ ๋ณด ์์)\n",
"# ๊ฐ์ ๋ณด๊ธ์ 5๊ณณ์ด ํ๋ชฉ 12์ข
์ ์
๊ณ ยท๋ถ์ถยท์กฐ์ ํธ๋์ญ์
์ ์คํ๋ผ์ธ์ผ๋ก ๊ธฐ๋กํ๊ณ ๋์ค์ ๋๊ธฐํํ๋ค.\n",
"# ๊ณต๊ธ๋ง ์ฌ๋ ์งํ(์
์ฒด ๋ฉ๊ธฐ ์ง์ฐ, ํ๋ชฉ ์ฌ๊ณ ๊ธ๊ฐ)๋ ๋๊ธฐํ๋ ์๋ฃ๋ฅผ ๋ง์ด๋ํด ์๋ณํ๋ค.\n",
"# ---------------------------------------------------------------------------\n",
"NODES = [\"๋ณด๊ธ์A\", \"๋ณด๊ธ์B\", \"๋ณด๊ธ์C\", \"๋ณด๊ธ์D\", \"๋ณด๊ธ์E\"]\n",
"SUPPLIERS = [\"๊ฐ์์
์ฒด1\", \"๊ฐ์์
์ฒด2\", \"๊ฐ์์
์ฒด3\", \"๊ฐ์์
์ฒด4\", \"๊ฐ์์
์ฒด5\", \"๊ฐ์์
์ฒด6\"]\n",
"CATS = [\"์ผ๋ฐ๋ณด๊ธ\", \"์ ๋น๋ถํ\", \"์๋ฌด์๋ชจ\"]\n",
"ITEMS = [{\"item\": f\"I{i + 1:02d}\", \"cat\": CATS[i % 3], \"supplier\": SUPPLIERS[i % 6], \"lead_days\": 3 + (i * 5) % 12, \"use\": 4 + (i * 7) % 11, \"min_level\": 30 + 10 * (i % 5)} for i in range(12)]\n",
"ITEM_IDS = [x[\"item\"] for x in ITEMS]\n",
"CTXS = [\"ํ์\", \"์ผ๊ฐ\", \"ํต์ ๋ถ๋\"]\n",
"\n",
"\n",
"def make_txns(seed=7, days=7, per_node_day=60, nodes=None):\n",
" \"\"\"๋
ธ๋๋ณ ์คํ๋ผ์ธ ํธ๋์ญ์
๋ชฉ๋ก. id๋ ๋
ธ๋-์ผ๋ จ๋ฒํธ(๋ฉฑ๋ฑ ํค), t๋ ์๊ฐ(์๊ฐ ๋จ์), ctx๋ ์
๋ ฅ ์ํฉ.\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" out = []\n",
" for n in (nodes or NODES):\n",
" seq = 0\n",
" for k in range(days * per_node_day):\n",
" seq += 1\n",
" it = ITEMS[int(rng.integers(len(ITEMS)))]\n",
" t = float(rng.uniform(0, 24 * days))\n",
" ctx = \"์ผ๊ฐ\" if (t % 24 >= 20 or t % 24 < 5) else \"ํ์\"\n",
" if rng.random() < 0.12:\n",
" ctx = \"ํต์ ๋ถ๋\"\n",
" r = rng.random()\n",
" if r < 0.22:\n",
" op, q = \"in\", int(it[\"use\"] * rng.uniform(4, 9))\n",
" elif r < 0.95:\n",
" op, q = \"out\", int(max(1, rng.normal(it[\"use\"] * 1.5, 3)))\n",
" else:\n",
" op, q = \"adjust\", int(rng.integers(-5, 6))\n",
" out.append({\"id\": f\"{n}-{seq:05d}\", \"node\": n, \"item\": it[\"item\"], \"op\": op, \"qty\": q, \"t\": round(t, 2), \"ctx\": ctx, \"supplier\": it[\"supplier\"]})\n",
" return out\n",
"\n",
"\n",
"def stock_of(txns, base=500):\n",
" \"\"\"ํ๋ชฉ๋ณ ์ฌ๊ณ = ๊ธฐ์ด ์ฌ๊ณ + ์
๊ณ - ๋ถ์ถ + ์กฐ์ (id ์ค๋ณต์ ํ ๋ฒ๋ง ๋ฐ์).\"\"\"\n",
" st, seen = {i: base for i in ITEM_IDS}, set()\n",
" for x in txns:\n",
" if x[\"id\"] in seen:\n",
" continue\n",
" seen.add(x[\"id\"])\n",
" st[x[\"item\"]] += x[\"qty\"] if x[\"op\"] in (\"in\", \"adjust\") else -x[\"qty\"]\n",
" return st\n",
"\n",
"THEMES = {\"์ฃผ๊ฐ\": {\"bg\": (255, 255, 255), \"fg\": (30, 30, 30), \"ok\": (30, 120, 60), \"warn\": (150, 90, 0), \"crit\": (190, 30, 30), \"off\": (90, 90, 90)},\n",
" \"์ผ๊ฐ\": {\"bg\": (18, 22, 28), \"fg\": (225, 230, 235), \"ok\": (90, 210, 130), \"warn\": (245, 190, 70), \"crit\": (255, 110, 110), \"off\": (160, 165, 170)}}\n",
"\n",
"\n",
"def lum(c):\n",
" f = [(v / 255 / 12.92) if v / 255 <= 0.03928 else ((v / 255 + 0.055) / 1.055) ** 2.4 for v in c]\n",
" return 0.2126 * f[0] + 0.7152 * f[1] + 0.0722 * f[2]\n",
"\n",
"\n",
"def contrast(a, b):\n",
" la, lb = sorted([lum(a), lum(b)], reverse=True)\n",
" return (la + 0.05) / (lb + 0.05)\n",
"\n",
"\n",
"class Dashboard:\n",
" \"\"\"ํ๋ฉด ๋ชจ๋ธ: ๋งค tick(1์๊ฐ)๋ง๋ค ์ ํธ๋์ญ์
์ ๋ฐ์ํ๊ณ 4๊ฐ ํจ๋์ ํ์ ๊ฐ์ ๊ฐฑ์ ํ๋ค. ๋ ๋๋ง์ ๋ชจ๋ธ๊ณผ ๋ถ๋ฆฌํด ์๋ต ์๊ฐ์ ๋ฐ๋ก ์ฐ๋ค.\"\"\"\n",
" def __init__(self, txns, outages, base=500):\n",
" self.tx = sorted(txns, key=lambda x: x[\"t\"]); self.p = 0; self.outages = outages\n",
" self.central = {i: base for i in ITEM_IDS}; self.local = {n: {i: 0 for i in ITEM_IDS} for n in NODES} # ์๋ฒ ๋ฐ์ ์ฌ๊ณ / ๋
ธ๋๋ณ ๋ฏธ๋๊ธฐ ๋ณ๋\n",
" self.pending = {n: 0 for n in NODES}; self.last_sync = {n: 0.0 for n in NODES}; self.feed = []; self.theme = \"์ฃผ๊ฐ\"; self.node_filter = None; self.only_alert = False; self.acked = set(); self.out24 = {i: [] for i in ITEM_IDS}\n",
"\n",
" def online(self, n, t):\n",
" return not any(a <= t < b for a, b in self.outages.get(n, []))\n",
"\n",
" def step(self, t):\n",
" while self.p < len(self.tx) and self.tx[self.p][\"t\"] < t:\n",
" x = self.tx[self.p]; self.p += 1; d = x[\"qty\"] if x[\"op\"] in (\"in\", \"adjust\") else -x[\"qty\"]\n",
" if self.online(x[\"node\"], x[\"t\"]):\n",
" self.central[x[\"item\"]] += d; self.last_sync[x[\"node\"]] = x[\"t\"]\n",
" else:\n",
" self.local[x[\"node\"]][x[\"item\"]] += d; self.pending[x[\"node\"]] += 1\n",
" if x[\"op\"] == \"out\":\n",
" self.out24[x[\"item\"]].append((x[\"t\"], x[\"qty\"]))\n",
" self.feed.append((x[\"t\"], x[\"node\"], x[\"item\"], x[\"op\"], x[\"qty\"], \"๋ฐ์\" if self.online(x[\"node\"], x[\"t\"]) else \"๋๊ธฐ\"))\n",
" for n in NODES: # ๋ณต๊ตฌ๋ ๋
ธ๋๋ ๋๊ธฐ๋ถ์ ํ๊บผ๋ฒ์ ๋ฐ์\n",
" if self.online(n, t) and self.pending[n]:\n",
" for i, v in self.local[n].items():\n",
" self.central[i] += v; self.local[n][i] = 0\n",
" self.pending[n] = 0; self.last_sync[n] = t\n",
" return self.view(t)\n",
"\n",
" def view(self, t):\n",
" status = [{\"node\": n, \"state\": \"์จ๋ผ์ธ\" if self.online(n, t) else \"์คํ๋ผ์ธ\", \"pending\": self.pending[n], \"lag_h\": round(t - self.last_sync[n], 1)} for n in NODES]\n",
" gauges = []\n",
" for it in ITEMS:\n",
" st = self.central[it[\"item\"]] + sum(self.local[n][it[\"item\"]] for n in NODES)\n",
" o24 = sum(q for (tt, q) in self.out24[it[\"item\"]] if tt >= t - 24)\n",
" cover = st / max(o24, 1) if t >= 24 else 99.0 # ์ต๊ทผ 24์๊ฐ ๋ถ์ถ๋ ๊ธฐ์ค ์์ง ์์ ์ผ์\n",
" lvl = \"๊ธด๊ธ\" if (st < it[\"min_level\"] or cover < 0.5 * it[\"lead_days\"]) else \"์ฃผ์\" if cover < it[\"lead_days\"] else \"์ ์\"\n",
" gauges.append({\"item\": it[\"item\"], \"stock\": st, \"min\": it[\"min_level\"], \"lvl\": lvl, \"cover\": cover, \"lead\": it[\"lead_days\"], \"acked\": it[\"item\"] in self.acked})\n",
" if self.only_alert:\n",
" gauges = [g for g in gauges if g[\"lvl\"] != \"์ ์\"]\n",
" feed = [f for f in self.feed[-200:] if not self.node_filter or f[1] == self.node_filter][-6:]\n",
" alerts = [g for g in gauges if g[\"lvl\"] == \"๊ธด๊ธ\" and not g[\"acked\"]] + [dict(node=s[\"node\"], lvl=\"์ฅ๊ธฐ ์คํ๋ผ์ธ\") for s in status if s[\"lag_h\"] >= 48]\n",
" return {\"t\": t, \"status\": status, \"gauges\": gauges, \"feed\": feed, \"alerts\": alerts, \"theme\": self.theme}\n",
"\n",
" # ---- ์ฌ์ฉ์ ์กฐ์ ํธ๋ค๋ฌ ----\n",
" def set_theme(self, name):\n",
" self.theme = name; return self.view(self.tx[self.p - 1][\"t\"] if self.p else 0)\n",
"\n",
" def set_node(self, n):\n",
" self.node_filter = n; return self.view(self.tx[self.p - 1][\"t\"])\n",
"\n",
" def toggle_alert(self):\n",
" self.only_alert = not self.only_alert; return self.view(self.tx[self.p - 1][\"t\"])\n",
"\n",
" def ack(self, item):\n",
" self.acked.add(item); return self.view(self.tx[self.p - 1][\"t\"])\n",
"\n",
"\n",
"rng = np.random.default_rng(21)\n",
"TX = make_txns(7, 7, per_node_day=60)\n",
"OUT = {\"๋ณด๊ธ์B\": [(24.0, 96.0)], \"๋ณด๊ธ์D\": [(60.0, 120.0), (140.0, 150.0)], \"๋ณด๊ธ์E\": [(10.0, 168.5)]} # ์คํ๋ผ์ธ ๊ตฌ๊ฐ(์๊ฐ)\n",
"db = Dashboard(TX, OUT)\n",
"lat, snaps, off_n = [], {}, []\n",
"for h in range(1, 169):\n",
" t0 = time.perf_counter(); v = db.step(float(h)); lat.append((time.perf_counter() - t0) * 1000)\n",
" off_n.append(sum(s[\"state\"] == \"์คํ๋ผ์ธ\" for s in v[\"status\"]))\n",
" if h in (12, 72, 168):\n",
" snaps[h] = v\n",
"lat = np.array(lat)\n",
"print(f\"[๋ชจ์ ์คํธ๋ฆผ] {len(TX)}๊ฑด ํธ๋์ญ์
, 168ํ ๊ฐฑ์ (1์๊ฐ ๋จ์), ์คํ๋ผ์ธ ๊ตฌ๊ฐ {sum(len(x) for x in OUT.values())}๊ฐ\")\n",
"print(f\"ํ๋ฉด ๋ชจ๋ธ ๊ฐฑ์ ์ง์ฐ: ํ๊ท {lat.mean():.2f}ms, 95% {np.percentile(lat, 95):.2f}ms, ์ต๋ {lat.max():.2f}ms (๋ชฉํ 1์ด ์ด๋ด: {'๋ฌ์ฑ' if lat.max() < 1000 else '๋ฏธ๋ฌ'})\")\n",
"for h, v in snaps.items():\n",
" print(f\" {h:3d}h ์์ : ์คํ๋ผ์ธ {sum(s['state'] == '์คํ๋ผ์ธ' for s in v['status'])}๊ณณ, ๋ฏธ๋๊ธฐ {sum(s['pending'] for s in v['status'])}๊ฑด, ๊ธด๊ธ/์ฃผ์ ํ๋ชฉ {sum(g['lvl'] == '๊ธด๊ธ' for g in v['gauges'])}/{sum(g['lvl'] == '์ฃผ์' for g in v['gauges'])}์ข
, ๊ฒฝ๋ณด {len(v['alerts'])}๊ฑด\")\n",
"last = snaps[168]\n",
"print(\"\\n[๋ณด๊ธ์ ์ํํ 168h] \" + \" | \".join(f\"{s['node'][-1]} {s['state']} ๋๊ธฐ{s['pending']} ์ง์ฐ{s['lag_h']}h\" for s in last[\"status\"]))\n",
"print(\"[์ฌ๊ณ ๊ฒฝ๋ณด ํ๋ชฉ] \" + \", \".join(f\"{g['item']}:{g['stock']}/์์ง{g['cover']:.1f}์ผ/๋ฉ๊ธฐ{g['lead']}์ผ({g['lvl']})\" for g in [x for x in last[\"gauges\"] if x[\"lvl\"] != \"์ ์\"][:4]) + \" ...\")\n",
"print(\"[๋ณ๋ ๋ด์ญ] \" + \"; \".join(f\"{f[1][-1]}-{f[2]} {f[3]} {f[4]} {f[5]}\" for f in last[\"feed\"][-3:]))\n",
"\n",
"# ---- ์ฌ์ฉ์ ์กฐ์ ์ํ: 100ํ์ฉ ์๋ต ์๊ฐ ----\n",
"def timeit(fn, k=100):\n",
" r = []\n",
" for _ in range(k):\n",
" t0 = time.perf_counter(); fn(); r.append((time.perf_counter() - t0) * 1000)\n",
" return np.array(r)\n",
"ops = {\"์ผ๊ฐ ๋ชจ๋ ์ ํ\": lambda: (db.set_theme(\"์ผ๊ฐ\"), db.set_theme(\"์ฃผ๊ฐ\")), \"๋ณด๊ธ์ ํํฐ\": lambda: (db.set_node(\"๋ณด๊ธ์C\"), db.set_node(None)),\n",
" \"๊ฒฝ๋ณด๋ง ๋ณด๊ธฐ\": lambda: (db.toggle_alert(), db.toggle_alert()), \"๊ฒฝ๋ณด ํ์ธ(ack)\": lambda: (db.ack(\"I01\"), db.acked.clear())}\n",
"print(\"\\n[UI ์กฐ์ ์๋ต ์๊ฐ ms] ์กฐ์ / ํ๊ท / 95% / ์ต๋\")\n",
"worst = 0\n",
"for k, fn in ops.items():\n",
" r = timeit(fn); worst = max(worst, r.max()); print(f\" {k:12s} {r.mean():6.3f} {np.percentile(r, 95):6.3f} {r.max():6.3f}\")\n",
"print(f\"-> ์ต๋ {worst:.2f}ms (๋ชฉํ 100ms ์ด๋ด: {'๋ฌ์ฑ' if worst < 100 else '๋ฏธ๋ฌ'})\")\n",
"\n",
"# ---- ์ผ๊ฐ ๋ชจ๋ ๊ฐ๋
์ฑ(WCAG ๋๋น 4.5 ์ด์)๊ณผ ๊ธด๊ธ ๊ฒฝ๋ณด ๊ฐ์กฐ ----\n",
"print(\"\\n[ํ
๋ง ๋๋น๋น(๊ธ์-๋ฐฐ๊ฒฝ)]\")\n",
"ok = True\n",
"for nm, th in THEMES.items():\n",
" cs = {k: contrast(th[k], th[\"bg\"]) for k in (\"fg\", \"ok\", \"warn\", \"crit\", \"off\")}; ok &= all(c >= 4.5 for c in cs.values())\n",
" print(f\" {nm}: \" + \", \".join(f\"{k} {c:.1f}\" for k, c in cs.items()))\n",
"print(f\"๋ชจ๋ ์์ ๋๋น 4.5 ์ด์: {'๋ฌ์ฑ' if ok else '๋ฏธ๋ฌ'}\")\n",
"\n",
"# ---- ๋ ๋๋ง(4๊ฐ ํจ๋) ----\n",
"th = THEMES[\"์ผ๊ฐ\"]; col = lambda k: tuple(c / 255 for c in th[k])\n",
"t0 = time.perf_counter()\n",
"fig, ax = plt.subplots(2, 2, figsize=(10, 6), facecolor=col(\"bg\"))\n",
"for a in ax.ravel():\n",
" a.set_facecolor(col(\"bg\")); a.tick_params(colors=col(\"fg\")); [s.set_color(col(\"off\")) for s in a.spines.values()]\n",
"a = ax[0, 0]; a.set_title(\"Node status (pending tx)\", color=col(\"fg\"))\n",
"a.bar([s[\"node\"][-1] for s in last[\"status\"]], [s[\"pending\"] for s in last[\"status\"]], color=[col(\"ok\") if s[\"state\"] == \"์จ๋ผ์ธ\" else col(\"crit\") for s in last[\"status\"]])\n",
"a = ax[0, 1]; a.set_title(\"Stock vs minimum level\", color=col(\"fg\")); ys = np.arange(len(last[\"gauges\"]))\n",
"a.barh(ys, [g[\"stock\"] for g in last[\"gauges\"]], color=[col({\"์ ์\": \"ok\", \"์ฃผ์\": \"warn\", \"๊ธด๊ธ\": \"crit\"}[g[\"lvl\"]]) for g in last[\"gauges\"]])\n",
"a.scatter([g[\"min\"] for g in last[\"gauges\"]], ys, color=col(\"fg\"), marker=\"|\", s=80); a.set_yticks(ys); a.set_yticklabels([g[\"item\"] for g in last[\"gauges\"]], fontsize=6)\n",
"a = ax[1, 0]; a.set_title(\"Offline nodes over time\", color=col(\"fg\")); a.step(range(1, 169), off_n, color=col(\"warn\")); a.set_xlabel(\"hour\", color=col(\"fg\"))\n",
"a = ax[1, 1]; a.set_title(\"Alert count by stage\", color=col(\"fg\"))\n",
"a.bar([\"critical\", \"caution\", \"normal\"], [sum(g[\"lvl\"] == k for g in last[\"gauges\"]) for k in (\"๊ธด๊ธ\", \"์ฃผ์\", \"์ ์\")], color=[col(\"crit\"), col(\"warn\"), col(\"ok\")])\n",
"fig.tight_layout(); fig.canvas.draw(); rt = (time.perf_counter() - t0) * 1000\n",
"print(f\"\\n๋ ๋๋ง 4๊ฐ ํจ๋ 1ํ ๊ทธ๋ฆฌ๊ธฐ {rt:.0f}ms (๊ฐฑ์ 1์ด ์ด๋ด: {'๋ฌ์ฑ' if rt + lat.max() < 1000 else '๋ฏธ๋ฌ'}, ์๋ต์ฑ 100ms๋ ํ๋ฉด ๋ชจ๋ธ ์กฐ์ ๊ธฐ์ค)\")\n",
"print(\"โป ๋์๋ณด๋๋ ํ๋จ์ ๋์ ํ์ง ์๋๋ค. ๊ฒฝ๋ณด ํ์ธ(ack)๊ณผ ์ฌ๊ณ ์กฐ์น๋ ๊ตฐ์๊ด์ด ํ๋ค. ํ๋ฉด์ ์ฌ๊ณ ๋ ๋๊ธฐํ๋ ๊ฐ๊ณผ ๋
ธ๋๋ณ ๋ฏธ๋๊ธฐ ๋ณ๋์ ํฉ์ด๋ค.\")\n",
"plt.show()\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"๐ก [์ฐธ๊ณ ] ๋ฏธ์
06 ์์ ์คํ ๊ฒฐ๊ณผ (Output)
\n",
"\n",
"```text\n",
"[๋ชจ์ ์คํธ๋ฆผ] 2100๊ฑด ํธ๋์ญ์
, 168ํ ๊ฐฑ์ (1์๊ฐ ๋จ์), ์คํ๋ผ์ธ ๊ตฌ๊ฐ 4๊ฐ\n",
"ํ๋ฉด ๋ชจ๋ธ ๊ฐฑ์ ์ง์ฐ: ํ๊ท 0.14ms, 95% 0.20ms, ์ต๋ 1.36ms (๋ชฉํ 1์ด ์ด๋ด: ๋ฌ์ฑ)\n",
" 12h ์์ : ์คํ๋ผ์ธ 1๊ณณ, ๋ฏธ๋๊ธฐ 6๊ฑด, ๊ธด๊ธ/์ฃผ์ ํ๋ชฉ 0/0์ข
, ๊ฒฝ๋ณด 0๊ฑด\n",
" 72h ์์ : ์คํ๋ผ์ธ 3๊ณณ, ๋ฏธ๋๊ธฐ 314๊ฑด, ๊ธด๊ธ/์ฃผ์ ํ๋ชฉ 7/3์ข
, ๊ฒฝ๋ณด 9๊ฑด\n",
" 168h ์์ : ์คํ๋ผ์ธ 1๊ณณ, ๋ฏธ๋๊ธฐ 400๊ฑด, ๊ธด๊ธ/์ฃผ์ ํ๋ชฉ 6/3์ข
, ๊ฒฝ๋ณด 7๊ฑด\n",
"\n",
"[๋ณด๊ธ์ ์ํํ 168h] A ์จ๋ผ์ธ ๋๊ธฐ0 ์ง์ฐ1.6h | B ์จ๋ผ์ธ ๋๊ธฐ0 ์ง์ฐ0.1h | C ์จ๋ผ์ธ ๋๊ธฐ0 ์ง์ฐ0.2h | D ์จ๋ผ์ธ ๋๊ธฐ0 ์ง์ฐ0.2h | E ์คํ๋ผ์ธ ๋๊ธฐ400 ์ง์ฐ160.4h\n",
"[์ฌ๊ณ ๊ฒฝ๋ณด ํ๋ชฉ] I02:679/์์ง2.0์ผ/๋ฉ๊ธฐ8์ผ(๊ธด๊ธ), I03:524/์์ง3.8์ผ/๋ฉ๊ธฐ13์ผ(๊ธด๊ธ), I04:997/์์ง2.5์ผ/๋ฉ๊ธฐ6์ผ(๊ธด๊ธ), I05:1503/์์ง4.9์ผ/๋ฉ๊ธฐ11์ผ(๊ธด๊ธ) ...\n",
"[๋ณ๋ ๋ด์ญ] C-I05 out 15 ๋ฐ์; C-I03 in 42 ๋ฐ์; B-I02 in 87 ๋ฐ์\n",
"\n",
"[UI ์กฐ์ ์๋ต ์๊ฐ ms] ์กฐ์ / ํ๊ท / 95% / ์ต๋\n",
" ์ผ๊ฐ ๋ชจ๋ ์ ํ 0.423 0.498 1.075\n",
" ๋ณด๊ธ์ ํํฐ 0.467 0.503 0.627\n",
" ๊ฒฝ๋ณด๋ง ๋ณด๊ธฐ 0.462 0.497 0.640\n",
" ๊ฒฝ๋ณด ํ์ธ(ack) 0.121 0.149 0.218\n",
"-> ์ต๋ 1.08ms (๋ชฉํ 100ms ์ด๋ด: ๋ฌ์ฑ)\n",
"\n",
"[ํ
๋ง ๋๋น๋น(๊ธ์-๋ฐฐ๊ฒฝ)]\n",
" ์ฃผ๊ฐ: fg 16.7, ok 5.5, warn 5.6, crit 6.2, off 6.9\n",
" ์ผ๊ฐ: fg 14.4, ok 9.5, warn 10.7, crit 6.7, off 7.3\n",
"๋ชจ๋ ์์ ๋๋น 4.5 ์ด์: ๋ฌ์ฑ\n",
"\n",
"๋ ๋๋ง 4๊ฐ ํจ๋ 1ํ ๊ทธ๋ฆฌ๊ธฐ 349ms (๊ฐฑ์ 1์ด ์ด๋ด: ๋ฌ์ฑ, ์๋ต์ฑ 100ms๋ ํ๋ฉด ๋ชจ๋ธ ์กฐ์ ๊ธฐ์ค)\n",
"โป ๋์๋ณด๋๋ ํ๋จ์ ๋์ ํ์ง ์๋๋ค. ๊ฒฝ๋ณด ํ์ธ(ack)๊ณผ ์ฌ๊ณ ์กฐ์น๋ ๊ตฐ์๊ด์ด ํ๋ค. ํ๋ฉด์ ์ฌ๊ณ ๋ ๋๊ธฐํ๋ ๊ฐ๊ณผ ๋
ธ๋๋ณ ๋ฏธ๋๊ธฐ ๋ณ๋์ ํฉ์ด๋ค.\n",
"```\n",
" \n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## ๐ฏ ๋ฏธ์
07. ๋๊ธฐํ ์ถฉ๋ ๋ฐ์ ์ ๊ตฐ์๊ด ์๋ ๋ณํฉ(Conflict Resolution) HITL ์ํฌํ๋ก์ฐ ์ค๊ณ\n",
"**์์์๊ฐ**: ์ฝ 3.0์๊ฐ\n",
"\n",
"### ๐ ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ์ํ ์ง์นจ\n",
"#### ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ๋น์์ฑ\n",
"\n",
"๋๊ธฐํ ์ถฉ๋ ๋ฐ์ ์ ๊ตฐ์๊ด ์๋ ๋ณํฉ(Conflict Resolution) HITL ์ํฌํ๋ก์ฐ๋ฅผ ์ค๊ณํ๋ค. ์ค์ ์ ์ ํ์ฅ์์๋ ๋จ์ ํ์ง์ ๊ทธ์น์ง ์๊ณ ์ ์ํ ์ํ ํ๋จ๊ณผ ์ ๋ขฐ๋ ๋์ ์์ฌ๊ฒฐ์ ์ด ํ์์ ์ด๋ฏ๋ก, **๋๊ธฐํ ์ถฉ๋์ด ๋ฐ์ํ์ ๋ ์์คํ
์ด ๊ฐ์ ์๋ ํ์ ํ์ง ์๊ณ ๊ตฐ์๊ด์ด ์๋ ๋ณํฉํ๋ฉฐ, ์ํฅ์ด ํฐ ๊ฑด์ ์งํ๊ด์ด ์น์ธํ๋ HITL ์ํฌํ๋ก์ฐ๋ฅผ ๊ตฌํํด ๊ฒํ ํ ๋๊ธฐ์๊ฐ๊ณผ ์ฒ๋ฆฌ ๋ก๊น
์ฑ๊ณต๋ฅ 100%๋ฅผ ํ์ธํ๋ค.** ๊ณผ์
์ ์์ํ์ฌ ์คํ์ ๋ฐฉ์งํ๊ณ ์์ ๋์๋ ฅ์ ๊ทน๋ํํด์ผ ํฉ๋๋ค.\n",
"\n",
"์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ณธ ๋ฏธ์
์์๋ **์ถฉ๋ 12๊ฑด(์ค์ฌ ์๋ยท์ต์ ๋ณด์ ๊ธฐ์ค์ ๋ ๋ณด๊ธ์๊ฐ ๋ค๋ฅด๊ฒ ์์ )์ ์์คํ
์ ์ - ๋ณด๊ธ๋ด๋น ์์ - ๊ตฐ์๊ด ๊ฒํ ์ ์๋ ๋ณํฉ - ํ์ (์ฐจ์ด 25% ์ด์์ด๋ฉด ์งํ๊ด ์น์ธ) ์์๋ก ์ฒ๋ฆฌํ๊ณ , ๋ณํฉ ์ ํ ๊ฐ๊ณผ ์ฌ์ ยท๊ทผ๊ฑฐ๋ฅผ ๊ธฐ๋กํ๋ฉฐ ๋ฐ๋ ค ์ฌ์ ๋ฅผ ํ์๋ก ์ํํ๊ณ ๊ฒ์ฆํ๋** ์ค์ต์ ์ฒด๊ณ์ ์ผ๋ก ์ํํฉ๋๋ค.\n",
"\n",
"ํ์ต์๋ ์ค์ต๊ฐ์ด๋์ ์ ์๋ ์ด 4๊ฐ ์ธ๋ถ ํ๋(๊ธฐ๋ณธ ๊ธฐ๋ฅ ๊ตฌํ โ ๊ฒฐ๊ณผ ์งํ ๊ฒ์ฆ โ ํ๋ผ๋ฏธํฐ ํ๋ ๋ฐ ๋์ ๊ณผ์ )์ ์์ฐจ์ ์ผ๋ก ์งํํ๋ฉฐ ํต์ฌ ํ์ ๊ธฐ์ค์ ๋ฌ์ฑํฉ๋๋ค.\n",
"\n",
"**์ด๋ฒ ๊ฒฐ๊ณผ๋ฌผ:** `src/hitl/dat10_approval_workflow.py ๋ฐ docs/dat10_hitl_guide.md` ํํ์ ๊ฒฐ๊ณผ๋ฌผ์ ์์ฑํ๋ค.\n",
"\n",
"### ๐ ํ๊ฐ ๋ฐ ๊ฒ์ฆ ๊ธฐ์ค (PASS ์กฐ๊ฑด)\n",
"#### ๋ฏธ์
ํ์ \n",
"\n",
"**ํ๊ฐ ๊ตฌ๋ถ:** ํ์\n",
"\n",
"**ํต์ฌ ํ๊ฐ์์ญ:** ์งํ ์ฑ
์ ๋ถ๊ณ์ ๋ช
ํํ ๋ฐ ์ฑ๊ธํ ์๋ ๋จ์ ๋ฐฉ์ง ์ฒด๊ณ ๋ถํฉ์ฑ\n",
"\n",
"#### ๋ฏธ์
PASS ์กฐ๊ฑด\n",
"\n",
"- ์ ๋ ๊ธฐ์ค: ์ธ๊ฐ ๊ฒํ ํ(Queue) ๋๊ธฐ์๊ฐ ๋ฐ ์น์ธ/๋ฐ๋ ค ์ฒ๋ฆฌ ๋ก๊น
์ฑ๊ณต๋ฅ 100%\n",
"\n",
"\n",
"- ์งํ ๊ธฐ์ค: ์น์ธ/๋ฐ๋ ค ์ธํฐํ์ด์ค ๋์ ๋ฐ ๊ฐ์ฌ ๋ก๊ทธ(Audit Log) ์์ฑ ๊ฒ์ฆ\n",
"\n",
"\n",
"- ๊ฒ์ฆ ๋ฐฉ๋ฒ: ๋ชจ์ ์กฐ์์ ์น์ธ/๋ฐ๋ ค ์๋ฎฌ๋ ์ด์
๋ฐ ๋ฐ์ดํฐ ๋ฌด๊ฒฐ์ฑ ๊ฒ์ฆ\n",
"\n",
"\n",
"#### ์ฑ์ ์ ํ์ ๋ฉ๋ชจ\n",
"\n",
"> ๋จ์ ์ฝ๋ ์คํ ์ฌ๋ถ๋ฟ๋ง ์๋๋ผ ์ถ๋ ฅ๋ ์์น์ ๊ทผ๊ฑฐ ์๋ฃ๊ฐ ๊ตฐ ์ค๋ฌด ์๊ตฌ์ฌํญ์ ๋ถํฉํ๋์ง ๋ฉด๋ฐํ ํ์ธํ๋ค.\n",
"\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"\"\"\"๋๊ธฐํ ์ถฉ๋ ๊ฑด์ ์์คํ
์ ์ -> ๋ณด๊ธ๋ด๋น ์์ -> ๊ตฐ์๊ด ์๋ ๋ณํฉ -> (์ํฅ์ด ํฐ ๊ฑด์ ์งํ๊ด ์น์ธ) ์์์ HITL ์ํฌํ๋ก์ฐ๋ก ์ฒ๋ฆฌํ๋, ์๋ ํ์ ๊ธ์งยท๋ณํฉ ์ ํ ๊ฐ๊ณผ ์ฌ์ ๊ธฐ๋กยท๋ฐ๋ ค ์ฌ์ ํ์ํยท๊ถํ ์ฐํ ์ฐจ๋จยทํด์ ์ฒด์ธ ๊ฐ์ฌ ๋ก๊ทธยท์งํ๊ด ํผ๋๋ฐฑ ์
๋ ฅ ๊ฒ์ฆ์ ๊ตฌํํ๊ณ ์ํ๋ ์ฐ์ ํ(๋๊ธฐ ๋
ธํ ๊ฐ์ฐ)์ ์ ์
์ ์ถ์ ๋๊ธฐ์๊ฐ์ ๋น๊ตํ๋ค.\"\"\"\n",
"import hashlib\n",
"import json\n",
"import numpy as np\n",
"import pandas as pd\n",
"import matplotlib.pyplot as plt\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# [๊ต์ก์ฉ๊ฐ์์๋ฃ] ๊ฐ์ ์ผ์ ๋ณด๊ธ์ยท์
์ฒดยทํ๋ชฉ ์๋ฃ ์์ฑ๊ธฐ (์ค์ ๋ถ๋ยท์
์ฒดยทํ๋ชฉยท์ฌ๊ณ ์ ๋ณด ์์)\n",
"# ๊ฐ์ ๋ณด๊ธ์ 5๊ณณ์ด ํ๋ชฉ 12์ข
์ ์
๊ณ ยท๋ถ์ถยท์กฐ์ ํธ๋์ญ์
์ ์คํ๋ผ์ธ์ผ๋ก ๊ธฐ๋กํ๊ณ ๋์ค์ ๋๊ธฐํํ๋ค.\n",
"# ๊ณต๊ธ๋ง ์ฌ๋ ์งํ(์
์ฒด ๋ฉ๊ธฐ ์ง์ฐ, ํ๋ชฉ ์ฌ๊ณ ๊ธ๊ฐ)๋ ๋๊ธฐํ๋ ์๋ฃ๋ฅผ ๋ง์ด๋ํด ์๋ณํ๋ค.\n",
"# ---------------------------------------------------------------------------\n",
"NODES = [\"๋ณด๊ธ์A\", \"๋ณด๊ธ์B\", \"๋ณด๊ธ์C\", \"๋ณด๊ธ์D\", \"๋ณด๊ธ์E\"]\n",
"SUPPLIERS = [\"๊ฐ์์
์ฒด1\", \"๊ฐ์์
์ฒด2\", \"๊ฐ์์
์ฒด3\", \"๊ฐ์์
์ฒด4\", \"๊ฐ์์
์ฒด5\", \"๊ฐ์์
์ฒด6\"]\n",
"CATS = [\"์ผ๋ฐ๋ณด๊ธ\", \"์ ๋น๋ถํ\", \"์๋ฌด์๋ชจ\"]\n",
"ITEMS = [{\"item\": f\"I{i + 1:02d}\", \"cat\": CATS[i % 3], \"supplier\": SUPPLIERS[i % 6], \"lead_days\": 3 + (i * 5) % 12, \"use\": 4 + (i * 7) % 11, \"min_level\": 30 + 10 * (i % 5)} for i in range(12)]\n",
"ITEM_IDS = [x[\"item\"] for x in ITEMS]\n",
"CTXS = [\"ํ์\", \"์ผ๊ฐ\", \"ํต์ ๋ถ๋\"]\n",
"\n",
"\n",
"def make_txns(seed=7, days=7, per_node_day=60, nodes=None):\n",
" \"\"\"๋
ธ๋๋ณ ์คํ๋ผ์ธ ํธ๋์ญ์
๋ชฉ๋ก. id๋ ๋
ธ๋-์ผ๋ จ๋ฒํธ(๋ฉฑ๋ฑ ํค), t๋ ์๊ฐ(์๊ฐ ๋จ์), ctx๋ ์
๋ ฅ ์ํฉ.\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" out = []\n",
" for n in (nodes or NODES):\n",
" seq = 0\n",
" for k in range(days * per_node_day):\n",
" seq += 1\n",
" it = ITEMS[int(rng.integers(len(ITEMS)))]\n",
" t = float(rng.uniform(0, 24 * days))\n",
" ctx = \"์ผ๊ฐ\" if (t % 24 >= 20 or t % 24 < 5) else \"ํ์\"\n",
" if rng.random() < 0.12:\n",
" ctx = \"ํต์ ๋ถ๋\"\n",
" r = rng.random()\n",
" if r < 0.22:\n",
" op, q = \"in\", int(it[\"use\"] * rng.uniform(4, 9))\n",
" elif r < 0.95:\n",
" op, q = \"out\", int(max(1, rng.normal(it[\"use\"] * 1.5, 3)))\n",
" else:\n",
" op, q = \"adjust\", int(rng.integers(-5, 6))\n",
" out.append({\"id\": f\"{n}-{seq:05d}\", \"node\": n, \"item\": it[\"item\"], \"op\": op, \"qty\": q, \"t\": round(t, 2), \"ctx\": ctx, \"supplier\": it[\"supplier\"]})\n",
" return out\n",
"\n",
"\n",
"def stock_of(txns, base=500):\n",
" \"\"\"ํ๋ชฉ๋ณ ์ฌ๊ณ = ๊ธฐ์ด ์ฌ๊ณ + ์
๊ณ - ๋ถ์ถ + ์กฐ์ (id ์ค๋ณต์ ํ ๋ฒ๋ง ๋ฐ์).\"\"\"\n",
" st, seen = {i: base for i in ITEM_IDS}, set()\n",
" for x in txns:\n",
" if x[\"id\"] in seen:\n",
" continue\n",
" seen.add(x[\"id\"])\n",
" st[x[\"item\"]] += x[\"qty\"] if x[\"op\"] in (\"in\", \"adjust\") else -x[\"qty\"]\n",
" return st\n",
"\n",
"PERM = {\"์์คํ
\": {\"propose\"}, \"๋ณด๊ธ๋ด๋น\": {\"submit\", \"redraft\", \"view\"}, \"๊ตฐ์๊ด\": {\"review\", \"merge\", \"confirm\", \"reject\", \"view\"}, \"์งํ๊ด\": {\"approve\", \"reject\", \"feedback\", \"view\"}, \"๊ฐ์ฌ๊ด\": {\"view\", \"audit\"}}\n",
"USERS = {\"sys\": \"์์คํ
\", \"kim\": \"๋ณด๊ธ๋ด๋น\", \"lee\": \"๊ตฐ์๊ด\", \"park\": \"์งํ๊ด\", \"jung\": \"๊ฐ์ฌ๊ด\"}\n",
"IMPACT_CUT = 0.25 # ๋ ๊ฐ์ ์ฐจ์ด๊ฐ ํฐ ์ชฝ ๊ฐ์ 25% ์ด์์ด๋ฉด ์งํ๊ด ์น์ธ ํ์\n",
"MAX_REDRAFT = 2\n",
"FB_CATS = {\"์ฐ์ ์์\", \"์์\", \"์ ์ฐจ\", \"๊ธฐํ\"} # ์งํ๊ด ํผ๋๋ฐฑ ์
๋ ฅ ํผ ๋ถ๋ฅ\n",
"SLA_H = 4.0 # ๊ตฐ์๊ด ๊ฒํ ๋๊ธฐ ํ์ฉ ์๊ฐ(h)\n",
"\n",
"\n",
"class Chain:\n",
" def __init__(self):\n",
" self.log = []\n",
"\n",
" def add(self, **body):\n",
" prev = self.log[-1][\"hash\"] if self.log else \"GENESIS\"\n",
" body = {\"seq\": len(self.log), **body}\n",
" self.log.append({**body, \"prev\": prev, \"hash\": hashlib.sha256((prev + json.dumps(body, sort_keys=True, ensure_ascii=False)).encode()).hexdigest()})\n",
"\n",
" def verify(self):\n",
" prev = \"GENESIS\"\n",
" for e in self.log:\n",
" b = {k: v for k, v in e.items() if k not in (\"prev\", \"hash\")}\n",
" if e[\"prev\"] != prev or hashlib.sha256((prev + json.dumps(b, sort_keys=True, ensure_ascii=False)).encode()).hexdigest() != e[\"hash\"]:\n",
" return e[\"seq\"]\n",
" prev = e[\"hash\"]\n",
" return None\n",
"\n",
"\n",
"class Workflow:\n",
" def __init__(self):\n",
" self.audit, self.cases, self.calls = Chain(), {}, 0\n",
"\n",
" def _deny(self, who, case, action, why):\n",
" self.audit.add(event=\"DENIED\", who=who, case=case, action=action, reason=why); return False, why\n",
"\n",
" def act(self, who, case, action, reason=\"\", evidence=\"\", value=None, cat=None, cf=None):\n",
" self.calls += 1\n",
" role = USERS.get(who)\n",
" if role is None:\n",
" return self._deny(who, case, action, \"๋ฏธ๋ฑ๋ก ์ฌ์ฉ์\")\n",
" if action in (\"delete_log\", \"edit_log\"):\n",
" return self._deny(who, case, action, \"๊ฐ์ฌ ๋ก๊ทธ ์ญ์ ยท์์ ๊ถํ์ ๋๊ตฌ์๊ฒ๋ ์์\")\n",
" if action not in PERM[role]:\n",
" return self._deny(who, case, action, f\"๊ถํ ์์({role} ์ญํ ์ {action} ๊ถํ ์์)\")\n",
" if action == \"propose\":\n",
" a, b = cf\n",
" self.cases[case] = {\"state\": \"์ ์\", \"submitter\": None, \"version\": 1, \"a\": a, \"b\": b, \"merged\": None, \"high\": abs(a - b) / max(abs(a), abs(b), 1) >= IMPACT_CUT, \"feedback\": []}\n",
" self.audit.add(event=\"PROPOSE\", who=who, case=case, a=a, b=b, reason=reason); return True, \"์ ์ ๋ฑ๋ก(์๋ ํ์ ์์)\"\n",
" c = self.cases.get(case)\n",
" if c is None:\n",
" return self._deny(who, case, action, \"์กด์ฌํ์ง ์๋ ๊ฑด\")\n",
" if c[\"state\"] in (\"ํ์ \", \"์ข
๊ฒฐ(๋ฐ๋ ค)\"):\n",
" return self._deny(who, case, action, f\"์ด๋ฏธ ์ข
๊ฒฐ๋ ๊ฑด({c['state']})\")\n",
" if not reason.strip() or not evidence.strip():\n",
" return self._deny(who, case, action, \"์ฌ์ ยท๊ทผ๊ฑฐ ์
๋ ฅ ํ์(๋ฐ๋ คยท๋ณํฉ ์ฌ์ ํฌํจ)\")\n",
" if action == \"feedback\":\n",
" if cat not in FB_CATS:\n",
" return self._deny(who, case, action, f\"ํผ๋๋ฐฑ ๋ถ๋ฅ ์ค๋ฅ({cat}), ํ์ฉ {sorted(FB_CATS)}\")\n",
" c[\"feedback\"].append((cat, reason))\n",
" self.audit.add(event=\"FEEDBACK\", who=who, role=role, case=case, cat=cat, reason=reason, evidence=evidence); return True, \"์งํ๊ด ํผ๋๋ฐฑ ๊ธฐ๋ก\"\n",
" if action == \"merge\":\n",
" if c[\"state\"] != \"๊ตฐ์๊ด๊ฒํ \":\n",
" return self._deny(who, case, action, f\"๋ณํฉ์ ๊ตฐ์๊ด ๊ฒํ ๋จ๊ณ์์๋ง ๊ฐ๋ฅ(ํ์ฌ {c['state']})\")\n",
" lo, hi = min(c[\"a\"], c[\"b\"]), max(c[\"a\"], c[\"b\"])\n",
" if value is None or not (lo - 0.2 * abs(lo) <= value <= hi + 0.2 * abs(hi)):\n",
" return self._deny(who, case, action, f\"๋ณํฉ ๊ฐ ๋ฒ์ ์ด๊ณผ(ํ์ฉ {lo - 0.2 * abs(lo):.0f}~{hi + 0.2 * abs(hi):.0f}, ์
๋ ฅ {value})\")\n",
" before, c[\"merged\"] = c[\"merged\"], value\n",
" self.audit.add(event=\"MERGE\", who=who, role=role, case=case, before=before, after=value, a=c[\"a\"], b=c[\"b\"], reason=reason, evidence=evidence); return True, f\"๋ณํฉ ๊ฐ {before} -> {value}\"\n",
" if action == \"reject\":\n",
" if c[\"state\"] in (\"์ ์\", \"๋ฐ๋ ค\"):\n",
" return self._deny(who, case, action, f\"๋ฐ๋ คํ ์ ์๋ ๋จ๊ณ({c['state']})\")\n",
" c[\"state\"] = \"์ข
๊ฒฐ(๋ฐ๋ ค)\" if c[\"version\"] > MAX_REDRAFT else \"๋ฐ๋ ค\"\n",
" self.audit.add(event=\"REJECTED\", who=who, role=role, case=case, version=c[\"version\"], reason=reason, evidence=evidence); return True, c[\"state\"]\n",
" if action == \"redraft\":\n",
" if c[\"state\"] != \"๋ฐ๋ ค\":\n",
" return self._deny(who, case, action, f\"์ฌ๊ธฐ์์ ๋ฐ๋ ค๋ ๊ฑด๋ง ๊ฐ๋ฅ(ํ์ฌ {c['state']})\")\n",
" if who != c[\"submitter\"]:\n",
" return self._deny(who, case, action, \"์ฌ๊ธฐ์์ ์ต์ด ์์ ์๋ง ๊ฐ๋ฅ\")\n",
" c[\"state\"], c[\"version\"] = \"์์ \", c[\"version\"] + 1\n",
" self.audit.add(event=\"REDRAFT\", who=who, role=role, case=case, version=c[\"version\"], reason=reason, evidence=evidence); return True, \"์ฌ๊ธฐ์ ์์ \"\n",
" if c[\"state\"] == \"๋ฐ๋ ค\":\n",
" return self._deny(who, case, action, \"๋ฐ๋ ค๋ ๊ฑด์ ์ฌ๊ธฐ์ ํ์๋ง ์งํ ๊ฐ๋ฅ\")\n",
" need = {\"์ ์\": \"submit\", \"์์ \": \"review\", \"๊ตฐ์๊ด๊ฒํ \": \"approve\" if c[\"high\"] else \"confirm\"}[c[\"state\"]]\n",
" if action != need:\n",
" return self._deny(who, case, action, f\"๋จ๊ณ ์๋ฐ(ํ์ฌ {c['state']}{'ยท์ํฅ ํผ' if c['high'] else ''}, ํ์ ํ์ {need})\")\n",
" if action == \"confirm\" and c[\"merged\"] is None or action == \"approve\" and c[\"merged\"] is None:\n",
" return self._deny(who, case, action, \"๊ตฐ์๊ด ๋ณํฉ ๊ฐ ์์(์๋ ํ์ ๊ธ์ง)\")\n",
" if action == \"submit\":\n",
" c[\"submitter\"] = who\n",
" c[\"state\"] = {\"submit\": \"์์ \", \"review\": \"๊ตฐ์๊ด๊ฒํ \", \"confirm\": \"ํ์ \", \"approve\": \"ํ์ \"}[action]\n",
" self.audit.add(event=action.upper(), who=who, role=role, case=case, version=c[\"version\"], to=c[\"state\"], merged=c[\"merged\"], reason=reason, evidence=evidence); return True, c[\"state\"]\n",
"\n",
"\n",
"# ---- ์ถฉ๋ ๊ฑด 12๊ฐ: ๊ฐ์ ํ๋ชฉ์ ์ฌ๊ณ ์ค์ฌ ์๋ยท์ต์ ๋ณด์ ๊ธฐ์ค์ ๋ ๋ณด๊ธ์๊ฐ ์๋ก ๋ค๋ฅด๊ฒ ์์ ----\n",
"rng = np.random.default_rng(5)\n",
"cands = []\n",
"for k in range(12):\n",
" it = ITEMS[k]; field = \"์ค์ฌ ์๋\" if k % 3 else \"์ต์ ๋ณด์ ๊ธฐ์ค\"\n",
" base = 500 if field == \"์ค์ฌ ์๋\" else it[\"min_level\"]\n",
" a = int(base * rng.uniform(0.9, 1.1)); b = int(a * rng.choice([1.05, 1.1, 1.4, 1.8, 0.5]))\n",
" cover = rng.uniform(1.5, 14) # ์์ง ์์ ์ผ์ (๋ฉ๊ธฐ ๋๋น ๋ถ์กฑํ๋ฉด ๊ณต๊ธ๋ง ์ฌ๋ ์งํ)\n",
" risk = 100 * (0.5 * min(abs(a - b) / max(a, b) / 0.5, 1) + 0.5 * max(0, 1 - cover / it[\"lead_days\"]))\n",
" cands.append({\"id\": f\"C{k + 1:02d}\", \"item\": it[\"item\"], \"field\": field, \"a\": a, \"b\": b, \"cover\": cover, \"lead\": it[\"lead_days\"], \"risk\": risk})\n",
"cd = pd.DataFrame(cands)\n",
"print(f\"[์ถฉ๋ ๊ฑด] {len(cd)}๊ฑด, ์ํฅ ํผ(์ฐจ์ด {IMPACT_CUT:.0%} ์ด์) {sum(abs(r.a - r.b) / max(r.a, r.b) >= IMPACT_CUT for r in cd.itertuples())}๊ฑด, ์ํ๋ ํ๊ท {cd['risk'].mean():.0f}\")\n",
"\n",
"# ---- ํ ์๋ฎฌ๋ ์ด์
: ๊ตฐ์๊ด 1๋ช
(๊ฑด๋น 2์๊ฐ), ๊ฑด์ด 1์๊ฐ ๊ฐ๊ฒฉ์ผ๋ก ๋์ฐฉ ----\n",
"def run_queue(policy, risks, service=2.0, arr_gap=1.0):\n",
" n = len(risks); arr = np.arange(n) * arr_gap; done, t, waits = set(), 0.0, {}\n",
" while len(done) < n:\n",
" ready = [i for i in range(n) if i not in done and arr[i] <= t]\n",
" if not ready:\n",
" t = min(arr[i] for i in range(n) if i not in done); continue\n",
" pick = ready[0] if policy == \"fifo\" else max(ready, key=lambda i: risks[i] + 5.0 * (t - arr[i])) # ๋
ธํ ๊ฐ์ฐ: 1์๊ฐ ๋๊ธฐ๋ง๋ค 5์ \n",
" waits[pick] = t - arr[pick]; t += service; done.add(pick)\n",
" return waits\n",
"rs = list(rng.permutation(cd[\"risk\"].to_numpy()))\n",
"wf_, wp_ = run_queue(\"fifo\", rs), run_queue(\"prio\", rs)\n",
"hi = [i for i, r in enumerate(rs) if r >= 50]\n",
"print(f\"\\n[๊ฒํ ํ ๋๊ธฐ์๊ฐ(h)] ๊ฑด {len(rs)}๊ฐ, ์ํ 50 ์ด์ {len(hi)}๊ฑด, ํ์ฉ {SLA_H:.0f}h\")\n",
"for nm, w in ((\"์ ์
์ ์ถ\", wf_), (\"์ํ๋ ์ฐ์ (๋
ธํ ๊ฐ์ฐ)\", wp_)):\n",
" print(f\" {nm:14s}: ์ ์ฒด ํ๊ท {np.mean(list(w.values())):.1f}h, ๊ณ ์ํ ํ๊ท {np.mean([w[i] for i in hi]):.1f}h, ์ต๋ {max(w.values()):.1f}h, ํ์ฉ ์ด๊ณผ {sum(v > SLA_H for v in w.values())}๊ฑด\")\n",
"\n",
"# ---- ์ํฌํ๋ก์ฐ ๋ชจ์ ----\n",
"wf = Workflow(); res = []\n",
"for k, r in cd.iterrows():\n",
" case = r[\"id\"]\n",
" wf.act(\"sys\", case, \"propose\", reason=f\"{r['item']} {r['field']} ์ถฉ๋: A {r['a']} / B {r['b']}\", cf=(int(r[\"a\"]), int(r[\"b\"])))\n",
" wf.act(\"kim\", case, \"submit\", \"์๋ ๋ณํฉ ๊ฒํ ์์ฒญ\", f\"์์ง ์์ {r['cover']:.1f}์ผ, ๋ฉ๊ธฐ {r['lead']}์ผ\")\n",
" for _ in range(12):\n",
" c = wf.cases[case]; s = c[\"state\"]\n",
" if s in (\"ํ์ \", \"์ข
๊ฒฐ(๋ฐ๋ ค)\"):\n",
" break\n",
" if s == \"์์ \":\n",
" if k in (2, 11) and c[\"version\"] <= (1 if k == 2 else 9): # 2๋ฒ ๊ฑด์ 1ํ ๋ฐ๋ ค, 11๋ฒ ๊ฑด์ ๋ณด์ ์์ด ๊ณ์ ๋ฐ๋ ค(์ฌ๊ธฐ์ ํ๋ ์์ฐ)\n",
" wf.act(\"lee\", case, \"reject\", \"๊ทผ๊ฑฐ ์ฌ์งยท์ค์ฌํ ๋๋ฝ, ๋ณด์ ์์ฒญ\", f\"์ถฉ๋ ๋ ๊ฐ {r['a']}/{r['b']}\")\n",
" else:\n",
" wf.act(\"lee\", case, \"review\", \"๋ ๊ธฐ๋ก๊ณผ ์ค์ฌ ๊ทผ๊ฑฐ ํ์ธ\", f\"์์ง ์์ {r['cover']:.1f}์ผ\")\n",
" elif s == \"๊ตฐ์๊ด๊ฒํ \":\n",
" if c[\"merged\"] is None:\n",
" val = int((c[\"a\"] + c[\"b\"]) / 2) if r[\"field\"] == \"์ค์ฌ ์๋\" else max(c[\"a\"], c[\"b\"]) # ์ค์ฌ๋ ํ๊ท ํ๋ณด, ๊ธฐ์ค์ ๋์ ์ชฝ(์์ ์ฐ์ )\n",
" wf.act(\"lee\", case, \"merge\", \"์๋ ๋ณํฉ: ํ์ฅ ์ค์ฌยท๋ฉ๊ธฐ ๊ณ ๋ คํด ๊ฐ ๊ฒฐ์ \", f\"A {c['a']} / B {c['b']}\", value=val)\n",
" if c[\"high\"]:\n",
" wf.act(\"park\", case, \"feedback\", \"์ํฅ์ด ํฐ ๊ฑด์ด๋ฏ๋ก ์ ๋ณ ํ์ธ ํ ๊ฒฐ์ \", \"์ฐ์ ์์\", cat=\"์ฐ์ ์์\")\n",
" wf.act(\"park\", case, \"approve\", \"์น์ธ: ๋ณํฉ ๊ฐ ํ์ \", f\"์ฐจ์ด {abs(c['a'] - c['b']) / max(c['a'], c['b']):.0%}, ๊ตฐ์๊ด ๋ณํฉ ๊ฒํ ์๋ฃ\")\n",
" else:\n",
" wf.act(\"lee\", case, \"confirm\", \"ํ์ : ๋ณํฉ ๊ฐ ๋ฐ์\", \"์ฐจ์ด๊ฐ ์์ ๊ตฐ์๊ด ํ์ ๋ฒ์\")\n",
" elif s == \"๋ฐ๋ ค\":\n",
" wf.act(\"kim\", case, \"redraft\", \"๋ฐ๋ ค ์ฌ์ ๋ฐ์ํด ์ฌ๊ธฐ์\", f\"๊ทผ๊ฑฐ ๋ณด์ {c['version']}์ฐจ\")\n",
" c = wf.cases[case]; res.append((case, r[\"field\"], c[\"a\"], c[\"b\"], \"-\" if c[\"merged\"] is None else str(c[\"merged\"]), \"์งํ๊ด\" if c[\"high\"] else \"๊ตฐ์๊ด\", c[\"state\"], c[\"version\"]))\n",
"print(\"\\n[์ฒ๋ฆฌ ๊ฒฐ๊ณผ] (๋ชจ์ ๊ฒฐ์ฌ์ ๊ธฐ์ค, ์ค์ ํ๋จ์ ์ฌ๋์ด ํ๋ค)\")\n",
"print(pd.DataFrame(res, columns=[\"๊ฑด\", \"ํญ๋ชฉ\", \"A๊ฐ\", \"B๊ฐ\", \"๋ณํฉ๊ฐ\", \"ํ์ ๊ถ์\", \"์ํ\", \"๋ฒ์ \"]).head(12).to_string(index=False))\n",
"print(f\"๊ตฐ์๊ด ์๋ ๋ณํฉ {sum(e['event'] == 'MERGE' for e in wf.audit.log)}๊ฑด(์ ํ ๊ฐยท์ฌ์ ๊ธฐ๋ก), ๋ฐ๋ ค {sum(e['event'] == 'REJECTED' for e in wf.audit.log)}๊ฑด(์ฌ์ ํ์), ์งํ๊ด ํผ๋๋ฐฑ {sum(e['event'] == 'FEEDBACK' for e in wf.audit.log)}๊ฑด\")\n",
"\n",
"# ---- ๊ถํ ์ฐํยท์ ์ฐจ ์๋ฐ ์๋ ----\n",
"def mk(cid, stage=\"submit\", ab=(100, 160)):\n",
" wf.act(\"sys\", cid, \"propose\", reason=\"์ํ\", cf=ab)\n",
" if stage in (\"submit\", \"review\"):\n",
" wf.act(\"kim\", cid, \"submit\", \"์ํ ์์ \", \"๊ทผ๊ฑฐ\")\n",
" if stage == \"review\":\n",
" wf.act(\"lee\", cid, \"review\", \"๊ฒํ \", \"์ด์ ์์\")\n",
" return cid\n",
"done_case = next(c for c, v in wf.cases.items() if v[\"state\"] == \"ํ์ \")\n",
"att = [(\"์์คํ
์๋ ๋ณํฉ\", lambda: wf.act(\"sys\", mk(\"T01\", \"review\"), \"merge\", \"์๋\", \"์๋\", value=130)), (\"๋ณด๊ธ๋ด๋น์ด ๋ณํฉ ์๋\", lambda: wf.act(\"kim\", mk(\"T02\", \"review\"), \"merge\", \"์์\", \"๊ทผ๊ฑฐ\", value=130)),\n",
" (\"๋ณํฉ ์์ด ํ์ \", lambda: wf.act(\"lee\", mk(\"T03\", \"review\", (100, 105)), \"confirm\", \"๋ฐ๋ก ํ์ \", \"๊ทผ๊ฑฐ\")), (\"๋ฏธ๋ฑ๋ก ์ฌ์ฉ์\", lambda: wf.act(\"guest\", mk(\"T04\"), \"review\", \"์ด๋\", \"๊ทผ๊ฑฐ\")),\n",
" (\"์ฌ์ ์๋ ๊ฒํ \", lambda: wf.act(\"lee\", mk(\"T05\"), \"review\", \"\", \"\")), (\"๊ฐ์ฌ ๋ก๊ทธ ์์ \", lambda: wf.act(\"kim\", mk(\"T06\"), \"edit_log\", \"์ ๋ฆฌ\", \"๊ทผ๊ฑฐ\")),\n",
" (\"๊ฐ์ฌ๊ด์ด ํ์ ์๋\", lambda: wf.act(\"jung\", mk(\"T07\", \"review\"), \"confirm\", \"ํ์ \", \"๊ทผ๊ฑฐ\")), (\"์กด์ฌํ์ง ์๋ ๊ฑด\", lambda: wf.act(\"park\", \"X99\", \"approve\", \"์น์ธ\", \"๊ทผ๊ฑฐ\")),\n",
" (\"์ข
๊ฒฐ๋ ๊ฑด ์ฌ์น์ธ\", lambda: wf.act(\"park\", done_case, \"approve\", \"์ฌ์น์ธ\", \"๊ทผ๊ฑฐ\")), (\"๋ณํฉ ๊ฐ ๋ฒ์ ์ด๊ณผ(100/160 -> 500)\", lambda: wf.act(\"lee\", mk(\"T08\", \"review\"), \"merge\", \"๋ํญ ๋ณ๊ฒฝ\", \"๊ทผ๊ฑฐ\", value=500)),\n",
" (\"์ํฅ ํฐ ๊ฑด์ ๊ตฐ์๊ด์ด ํ์ \", lambda: (wf.act(\"lee\", \"T09\", \"merge\", \"๋ณํฉ\", \"๊ทผ๊ฑฐ\", value=130) if mk(\"T09\", \"review\") else None, wf.act(\"lee\", \"T09\", \"confirm\", \"ํ์ \", \"๊ทผ๊ฑฐ\"))[-1]),\n",
" (\"์งํ๊ด์ด ๊ฒํ ๊ฑด๋๋ฐ๊ธฐ\", lambda: wf.act(\"park\", mk(\"T10\"), \"approve\", \"๋ฐ๋ก ์น์ธ\", \"๊ทผ๊ฑฐ\")), (\"ํผ๋๋ฐฑ ๋ถ๋ฅ ์ค๋ฅ\", lambda: wf.act(\"park\", mk(\"T11\", \"review\"), \"feedback\", \"์๊ฒฌ\", \"๊ทผ๊ฑฐ\", cat=\"์ก๋ด\")),\n",
" (\"์์ ์ ๋ณํฉ\", lambda: wf.act(\"lee\", mk(\"T12\", \"submit\"), \"merge\", \"๋ณํฉ\", \"๊ทผ๊ฑฐ\", value=130))]\n",
"print(\"\\n[๊ถํ ์ฐํยท์ ์ฐจ ์๋ฐ ์๋]\")\n",
"blocked = 0\n",
"for n, fn in att:\n",
" ok, why = fn(); blocked += (not ok); print(f\" {n:26s}-> {'์ฐจ๋จ' if not ok else 'ํต๊ณผ(๊ฒฐํจ)'}: {why}\")\n",
"print(f\"์ฐจ๋จ {blocked}/{len(att)} ({blocked / len(att):.0%})\")\n",
"\n",
"# ---- ๋ก๊น
์ฑ๊ณต๋ฅ ๊ณผ ์ด๋ ฅ ๋ฌด๊ฒฐ์ฑ ----\n",
"n_log = len(wf.audit.log); bad = wf.audit.verify()\n",
"print(f\"\\n์ฒ๋ฆฌ ์์ฒญ {wf.calls}๊ฑด -> ๊ฐ์ฌ ๋ก๊ทธ {n_log}๊ฑด(ํ์ ยท๋ฐ๋ คยท๋ณํฉยท๊ฑฐ๋ถ ๋ชจ๋ ๊ธฐ๋ก), ๋ก๊น
์ฑ๊ณต๋ฅ {n_log / wf.calls:.0%} (๋ชฉํ 100%: {'๋ฌ์ฑ' if n_log == wf.calls else '๋ฏธ๋ฌ'})\")\n",
"print(f\"ํด์ ์ฒด์ธ ๊ฒ์ฆ: {'์ ์' if bad is None else f'seq {bad} ๋ถ์ผ์น'} -> ์ด๋ ฅ ๋ฌด๊ฒฐ์ฑ {'100%' if bad is None else 'ํผ์'}\")\n",
"saved = json.dumps(wf.audit.log[12], ensure_ascii=False); wf.audit.log[12][\"reason\"] = \"์ฌํ ์์ \"; v = wf.audit.verify()\n",
"print(f\"12๋ฒ์งธ ๊ธฐ๋ก ์์ ์์ ํ ๊ฒ์ฆ: {'์๋ณ์กฐ ํ์ง(seq ' + str(v) + ')' if v is not None else 'ํ์ง ์คํจ'}\"); wf.audit.log[12] = json.loads(saved)\n",
"print(\"โป ์ถฉ๋ ๋ณํฉ์ ๊ตฐ์๊ด์ด ํ๊ณ , ์ํฅ์ด ํฐ ๊ฑด์ ์งํ๊ด์ด ์น์ธํ๋ค. ์์คํ
์ ๋ ๊ฐ๊ณผ ๊ทผ๊ฑฐ๋ง ์ ์ํ๋ฉฐ ์๋ ๋จ์ ํ์ง ์๋๋ค. ๊ฒฐ์ฌ์ ์ ์์ ๊ธฐ๊ด ๊ท์ ์ ๋ฐ๋ฅธ๋ค.\")\n",
"\n",
"plt.figure(figsize=(6, 3.5)); plt.bar([\"FIFO\", \"priority\"], [np.mean([wf_[i] for i in hi]), np.mean([wp_[i] for i in hi])], color=[\"tab:gray\", \"tab:blue\"])\n",
"plt.ylabel(\"mean wait of high-risk conflicts (h)\"); plt.title(\"Review queue wait: FIFO vs risk priority\"); plt.tight_layout(); plt.show()\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"๐ก [์ฐธ๊ณ ] ๋ฏธ์
07 ์์ ์คํ ๊ฒฐ๊ณผ (Output)
\n",
"\n",
"```text\n",
"[์ถฉ๋ ๊ฑด] 12๊ฑด, ์ํฅ ํผ(์ฐจ์ด 25% ์ด์) 5๊ฑด, ์ํ๋ ํ๊ท 37\n",
"\n",
"[๊ฒํ ํ ๋๊ธฐ์๊ฐ(h)] ๊ฑด 12๊ฐ, ์ํ 50 ์ด์ 6๊ฑด, ํ์ฉ 4h\n",
" ์ ์
์ ์ถ : ์ ์ฒด ํ๊ท 5.5h, ๊ณ ์ํ ํ๊ท 5.5h, ์ต๋ 11.0h, ํ์ฉ ์ด๊ณผ 7๊ฑด\n",
" ์ํ๋ ์ฐ์ (๋
ธํ ๊ฐ์ฐ) : ์ ์ฒด ํ๊ท 5.5h, ๊ณ ์ํ ํ๊ท 0.8h, ์ต๋ 13.0h, ํ์ฉ ์ด๊ณผ 5๊ฑด\n",
"\n",
"[์ฒ๋ฆฌ ๊ฒฐ๊ณผ] (๋ชจ์ ๊ฒฐ์ฌ์ ๊ธฐ์ค, ์ค์ ํ๋จ์ ์ฌ๋์ด ํ๋ค)\n",
" ๊ฑด ํญ๋ชฉ A๊ฐ B๊ฐ ๋ณํฉ๊ฐ ํ์ ๊ถ์ ์ํ ๋ฒ์ \n",
"C01 ์ต์ ๋ณด์ ๊ธฐ์ค 31 32 32 ๊ตฐ์๊ด ํ์ 1\n",
"C02 ์ค์ฌ ์๋ 478 239 358 ์งํ๊ด ํ์ 1\n",
"C03 ์ค์ฌ ์๋ 488 683 585 ์งํ๊ด ํ์ 2\n",
"C04 ์ต์ ๋ณด์ ๊ธฐ์ค 54 75 75 ์งํ๊ด ํ์ 1\n",
"C05 ์ค์ฌ ์๋ 515 927 721 ์งํ๊ด ํ์ 1\n",
"C06 ์ค์ฌ ์๋ 547 601 574 ๊ตฐ์๊ด ํ์ 1\n",
"C07 ์ต์ ๋ณด์ ๊ธฐ์ค 42 44 44 ๊ตฐ์๊ด ํ์ 1\n",
"C08 ์ค์ฌ ์๋ 517 568 542 ๊ตฐ์๊ด ํ์ 1\n",
"C09 ์ค์ฌ ์๋ 505 252 378 ์งํ๊ด ํ์ 1\n",
"C10 ์ต์ ๋ณด์ ๊ธฐ์ค 63 69 69 ๊ตฐ์๊ด ํ์ 1\n",
"C11 ์ค์ฌ ์๋ 537 590 563 ๊ตฐ์๊ด ํ์ 1\n",
"C12 ์ค์ฌ ์๋ 537 590 - ๊ตฐ์๊ด ์ข
๊ฒฐ(๋ฐ๋ ค) 3\n",
"๊ตฐ์๊ด ์๋ ๋ณํฉ 11๊ฑด(์ ํ ๊ฐยท์ฌ์ ๊ธฐ๋ก), ๋ฐ๋ ค 4๊ฑด(์ฌ์ ํ์), ์งํ๊ด ํผ๋๋ฐฑ 5๊ฑด\n",
"\n",
"[๊ถํ ์ฐํยท์ ์ฐจ ์๋ฐ ์๋]\n",
" ์์คํ
์๋ ๋ณํฉ -> ์ฐจ๋จ: ๊ถํ ์์(์์คํ
์ญํ ์ merge ๊ถํ ์์)\n",
" ๋ณด๊ธ๋ด๋น์ด ๋ณํฉ ์๋ -> ์ฐจ๋จ: ๊ถํ ์์(๋ณด๊ธ๋ด๋น ์ญํ ์ merge ๊ถํ ์์)\n",
" ๋ณํฉ ์์ด ํ์ -> ์ฐจ๋จ: ๊ตฐ์๊ด ๋ณํฉ ๊ฐ ์์(์๋ ํ์ ๊ธ์ง)\n",
" ๋ฏธ๋ฑ๋ก ์ฌ์ฉ์ -> ์ฐจ๋จ: ๋ฏธ๋ฑ๋ก ์ฌ์ฉ์\n",
" ์ฌ์ ์๋ ๊ฒํ -> ์ฐจ๋จ: ์ฌ์ ยท๊ทผ๊ฑฐ ์
๋ ฅ ํ์(๋ฐ๋ คยท๋ณํฉ ์ฌ์ ํฌํจ)\n",
" ๊ฐ์ฌ ๋ก๊ทธ ์์ -> ์ฐจ๋จ: ๊ฐ์ฌ ๋ก๊ทธ ์ญ์ ยท์์ ๊ถํ์ ๋๊ตฌ์๊ฒ๋ ์์\n",
" ๊ฐ์ฌ๊ด์ด ํ์ ์๋ -> ์ฐจ๋จ: ๊ถํ ์์(๊ฐ์ฌ๊ด ์ญํ ์ confirm ๊ถํ ์์)\n",
" ์กด์ฌํ์ง ์๋ ๊ฑด -> ์ฐจ๋จ: ์กด์ฌํ์ง ์๋ ๊ฑด\n",
" ์ข
๊ฒฐ๋ ๊ฑด ์ฌ์น์ธ -> ์ฐจ๋จ: ์ด๋ฏธ ์ข
๊ฒฐ๋ ๊ฑด(ํ์ )\n",
" ๋ณํฉ ๊ฐ ๋ฒ์ ์ด๊ณผ(100/160 -> 500)-> ์ฐจ๋จ: ๋ณํฉ ๊ฐ ๋ฒ์ ์ด๊ณผ(ํ์ฉ 80~192, ์
๋ ฅ 500)\n",
" ์ํฅ ํฐ ๊ฑด์ ๊ตฐ์๊ด์ด ํ์ -> ์ฐจ๋จ: ๋จ๊ณ ์๋ฐ(ํ์ฌ ๊ตฐ์๊ด๊ฒํ ยท์ํฅ ํผ, ํ์ ํ์ approve)\n",
" ์งํ๊ด์ด ๊ฒํ ๊ฑด๋๋ฐ๊ธฐ -> ์ฐจ๋จ: ๋จ๊ณ ์๋ฐ(ํ์ฌ ์์ ยท์ํฅ ํผ, ํ์ ํ์ review)\n",
" ํผ๋๋ฐฑ ๋ถ๋ฅ ์ค๋ฅ -> ์ฐจ๋จ: ํผ๋๋ฐฑ ๋ถ๋ฅ ์ค๋ฅ(์ก๋ด), ํ์ฉ ['๊ธฐํ', '์ฐ์ ์์', '์์', '์ ์ฐจ']\n",
" ์์ ์ ๋ณํฉ -> ์ฐจ๋จ: ๋ณํฉ์ ๊ตฐ์๊ด ๊ฒํ ๋จ๊ณ์์๋ง ๊ฐ๋ฅ(ํ์ฌ ์์ )\n",
"์ฐจ๋จ 14/14 (100%)\n",
"\n",
"์ฒ๋ฆฌ ์์ฒญ 115๊ฑด -> ๊ฐ์ฌ ๋ก๊ทธ 115๊ฑด(ํ์ ยท๋ฐ๋ คยท๋ณํฉยท๊ฑฐ๋ถ ๋ชจ๋ ๊ธฐ๋ก), ๋ก๊น
์ฑ๊ณต๋ฅ 100% (๋ชฉํ 100%: ๋ฌ์ฑ)\n",
"... (์ดํ ์๋ต)\n",
"```\n",
" \n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## ๐ฏ ๋ฏธ์
08. 7์ผ๊ฐ ํต์ ๋จ์ ํ ๋๋ ์ฌ๊ณ ํธ๋์ญ์
์ผ๊ด ๋๊ธฐํ ์คํธ๋ ์ค ํ
์คํธ\n",
"**์์์๊ฐ**: ์ฝ 3.5์๊ฐ\n",
"\n",
"### ๐ ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ์ํ ์ง์นจ\n",
"#### ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ๋น์์ฑ\n",
"\n",
"7์ผ๊ฐ ํต์ ๋จ์ ํ ๋๋ ์ฌ๊ณ ํธ๋์ญ์
์ผ๊ด ๋๊ธฐํ ์คํธ๋ ์ค ํ
์คํธ๋ฅผ ์ํํ๋ค. ์ค์ ์ ์ ํ์ฅ์์๋ ๋จ์ ํ์ง์ ๊ทธ์น์ง ์๊ณ ์ ์ํ ์ํ ํ๋จ๊ณผ ์ ๋ขฐ๋ ๋์ ์์ฌ๊ฒฐ์ ์ด ํ์์ ์ด๋ฏ๋ก, **7์ผ๊ฐ ํต์ ์ด ๋๊ฒผ๋ค๊ฐ ๋ณต๊ตฌ๋ ์งํ ๋ณด๊ธ์ 5๊ณณ์ ๋์ ํธ๋์ญ์
์ด ํ๊บผ๋ฒ์ ์๋ฒ๋ก ๋ชฐ๋ฆฌ๋ ์ํฉ์ ๊ธฐ์ค 100%ยท200%ยท500%๋ก ์ํํด ์์คํ
๋ค์ด 0๊ฑดยท๋ฐ์ดํฐ ์ ์ค 0%์ ๊ธฐ๋ฅ ์ถ์ ๋์์ ํ์ธํ๋ค.** ๊ณผ์
์ ์์ํ์ฌ ์คํ์ ๋ฐฉ์งํ๊ณ ์์ ๋์๋ ฅ์ ๊ทน๋ํํด์ผ ํฉ๋๋ค.\n",
"\n",
"์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ณธ ๋ฏธ์
์์๋ **ํธ๋์ญ์
2,100๊ฑด์ ๊ธฐ์ค์ผ๋ก 4,200๊ฑดยท10,500๊ฑด๊น์ง ๋๋ ค ๋ฐฐ์น 50๊ฑด ๋จ์๋ก ํ์ ์์
์ ์ค๋ ๋ 4๊ฐ๊ฐ ์ฒ๋ฆฌํ๊ฒ ํ๊ณ , ๊ฐ์ ๋ฐฐ์น ์ค๋ณต ์ฌ์ ์ก(5%)ยท์๋ช
ํ ์๋ ๋ณ์กฐ(1%)ยท์์
์ ์ค๋ฅ(37๋ฐฐ์น๋ง๋ค) ์ฃผ์
ํ์ฌ ์ฒ๋ฆฌ ์ง์ฐ ๋ฐ ๋ณต์๋ ฅ์ ์ํํ๋** ์ค์ต์ ์ฒด๊ณ์ ์ผ๋ก ์ํํฉ๋๋ค.\n",
"\n",
"ํ์ต์๋ ์ค์ต๊ฐ์ด๋์ ์ ์๋ ์ด 4๊ฐ ์ธ๋ถ ํ๋(๊ธฐ๋ณธ ๊ธฐ๋ฅ ๊ตฌํ โ ๊ฒฐ๊ณผ ์งํ ๊ฒ์ฆ โ ํ๋ผ๋ฏธํฐ ํ๋ ๋ฐ ๋์ ๊ณผ์ )์ ์์ฐจ์ ์ผ๋ก ์งํํ๋ฉฐ ํต์ฌ ํ์ ๊ธฐ์ค์ ๋ฌ์ฑํฉ๋๋ค.\n",
"\n",
"**์ด๋ฒ ๊ฒฐ๊ณผ๋ฌผ:** `tests/dat10_stress_test.py ๋ฐ ์คํธ๋ ์ค ํ
์คํธ ๋ถ์ ๋ณด๊ณ ์` ํํ์ ๊ฒฐ๊ณผ๋ฌผ์ ์์ฑํ๋ค.\n",
"\n",
"### ๐ ํ๊ฐ ๋ฐ ๊ฒ์ฆ ๊ธฐ์ค (PASS ์กฐ๊ฑด)\n",
"#### ๋ฏธ์
ํ์ \n",
"\n",
"**ํ๊ฐ ๊ตฌ๋ถ:** ํ์\n",
"\n",
"**ํต์ฌ ํ๊ฐ์์ญ:** ๊ทน๋จ ์ํฉ์์์ ์์ ์คํจ(Fail-safe) ๋ฐ ๊ธฐ๋ฅ ์ถ์ ์๋ ์ ์ ์ฑ\n",
"\n",
"#### ๋ฏธ์
PASS ์กฐ๊ฑด\n",
"\n",
"- ์ ๋ ๊ธฐ์ค: ๋์ ํธ๋ํฝ 500% ์ฆ๊ฐ ์ ์์คํ
๋ค์ด 0๊ฑด, ๋ฐ์ดํฐ ์ ์ค๋ฅ 0%\n",
"\n",
"\n",
"- ์งํ ๊ธฐ์ค: ๋๋ ์๋ฎฌ๋ ์ด์
๋ถํ ์ฃผ์
ํ
์คํธ ํต๊ณผ ๋ฐ ๋ณ๋ชฉ ๊ตฌ๊ฐ ํ๋กํ์ผ๋ง ์๋ฃ\n",
"\n",
"\n",
"- ๊ฒ์ฆ ๋ฐฉ๋ฒ: ๋ถํ ํ
์คํธ ๋๊ตฌ๋ฅผ ํตํ ์ฒ๋ฆฌ๋(Throughput) ๋ฐ ๋ฉ๋ชจ๋ฆฌ ๋์ ๊ฒ์ฆ\n",
"\n",
"\n",
"#### ์ฑ์ ์ ํ์ ๋ฉ๋ชจ\n",
"\n",
"> ๋จ์ ์ฝ๋ ์คํ ์ฌ๋ถ๋ฟ๋ง ์๋๋ผ ์ถ๋ ฅ๋ ์์น์ ๊ทผ๊ฑฐ ์๋ฃ๊ฐ ๊ตฐ ์ค๋ฌด ์๊ตฌ์ฌํญ์ ๋ถํฉํ๋์ง ๋ฉด๋ฐํ ํ์ธํ๋ค.\n",
"\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"\"\"\"7์ผ๊ฐ ํต์ ์ด ๋๊ธด ๋ณด๊ธ์ 5๊ณณ์ ๋์ ํธ๋์ญ์
์ ๋ณต๊ตฌ ์งํ ์๋ฒ๋ก ํ๊บผ๋ฒ์ ์ฌ๋ฆฌ๋ ์ํฉ์ ๊ธฐ์ค 100%ยท200%ยท500%๋ก ๋๋ ค ํ ๊ธฐ๋ฐ ์์
์ ์ค๋ ๋๋ก ์ฒ๋ฆฌํ๊ณ , ์ค๋ณต ์ฌ์ ์กยท์๋ช
๋ถ์ผ์นยท์์
์ ์ค๋ฅ๋ฅผ ์ฃผ์
ํด ๋ค์ด 0๊ฑดยท์ ์ค 0๊ฑดยท๊ธฐ๋ฅ ์ถ์(ํ๋ณธ ์๋ช
์ ๊ฒ ํ ์ฌํ ๊ฒ์ฆ)ยท๋ณ๋ชฉ(๋ชฉ๋ก ํ์ ์ค๋ณต ํ์ธ์ ์งํฉ์ผ๋ก ๊ต์ฒด)ยท๋ฉ๋ชจ๋ฆฌ ์ฆ๊ฐ๋ฅผ ์ธก์ ํ๋ค.\"\"\"\n",
"import hashlib\n",
"import hmac\n",
"import queue\n",
"import threading\n",
"import time\n",
"import tracemalloc\n",
"import numpy as np\n",
"import pandas as pd\n",
"import matplotlib.pyplot as plt\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# [๊ต์ก์ฉ๊ฐ์์๋ฃ] ๊ฐ์ ์ผ์ ๋ณด๊ธ์ยท์
์ฒดยทํ๋ชฉ ์๋ฃ ์์ฑ๊ธฐ (์ค์ ๋ถ๋ยท์
์ฒดยทํ๋ชฉยท์ฌ๊ณ ์ ๋ณด ์์)\n",
"# ๊ฐ์ ๋ณด๊ธ์ 5๊ณณ์ด ํ๋ชฉ 12์ข
์ ์
๊ณ ยท๋ถ์ถยท์กฐ์ ํธ๋์ญ์
์ ์คํ๋ผ์ธ์ผ๋ก ๊ธฐ๋กํ๊ณ ๋์ค์ ๋๊ธฐํํ๋ค.\n",
"# ๊ณต๊ธ๋ง ์ฌ๋ ์งํ(์
์ฒด ๋ฉ๊ธฐ ์ง์ฐ, ํ๋ชฉ ์ฌ๊ณ ๊ธ๊ฐ)๋ ๋๊ธฐํ๋ ์๋ฃ๋ฅผ ๋ง์ด๋ํด ์๋ณํ๋ค.\n",
"# ---------------------------------------------------------------------------\n",
"NODES = [\"๋ณด๊ธ์A\", \"๋ณด๊ธ์B\", \"๋ณด๊ธ์C\", \"๋ณด๊ธ์D\", \"๋ณด๊ธ์E\"]\n",
"SUPPLIERS = [\"๊ฐ์์
์ฒด1\", \"๊ฐ์์
์ฒด2\", \"๊ฐ์์
์ฒด3\", \"๊ฐ์์
์ฒด4\", \"๊ฐ์์
์ฒด5\", \"๊ฐ์์
์ฒด6\"]\n",
"CATS = [\"์ผ๋ฐ๋ณด๊ธ\", \"์ ๋น๋ถํ\", \"์๋ฌด์๋ชจ\"]\n",
"ITEMS = [{\"item\": f\"I{i + 1:02d}\", \"cat\": CATS[i % 3], \"supplier\": SUPPLIERS[i % 6], \"lead_days\": 3 + (i * 5) % 12, \"use\": 4 + (i * 7) % 11, \"min_level\": 30 + 10 * (i % 5)} for i in range(12)]\n",
"ITEM_IDS = [x[\"item\"] for x in ITEMS]\n",
"CTXS = [\"ํ์\", \"์ผ๊ฐ\", \"ํต์ ๋ถ๋\"]\n",
"\n",
"\n",
"def make_txns(seed=7, days=7, per_node_day=60, nodes=None):\n",
" \"\"\"๋
ธ๋๋ณ ์คํ๋ผ์ธ ํธ๋์ญ์
๋ชฉ๋ก. id๋ ๋
ธ๋-์ผ๋ จ๋ฒํธ(๋ฉฑ๋ฑ ํค), t๋ ์๊ฐ(์๊ฐ ๋จ์), ctx๋ ์
๋ ฅ ์ํฉ.\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" out = []\n",
" for n in (nodes or NODES):\n",
" seq = 0\n",
" for k in range(days * per_node_day):\n",
" seq += 1\n",
" it = ITEMS[int(rng.integers(len(ITEMS)))]\n",
" t = float(rng.uniform(0, 24 * days))\n",
" ctx = \"์ผ๊ฐ\" if (t % 24 >= 20 or t % 24 < 5) else \"ํ์\"\n",
" if rng.random() < 0.12:\n",
" ctx = \"ํต์ ๋ถ๋\"\n",
" r = rng.random()\n",
" if r < 0.22:\n",
" op, q = \"in\", int(it[\"use\"] * rng.uniform(4, 9))\n",
" elif r < 0.95:\n",
" op, q = \"out\", int(max(1, rng.normal(it[\"use\"] * 1.5, 3)))\n",
" else:\n",
" op, q = \"adjust\", int(rng.integers(-5, 6))\n",
" out.append({\"id\": f\"{n}-{seq:05d}\", \"node\": n, \"item\": it[\"item\"], \"op\": op, \"qty\": q, \"t\": round(t, 2), \"ctx\": ctx, \"supplier\": it[\"supplier\"]})\n",
" return out\n",
"\n",
"\n",
"def stock_of(txns, base=500):\n",
" \"\"\"ํ๋ชฉ๋ณ ์ฌ๊ณ = ๊ธฐ์ด ์ฌ๊ณ + ์
๊ณ - ๋ถ์ถ + ์กฐ์ (id ์ค๋ณต์ ํ ๋ฒ๋ง ๋ฐ์).\"\"\"\n",
" st, seen = {i: base for i in ITEM_IDS}, set()\n",
" for x in txns:\n",
" if x[\"id\"] in seen:\n",
" continue\n",
" seen.add(x[\"id\"])\n",
" st[x[\"item\"]] += x[\"qty\"] if x[\"op\"] in (\"in\", \"adjust\") else -x[\"qty\"]\n",
" return st\n",
"\n",
"KEYS = {n: hashlib.sha256(f\"demo-key-{n}\".encode()).digest() for n in NODES} # [๊ต์ก์ฉ๊ฐ์์๋ฃ] ๋ฐ๋ชจ ํค\n",
"BATCH, QMAX, HIGH_WATER, WORKERS = 50, 12, 8, 4\n",
"\n",
"\n",
"def sig_of(x):\n",
" return hmac.new(KEYS[x[\"node\"]], f\"{x['id']}|{x['item']}|{x['op']}|{x['qty']}|{x['t']}\".encode(), \"sha256\").hexdigest()[:16]\n",
"\n",
"\n",
"class Server:\n",
" def __init__(self, fast):\n",
" self.fast, self.lock = fast, threading.Lock()\n",
" self.ids = set() if fast else [] # ์ต์ ํ ์ : ๋ชฉ๋ก ์ ํ ํ์์ผ๋ก ์ค๋ณต ํ์ธ\n",
" self.stock = {i: 500 for i in ITEM_IDS}; self.quar, self.dups, self.unver, self.lite_batches, self.applied = [], 0, [], 0, 0\n",
"\n",
" def ingest(self, batch, lite):\n",
" ok_rows, unv = [], set()\n",
" for k, x in enumerate(batch):\n",
" if lite and k % 10: # ๊ธฐ๋ฅ ์ถ์: ํ๋ณธ 10%๋ง ์๋ช
์ ๊ฒ, ๋๋จธ์ง๋ ์ฌํ ๊ฒ์ฆ ๋์\n",
" good = True; unv.add(x[\"id\"])\n",
" else:\n",
" good = hmac.compare_digest(sig_of(x), x[\"sig\"])\n",
" if not good:\n",
" with self.lock:\n",
" self.quar.append(x[\"id\"])\n",
" continue\n",
" ok_rows.append(x)\n",
" with self.lock:\n",
" self.lite_batches += lite\n",
" for x in ok_rows:\n",
" dup = x[\"id\"] in self.ids\n",
" if dup:\n",
" self.dups += 1; continue\n",
" self.ids.add(x[\"id\"]) if self.fast else self.ids.append(x[\"id\"])\n",
" self.stock[x[\"item\"]] += x[\"qty\"] if x[\"op\"] in (\"in\", \"adjust\") else -x[\"qty\"]; self.applied += 1\n",
" if x[\"id\"] in unv:\n",
" self.unver.append(x) # ์ค์ ๋ก ๋ฐ์๋ ๋ฏธ๊ฒ์ฆ ๋ ์ฝ๋๋ง ์ฌํ ๊ฒ์ฆ ๋์\n",
"\n",
" def post_verify(self):\n",
" \"\"\"์ฌํ ๊ฒ์ฆ: ํ๋ณธ ์ ๊ฒ๋ง ๊ฑฐ์น ๋ ์ฝ๋๋ฅผ ์ ์ ์ ๊ฒํ๊ณ , ์์กฐ๋ก ํ์ธ๋๋ฉด ๋ฐ์๋ถ์ ๋๋๋ฆฐ๋ค.\"\"\"\n",
" rolled = 0\n",
" for x in self.unver:\n",
" if not hmac.compare_digest(sig_of(x), x[\"sig\"]):\n",
" self.stock[x[\"item\"]] -= x[\"qty\"] if x[\"op\"] in (\"in\", \"adjust\") else -x[\"qty\"]; self.quar.append(x[\"id\"]); self.applied -= 1; rolled += 1\n",
" return rolled\n",
"\n",
"\n",
"def run(mult, fast, seed=3):\n",
" rng = np.random.default_rng(seed)\n",
" tx = make_txns(7, 7, per_node_day=60 * mult)\n",
" for x in tx:\n",
" x[\"sig\"] = sig_of(x)\n",
" bad = set(rng.choice(len(tx), int(len(tx) * 0.01), replace=False).tolist())\n",
" for i in bad:\n",
" tx[i][\"qty\"] += 13 # ์๋ช
ํ ์๋ ๋ณ์กฐ(1%)\n",
" valid = [x for i, x in enumerate(tx) if i not in bad]\n",
" truth = stock_of(valid)\n",
" srv, q, lat, depth, dead, crash = Server(fast), queue.Queue(maxsize=QMAX), [], [], [], [0]\n",
" lk = threading.Lock(); cnt = {\"b\": 0}\n",
"\n",
" def worker():\n",
" while True:\n",
" item = q.get()\n",
" if item is None:\n",
" return\n",
" batch, t_in, retry = item\n",
" depth.append(q.qsize())\n",
" try:\n",
" with lk:\n",
" cnt[\"b\"] += 1; inj = (cnt[\"b\"] % 37 == 0) and not retry # 37๋ฒ์งธ ๋ฐฐ์น๋ง๋ค ์์
์ ์ค๋ฅ ์ฃผ์
(์ฌ์๋ 1ํ)\n",
" if inj:\n",
" raise RuntimeError(\"worker failure(injected)\")\n",
" srv.ingest(batch, lite=fast and q.qsize() >= HIGH_WATER)\n",
" except RuntimeError:\n",
" dead.append((batch, t_in)) # ๋ฐ๋๋ ํฐ ๋ณด๊ด ํ ์ฌ์ฒ๋ฆฌ(์ ์ค ๋ฐฉ์ง)\n",
" except Exception:\n",
" crash[0] += 1\n",
" lat.append((time.perf_counter() - t_in) * 1000)\n",
"\n",
" ths = [threading.Thread(target=worker) for _ in range(WORKERS)]\n",
" [t.start() for t in ths]\n",
" t0 = time.perf_counter()\n",
" batches = []\n",
" for n in NODES:\n",
" mine = [x for x in tx if x[\"node\"] == n]\n",
" batches += [mine[i:i + BATCH] for i in range(0, len(mine), BATCH)]\n",
" order = rng.permutation(len(batches)); resend = set(rng.choice(len(batches), max(1, len(batches) // 20), replace=False).tolist())\n",
" for i in order:\n",
" q.put((batches[i], time.perf_counter(), False)) # ๊ฐ๋ ์ฐจ๋ฉด ์์ฐ์๊ฐ ๊ธฐ๋ค๋ฆฐ๋ค(์ญ์, ์ ์ค ์์)\n",
" if i in resend:\n",
" q.put((batches[i], time.perf_counter(), False)) # ๊ฐ์ ๋ฐฐ์น ์ค๋ณต ์ฌ์ ์ก\n",
" for _ in ths:\n",
" q.put(None)\n",
" [t.join() for t in ths]\n",
" for b, t_in in dead:\n",
" srv.ingest(b, lite=False)\n",
" rolled = srv.post_verify() if fast else 0\n",
" dt = time.perf_counter() - t0\n",
" diff = sum(abs(srv.stock[i] - truth[i]) for i in ITEM_IDS)\n",
" lost = len(valid) - srv.applied\n",
" return {\"n\": len(tx), \"dt\": dt, \"tps\": len(tx) / dt, \"p95\": float(np.percentile(lat, 95)), \"qmax\": max(depth) if depth else 0, \"lite\": srv.lite_batches, \"dead\": len(dead),\n",
" \"dups\": srv.dups, \"quar\": len(set(srv.quar)), \"bad\": len(bad), \"lost\": lost, \"diff\": diff, \"crash\": crash[0], \"rolled\": rolled}\n",
"\n",
"\n",
"print(f\"[7์ผ ๋จ์ ํ ์ผ๊ด ๋๊ธฐํ] ๊ธฐ์ค {5 * 7 * 60}๊ฑด(5๊ณณ x 7์ผ x 60๊ฑด) -> 100%/200%/500%, ๋ฐฐ์น {BATCH}๊ฑด, ์์
์ {WORKERS}, ํ {QMAX}, ์ค๋ณต ์ฌ์ ์ก 5%, ์์กฐ 1%, ์ค๋ฅ ์ฃผ์
37๋ฐฐ์น๋ง๋ค\")\n",
"rows = []\n",
"for mult in (1, 2, 5):\n",
" r0, r1 = run(mult, False), run(mult, True); rows.append((mult, r0, r1))\n",
" print(f\" {mult * 100:3d}% ({r1['n']}๊ฑด): ์ต์ ํ ์ {r0['dt']:.2f}์ด({r0['tps']:.0f}๊ฑด/์ด, p95 {r0['p95']:.0f}ms) -> ํ {r1['dt']:.2f}์ด({r1['tps']:.0f}๊ฑด/์ด, p95 {r1['p95']:.0f}ms), ํ ์ต๋ {r1['qmax']}, ์ถ์ ๋ฐฐ์น {r1['lite']}\")\n",
"print(\"\\n[500% ์์ธ] ํญ๋ชฉ: ์ต์ ํ ์ / ํ\")\n",
"a, b = rows[-1][1], rows[-1][2]\n",
"for k, lab in ((\"dups\", \"์ค๋ณต ๊ฑฐ๋ถ\"), (\"quar\", \"์์กฐ ๊ฒฉ๋ฆฌ\"), (\"dead\", \"์์
์ ์ค๋ฅ ์ฌ์ฒ๋ฆฌ ๋ฐฐ์น\"), (\"rolled\", \"์ฌํ ๊ฒ์ฆ ๋๋๋ฆผ\"), (\"lost\", \"์ ์ค ๊ฑด์\"), (\"diff\", \"์ฌ๊ณ ์ฐจ์ด ํฉ๊ณ\"), (\"crash\", \"๋ฏธ์ฒ๋ฆฌ ์์ธ(๋ค์ด)\")):\n",
" print(f\" {lab:16s} {a[k]:6d} / {b[k]:6d}\")\n",
"lost_all = sum(r[1][\"lost\"] + r[2][\"lost\"] for r in rows); diff_all = sum(r[1][\"diff\"] + r[2][\"diff\"] for r in rows); crash_all = sum(r[1][\"crash\"] + r[2][\"crash\"] for r in rows)\n",
"quar_ok = all(r[2][\"quar\"] == r[2][\"bad\"] for r in rows)\n",
"print(f\"\\n์ ์ฒด 6ํ ์คํ: ๋ค์ด {crash_all}๊ฑด, ์ ์ค {lost_all}๊ฑด, ์ฌ๊ณ ์ฐจ์ด {diff_all}, ์์กฐ ์ ๋ถ ๊ฒฉ๋ฆฌ {'์' if quar_ok else '์๋์ค'} (๋ชฉํ ๋ค์ด 0๊ฑดยท์ ์ค 0%: {'๋ฌ์ฑ' if crash_all == 0 and lost_all == 0 and diff_all == 0 else '๋ฏธ๋ฌ'})\")\n",
"print(f\"๋ณ๋ชฉ ์ต์ ํ(500%): ์ค๋ณต ํ์ธ์ ๋ชฉ๋ก ํ์ -> ์งํฉ ์กฐํ๋ก ๊ต์ฒดํด {a['dt']:.2f}์ด -> {b['dt']:.2f}์ด({a['dt'] / b['dt']:.1f}๋ฐฐ), ์ฒ๋ฆฌ๋ {a['tps']:.0f} -> {b['tps']:.0f}๊ฑด/์ด\")\n",
"\n",
"# ---- ๋จ๊ณ๋ณ ๋ณ๋ชฉ ํ๋กํ์ผ๋ง(์๋ ํ์ด๋จธ): ์๋ช
์ ๊ฒ / ์ค๋ณต ํ์ธ(๋ชฉ๋กยท์งํฉ) ----\n",
"tx5 = make_txns(7, 7, per_node_day=300)\n",
"for x in tx5:\n",
" x[\"sig\"] = sig_of(x)\n",
"t0 = time.perf_counter(); [hmac.compare_digest(sig_of(x), x[\"sig\"]) for x in tx5]; t_sig = time.perf_counter() - t0\n",
"ids = [x[\"id\"] for x in tx5]; t0 = time.perf_counter(); lst = []\n",
"for i in ids:\n",
" _ = i in lst; lst.append(i)\n",
"t_lst = time.perf_counter() - t0; t0 = time.perf_counter(); st = set()\n",
"for i in ids:\n",
" _ = i in st; st.add(i)\n",
"t_set = time.perf_counter() - t0\n",
"print(f\"\\n[๋ณ๋ชฉ ํ๋กํ์ผ] {len(ids)}๊ฑด ๊ธฐ์ค: ์๋ช
์ ๊ฒ {t_sig * 1000:.0f}ms, ์ค๋ณต ํ์ธ ๋ชฉ๋ก {t_lst * 1000:.0f}ms / ์งํฉ {t_set * 1000:.1f}ms -> ๋ณ๋ชฉ์ ์ค๋ณต ํ์ธ์ด์๊ณ ์๋ช
์ ๊ฒ์ด ๋ค์ ๋ณ๋ชฉ\")\n",
"print(\"[๊ธฐ๋ฅ ์ถ์ ์ ์ฑ
] ํ๊ฐ %d๊ฑด ์ด์ ์์ด๋ฉด ์๋ช
์ ํ๋ณธ 10%%๋ง ์ ๊ฒํ๊ณ ๋๋จธ์ง๋ ์ฌํ ์ ์ ๊ฒ์ฆํ๋ค. ์์กฐ๊ฐ ํ์ธ๋๋ฉด ๋๋๋ฆฌ๊ณ ๊ฒฉ๋ฆฌํ๋ค. ๋ฐ์ดํฐ๋ ๋ฒ๋ฆฌ์ง ์๋๋ค.\" % HIGH_WATER)\n",
"\n",
"# ---- ๋ฉ๋ชจ๋ฆฌ ๋์ ์ ๊ฒ: ๊ฐ์ ๋ถํ๋ฅผ 3ํ ๋ฐ๋ณตํด ๋จ๋ ๋ฉ๋ชจ๋ฆฌ ----\n",
"tracemalloc.start(); mem = []\n",
"for _ in range(3):\n",
" run(1, True); mem.append(tracemalloc.get_traced_memory()[0] / 1e6)\n",
"tracemalloc.stop()\n",
"print(f\"[๋ฉ๋ชจ๋ฆฌ] ๋ฐ๋ณต ์คํ ํ ๋จ์ ๋ฉ๋ชจ๋ฆฌ {', '.join(f'{m:.1f}MB' for m in mem)} -> ์ฆ๊ฐ {mem[-1] - mem[0]:+.1f}MB ({'๋์ ์งํ ์์' if mem[-1] - mem[0] < 5 else 'ํ์ธ ํ์'})\")\n",
"print(\"โป ์ฒ๋ฆฌ ์๊ฐ์ ์คํ ํ๊ฒฝ์ ๋ฐ๋ผ ๋ฌ๋ผ์ง๋ค. ์ค์ ์๋ฒ๋ ๋ณ๋ ์์
ํยท์์ ์ ์ฅ์ยท๋ถํ ์ํ ๋๊ตฌ๊ฐ ํ์ํ๋ค. ์ด ์ํ์ ๋จ์ผ ํ๋ก์ธ์ค ๋ชจ์์ด๋ค.\")\n",
"\n",
"plt.figure(figsize=(7, 3.6)); x = np.arange(3)\n",
"plt.bar(x - .2, [r[1][\"dt\"] for r in rows], .4, label=\"before (list scan)\", color=\"tab:gray\"); plt.bar(x + .2, [r[2][\"dt\"] for r in rows], .4, label=\"after (set + lite mode)\", color=\"tab:blue\")\n",
"plt.xticks(x, [\"100%\", \"200%\", \"500%\"]); plt.ylabel(\"seconds\"); plt.title(\"Bulk sync time after 7-day outage\"); plt.legend(); plt.tight_layout(); plt.show()\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"๐ก [์ฐธ๊ณ ] ๋ฏธ์
08 ์์ ์คํ ๊ฒฐ๊ณผ (Output)
\n",
"\n",
"```text\n",
"[7์ผ ๋จ์ ํ ์ผ๊ด ๋๊ธฐํ] ๊ธฐ์ค 2100๊ฑด(5๊ณณ x 7์ผ x 60๊ฑด) -> 100%/200%/500%, ๋ฐฐ์น 50๊ฑด, ์์
์ 4, ํ 12, ์ค๋ณต ์ฌ์ ์ก 5%, ์์กฐ 1%, ์ค๋ฅ ์ฃผ์
37๋ฐฐ์น๋ง๋ค\n",
" 100% (2100๊ฑด): ์ต์ ํ ์ 0.05์ด(45132๊ฑด/์ด, p95 21ms) -> ํ 0.01์ด(161281๊ฑด/์ด, p95 2ms), ํ ์ต๋ 11, ์ถ์ ๋ฐฐ์น 15\n",
" 200% (4200๊ฑด): ์ต์ ํ ์ 0.16์ด(26448๊ฑด/์ด, p95 45ms) -> ํ 0.03์ด(126250๊ฑด/์ด, p95 6ms), ํ ์ต๋ 11, ์ถ์ ๋ฐฐ์น 27\n",
" 500% (10500๊ฑด): ์ต์ ํ ์ 1.00์ด(10489๊ฑด/์ด, p95 134ms) -> ํ 0.06์ด(165580๊ฑด/์ด, p95 3ms), ํ ์ต๋ 11, ์ถ์ ๋ฐฐ์น 68\n",
"\n",
"[500% ์์ธ] ํญ๋ชฉ: ์ต์ ํ ์ / ํ\n",
" ์ค๋ณต ๊ฑฐ๋ถ 496 / 497\n",
" ์์กฐ ๊ฒฉ๋ฆฌ 105 / 105\n",
" ์์
์ ์ค๋ฅ ์ฌ์ฒ๋ฆฌ ๋ฐฐ์น 5 / 5\n",
" ์ฌํ ๊ฒ์ฆ ๋๋๋ฆผ 0 / 32\n",
" ์ ์ค ๊ฑด์ 0 / 0\n",
" ์ฌ๊ณ ์ฐจ์ด ํฉ๊ณ 0 / 0\n",
" ๋ฏธ์ฒ๋ฆฌ ์์ธ(๋ค์ด) 0 / 0\n",
"\n",
"์ ์ฒด 6ํ ์คํ: ๋ค์ด 0๊ฑด, ์ ์ค 0๊ฑด, ์ฌ๊ณ ์ฐจ์ด 0, ์์กฐ ์ ๋ถ ๊ฒฉ๋ฆฌ ์ (๋ชฉํ ๋ค์ด 0๊ฑดยท์ ์ค 0%: ๋ฌ์ฑ)\n",
"๋ณ๋ชฉ ์ต์ ํ(500%): ์ค๋ณต ํ์ธ์ ๋ชฉ๋ก ํ์ -> ์งํฉ ์กฐํ๋ก ๊ต์ฒดํด 1.00์ด -> 0.06์ด(15.8๋ฐฐ), ์ฒ๋ฆฌ๋ 10489 -> 165580๊ฑด/์ด\n",
"\n",
"[๋ณ๋ชฉ ํ๋กํ์ผ] 10500๊ฑด ๊ธฐ์ค: ์๋ช
์ ๊ฒ 41ms, ์ค๋ณต ํ์ธ ๋ชฉ๋ก 765ms / ์งํฉ 2.1ms -> ๋ณ๋ชฉ์ ์ค๋ณต ํ์ธ์ด์๊ณ ์๋ช
์ ๊ฒ์ด ๋ค์ ๋ณ๋ชฉ\n",
"[๊ธฐ๋ฅ ์ถ์ ์ ์ฑ
] ํ๊ฐ 8๊ฑด ์ด์ ์์ด๋ฉด ์๋ช
์ ํ๋ณธ 10%๋ง ์ ๊ฒํ๊ณ ๋๋จธ์ง๋ ์ฌํ ์ ์ ๊ฒ์ฆํ๋ค. ์์กฐ๊ฐ ํ์ธ๋๋ฉด ๋๋๋ฆฌ๊ณ ๊ฒฉ๋ฆฌํ๋ค. ๋ฐ์ดํฐ๋ ๋ฒ๋ฆฌ์ง ์๋๋ค.\n",
"[๋ฉ๋ชจ๋ฆฌ] ๋ฐ๋ณต ์คํ ํ ๋จ์ ๋ฉ๋ชจ๋ฆฌ 0.0MB, 0.0MB, 0.0MB -> ์ฆ๊ฐ +0.0MB (๋์ ์งํ ์์)\n",
"โป ์ฒ๋ฆฌ ์๊ฐ์ ์คํ ํ๊ฒฝ์ ๋ฐ๋ผ ๋ฌ๋ผ์ง๋ค. ์ค์ ์๋ฒ๋ ๋ณ๋ ์์
ํยท์์ ์ ์ฅ์ยท๋ถํ ์ํ ๋๊ตฌ๊ฐ ํ์ํ๋ค. ์ด ์ํ์ ๋จ์ผ ํ๋ก์ธ์ค ๋ชจ์์ด๋ค.\n",
"```\n",
" \n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## ๐ฏ ๋ฏธ์
09. AAR ๊ธฐ๋ฐ ๋๊ธฐํ ๋๋ฝ ๋ฐ ๋ฐ์ดํฐ ์ถฉ๋ ์ฌ๋ก ๋ถ์ ๋ฐ ๋๊ธฐํ ๋ฃฐ์
์ฌํ๋\n",
"**์์์๊ฐ**: ์ฝ 3.0์๊ฐ\n",
"\n",
"### ๐ ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ์ํ ์ง์นจ\n",
"#### ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ๋น์์ฑ\n",
"\n",
"AAR ๊ธฐ๋ฐ ๋๊ธฐํ ๋๋ฝ ๋ฐ ๋ฐ์ดํฐ ์ถฉ๋ ์ฌ๋ก๋ฅผ ๋ถ์ํ๊ณ ๋๊ธฐํ ๋ฃฐ์ ์ฌํ๋ํ๋ค. ์ค์ ์ ์ ํ์ฅ์์๋ ๋จ์ ํ์ง์ ๊ทธ์น์ง ์๊ณ ์ ์ํ ์ํ ํ๋จ๊ณผ ์ ๋ขฐ๋ ๋์ ์์ฌ๊ฒฐ์ ์ด ํ์์ ์ด๋ฏ๋ก, **๋๊ธฐํ ๋๋ฝยท์ถฉ๋ ํ์ ์ค๋ฅ ์ฌ๋ก๋ฅผ AAR๋ก ์์ธ๋ณ๋ก ๋ถ๋ฅํ๊ณ ๊ท์น ํ๋ผ๋ฏธํฐ(์ค๋ณต ํ์ ๋ฐฉ์, ์ถฉ๋ ํ์ง ๊ท์น, ์๋ ๋ณํฉ ์๊ณ, ์ฌ์๋ ํ์)๋ฅผ ์ฌํ๋ํด ์ข
ํฉ ์ ํ๋ 10%p ์ด์ ํฅ์๊ณผ ์ฌ๋ฐ ์ค๋ฅ 0๊ฑด์ ์ํํ๋ค.** ๊ณผ์
์ ์์ํ์ฌ ์คํ์ ๋ฐฉ์งํ๊ณ ์์ ๋์๋ ฅ์ ๊ทน๋ํํด์ผ ํฉ๋๋ค.\n",
"\n",
"์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ณธ ๋ฏธ์
์์๋ **๊ฐ์ ์ฌ๋ก 1,200๊ฑด(์ค๋ณต ์ฌ์ ์กยท์ฌ์
๋ ฅ, ๊ฐ์ ํฌ๊ธฐ์ ์๋ก ๋ค๋ฅธ ๊ฑฐ๋, ๋์ยทํ์ ๋ฉํ ์์ ๊ณผ ์๊ณ ์ด๊ธ๋จ, ์๋ ์ฐจ์ด, ํต์ ๋ถ๋ ์ ์ก ์คํจ)์ ํํ ๊ท์น์ ์ ์ฉํด ์ค๋ฅ๋ฅผ D1~D6 ์์ธ์ผ๋ก ๋ถ๋ฅ ์ํํ๊ณ ๊ฒ์ฆํ๋** ์ค์ต์ ์ฒด๊ณ์ ์ผ๋ก ์ํํฉ๋๋ค.\n",
"\n",
"ํ์ต์๋ ์ค์ต๊ฐ์ด๋์ ์ ์๋ ์ด 4๊ฐ ์ธ๋ถ ํ๋(๊ธฐ๋ณธ ๊ธฐ๋ฅ ๊ตฌํ โ ๊ฒฐ๊ณผ ์งํ ๊ฒ์ฆ โ ํ๋ผ๋ฏธํฐ ํ๋ ๋ฐ ๋์ ๊ณผ์ )์ ์์ฐจ์ ์ผ๋ก ์งํํ๋ฉฐ ํต์ฌ ํ์ ๊ธฐ์ค์ ๋ฌ์ฑํฉ๋๋ค.\n",
"\n",
"**์ด๋ฒ ๊ฒฐ๊ณผ๋ฌผ:** `docs/dat10_aar_report.md ๋ฐ configs/dat10_tuned_params.yaml` ํํ์ ๊ฒฐ๊ณผ๋ฌผ์ ์์ฑํ๋ค.\n",
"\n",
"### ๐ ํ๊ฐ ๋ฐ ๊ฒ์ฆ ๊ธฐ์ค (PASS ์กฐ๊ฑด)\n",
"#### ๋ฏธ์
ํ์ \n",
"\n",
"**ํ๊ฐ ๊ตฌ๋ถ:** ํ์\n",
"\n",
"**ํต์ฌ ํ๊ฐ์์ญ:** ์ค๋ฅ ์์ธ ๊ท๋ช
์ ๊ฐ๊ด์ฑ ๋ฐ ๊ฐ์ ์กฐ์น์ ๋
ผ๋ฆฌ์ ํ๋น์ฑ\n",
"\n",
"#### ๋ฏธ์
PASS ์กฐ๊ฑด\n",
"\n",
"- ์ ๋ ๊ธฐ์ค: AAR ํผ๋๋ฐฑ ์ ์ฉ ํ ์ข
ํฉ ์ ํ๋ 10%p ์ด์ ํฅ์, ์ฌ๋ฐ ์ค๋ฅ 0๊ฑด\n",
"\n",
"\n",
"- ์งํ ๊ธฐ์ค: AAR ๋ณด๊ณ ์ ์์ฑ ๋ฐ ํ๋ผ๋ฏธํฐ ํ๋ ์ ํ ํ๊ท ํ
์คํธ ํต๊ณผ\n",
"\n",
"\n",
"- ๊ฒ์ฆ ๋ฐฉ๋ฒ: ํ๊ท ํ
์คํธ(Regression Test) ์ค์ํธ ์ ์ฒด ์๋ ์คํ ๋ฐ ๋น๊ต\n",
"\n",
"\n",
"#### ์ฑ์ ์ ํ์ ๋ฉ๋ชจ\n",
"\n",
"> ๋จ์ ์ฝ๋ ์คํ ์ฌ๋ถ๋ฟ๋ง ์๋๋ผ ์ถ๋ ฅ๋ ์์น์ ๊ทผ๊ฑฐ ์๋ฃ๊ฐ ๊ตฐ ์ค๋ฌด ์๊ตฌ์ฌํญ์ ๋ถํฉํ๋์ง ๋ฉด๋ฐํ ํ์ธํ๋ค.\n",
"\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"\"\"\"๋๊ธฐํ ์ฌํ ๊ฒํ (AAR) ์ฌ๋ก 1,200๊ฑด์์ ํ์ ์ค๋ฅ๋ฅผ ์์ธ(์ค๋ณต ํ์ ๋ฐฉ์ยท์๊ณ ์ด๊ธ๋จยท์ถฉ๋ ํ์ง ๊ท์นยท์๋ ๋ณํฉ ์๊ณยท์ฌ์๋ ํ์)๋ณ๋ก ๋ถ๋ฅํ๊ณ ๊ท์น ํ๋ผ๋ฏธํฐ๋ฅผ ์ฌํ๋ํด, ๋ณ๋ ์ํ 3,000๊ฑด์ ์ข
ํฉ ์ ํ๋ ํฅ์ํญยทAAR ์ฌ๋ก ์ฌ๋ฐ ์ค๋ฅยทํ๊ท ํ
์คํธ ํต๊ณผ๋ฅผ ๊ณ์ฐํ๊ณ ํ๋ ๊ฒฐ๊ณผ๋ฅผ YAML ๋ฌธ์์ด๋ก ์ถ๋ ฅํ๋ค.\"\"\"\n",
"import numpy as np\n",
"import pandas as pd\n",
"import matplotlib.pyplot as plt\n",
"\n",
"KINDS = [\"dup_resend\", \"distinct_similar\", \"meta_conflict\", \"meta_causal\", \"qty_diff\", \"send_fail\", \"dup_reinput\"]\n",
"PK = [.20, .12, .18, .15, .15, .15, .05]\n",
"CAUSE = {\"dup_resend\": \"D2 ์ฌ์ ์ก ์ค๋ณต ๋์นจ\", \"distinct_similar\": \"D1 ๋ค๋ฅธ ๊ฑฐ๋๋ฅผ ์ค๋ณต์ผ๋ก ํ์ \", \"meta_conflict\": \"D4 ๋์ ์์ ๋์นจ(์กฐ์ฉํ ๋ฎ์ด์ฐ๊ธฐ)\", \"meta_causal\": \"D3 ํ์ ์์ ์ ์น์ ์คํยท๋ถํ์ ๊ฒฝ๋ณด\",\n",
" \"qty_diff\": \"D5 ์๋ ๋ณํฉ ์๊ณ ๋ถ์ ์ \", \"send_fail\": \"D6 ์ฌ์๋ ๋ถ์กฑ ๋๋ฝ\", \"dup_reinput\": \"D2b ์ฌ์
๋ ฅ(์ ID) ์ค๋ณต ๋์นจ\"}\n",
"\n",
"\n",
"def make_events(seed, n):\n",
" \"\"\"[๊ต์ก์ฉ๊ฐ์์๋ฃ] ๋๊ธฐํ ์ฌ๋ก ์์ฑ. ๋จ๋ง ์๊ณ ์ค์ฐจ(+-2์๊ฐ), ๋ฒ์ ๋ฒกํฐ ์๋ ๊ตฌํ ๊ธฐ๋ก 8%, ํต์ ๋ถ๋ ์ํฉ์ ์ ์ก ์คํจ๋ฅผ ํฌํจํ๋ค.\"\"\"\n",
" rng = np.random.default_rng(seed); out = []\n",
" for i in range(n):\n",
" k = str(rng.choice(KINDS, p=PK)); o = rng.uniform(-2, 2, 2); ev = {\"id\": i, \"kind\": k, \"vv\": rng.random() > 0.08}\n",
" if k == \"dup_resend\":\n",
" ev.update(same_id=True, dt=0.0 if rng.random() < .5 else float(rng.uniform(0.5, 6)))\n",
" elif k == \"distinct_similar\":\n",
" ev.update(same_id=False, dt=float(rng.uniform(0, 0.5)))\n",
" elif k == \"dup_reinput\": # ์ค์
๋ ฅ ์ ์ ๋ฑ์ผ๋ก ๊ฐ์ ๋ด์ฉ์ ์ ID๋ก ๋ค์ ์
๋ ฅ\n",
" ev.update(same_id=False, dt=float(rng.uniform(0.1, 6)))\n",
" elif k in (\"meta_conflict\", \"meta_causal\"):\n",
" gap = float(rng.uniform(0.5, 60)) if k == \"meta_conflict\" else float(rng.uniform(0.01, 1.5))\n",
" ev.update(ta=100 + o[0], tb=100 + gap + o[1]) # ๊ธฐ๋ก๋ ์๊ฐ = ์ค์ ์๊ฐ + ๋จ๋ง ์๊ณ ์ค์ฐจ\n",
" elif k == \"qty_diff\":\n",
" ev.update(d=float(rng.uniform(0.0, 0.8)))\n",
" else:\n",
" ev.update(p=0.5 if rng.random() < .6 else 0.8, u=rng.random(8))\n",
" out.append(ev)\n",
" return out\n",
"\n",
"\n",
"def decide(ev, P):\n",
" k = ev[\"kind\"]\n",
" if k in (\"dup_resend\", \"distinct_similar\", \"dup_reinput\"):\n",
" if P[\"dup\"] == \"id\":\n",
" return \"skip\" if ev[\"same_id\"] else \"apply\"\n",
" if P[\"dup\"] == \"id+content\":\n",
" return \"skip\" if ev[\"same_id\"] or ev[\"dt\"] <= P[\"win\"] else \"apply\"\n",
" return \"skip\" if ev[\"dt\"] <= P[\"win\"] else \"apply\" # ๋ด์ฉ ๋น๊ต: ๊ฐ์ ํ๋ชฉยท์๋์ด ์๊ฐ์ฐฝ ์์ด๋ฉด ์ค๋ณต\n",
" if k in (\"meta_conflict\", \"meta_causal\"):\n",
" if P[\"conflict\"] == \"vv\" and ev[\"vv\"]:\n",
" return \"hitl\" if k == \"meta_conflict\" else \"pick_b\"\n",
" if abs(ev[\"ta\"] - ev[\"tb\"]) <= P[\"tw\"]:\n",
" return \"hitl\"\n",
" return \"pick_b\" if ev[\"tb\"] > ev[\"ta\"] else \"pick_a\" # ๋ง์ง๋ง ๊ธฐ๋ก ์ฐ์ (LWW)\n",
" if k == \"qty_diff\":\n",
" return \"auto\" if ev[\"d\"] < P[\"thr\"] else \"hitl\"\n",
" return \"applied\" if (ev[\"u\"][:1 + P[\"retries\"]] < ev[\"p\"]).any() else \"failed\"\n",
"\n",
"\n",
"def truth(ev):\n",
" k = ev[\"kind\"]\n",
" if k == \"qty_diff\":\n",
" return \"auto\" if ev[\"d\"] < 0.25 else \"hitl\"\n",
" return {\"dup_resend\": \"skip\", \"dup_reinput\": \"skip\", \"distinct_similar\": \"apply\", \"meta_conflict\": \"hitl\", \"meta_causal\": \"pick_b\", \"send_fail\": \"applied\"}[k]\n",
"\n",
"\n",
"def evaluate(evs, P):\n",
" ok = [decide(e, P) == truth(e) for e in evs]\n",
" return np.mean(ok), pd.Series([CAUSE[e[\"kind\"]] for e, o in zip(evs, ok) if not o]).value_counts(), ok\n",
"\n",
"\n",
"P0 = {\"dup\": \"content\", \"win\": 2.0, \"conflict\": \"time\", \"tw\": 4.0, \"thr\": 0.3, \"retries\": 2} # ์ด๊ธฐ ์ด์ฉ ๊ท์น(M3 ๋จ๊ณ ์ค์ )\n",
"aar, test = make_events(31, 1200), make_events(77, 3000)\n",
"a0, causes, ok0 = evaluate(aar, P0)\n",
"print(f\"[AAR ์ฌ๋ก] {len(aar)}๊ฑด, ํํ ๊ท์น ์ ํ๋ {a0:.1%}, ์ค๋ฅ {int(len(aar) * (1 - a0))}๊ฑด\")\n",
"print(\"[์์ธ ๋ถ๋ฅ] ์์ธ / ๊ฑด์ / ๋น์จ\")\n",
"for c, n in causes.items():\n",
" print(f\" {c:28s} {n:4d}๊ฑด {n / (len(aar) * (1 - a0)):5.1%}\")\n",
"\n",
"# ---- ์ฌํ๋: ์ฌ๋ก ์ข
๋ฅ๊ฐ ์๋ก ๋ค๋ฅด๋ฏ๋ก ํ๋ผ๋ฏธํฐ๋ง๋ค AAR ์ฌ๋ก์์ ๋ฐ๋ก ํ์(์ฌ์๋๋ ํ์๋น ์ง์ฐ ๋น์ฉ 0.2%p ๊ฐ์ฐ) ----\n",
"def best(param, grid, kinds, cost=lambda v: 0.0):\n",
" sub = [e for e in aar if e[\"kind\"] in kinds]; sc = []\n",
" for v in grid:\n",
" P = {**P0, **(v if isinstance(v, dict) else {param: v})}; sc.append((np.mean([decide(e, P) == truth(e) for e in sub]) - cost(v), v))\n",
" return max(sc, key=lambda x: x[0]), sc\n",
"(_, dup_best), _ = best(\"dup\", [{\"dup\": \"content\", \"win\": w} for w in (0.0, 0.25, 0.5, 1, 2, 4)] + [{\"dup\": \"id+content\", \"win\": w} for w in (0.25, 0.5, 1, 2, 4)] + [{\"dup\": \"id\"}], (\"dup_resend\", \"distinct_similar\", \"dup_reinput\"))\n",
"(_, cf_best), _ = best(\"conflict\", [{\"conflict\": \"time\", \"tw\": w} for w in (0.25, 0.5, 1, 2, 4)] + [{\"conflict\": \"vv\", \"tw\": w} for w in (0.5, 1, 2, 4, 8)], (\"meta_conflict\", \"meta_causal\"))\n",
"(_, thr_best), _ = best(\"thr\", [0.1, 0.15, 0.2, 0.25, 0.3, 0.4, 0.5], (\"qty_diff\",))\n",
"(_, rt_best), _ = best(\"retries\", [1, 2, 3, 4, 5, 6], (\"send_fail\",), cost=lambda v: 0.002 * v)\n",
"P1 = {**P0, **dup_best, **cf_best, \"thr\": thr_best, \"retries\": rt_best}\n",
"\n",
"a_aar, rec, ok1 = evaluate(aar, P1)\n",
"a_old_t, c_old_t, _ = evaluate(test, P0)\n",
"a_new_t, c_new_t, _ = evaluate(test, P1)\n",
"rec_n = int(sum((not o0) and (not o1) for o0, o1 in zip(ok0, ok1)))\n",
"print(f\"\\n[์ฌํ๋ ๊ฒฐ๊ณผ] ์ค๋ณต ํ์ {P0['dup']}(์ฐฝ {P0['win']}h) -> {P1['dup']}{'' if P1['dup'] == 'id' else '(์ฐฝ ' + str(P1['win']) + 'h)'}, ์ถฉ๋ ํ์ง {P0['conflict']}(์ฐฝ {P0['tw']}h) -> {P1['conflict']}(๊ตฌํ ๊ธฐ๋ก์ ์๊ฐ์ฐฝ {P1['tw']}h), ์๋ ๋ณํฉ ์๊ณ {P0['thr']} -> {P1['thr']}, ์ฌ์๋ {P0['retries']} -> {P1['retries']}ํ\")\n",
"print(f\"AAR ์ฌ๋ก ์ฌ์คํ: ์ ํ๋ {a0:.1%} -> {a_aar:.1%}, ๊ฐ์ ์ฌ๋ก์์ ์ค๋ฅ ์ฌ๋ฐ {rec_n}๊ฑด (๋ชฉํ 0๊ฑด: {'๋ฌ์ฑ' if rec_n == 0 else '๋ฏธ๋ฌ'})\")\n",
"print(f\"๋ณ๋ ์ํ 3,000๊ฑด: {a_old_t:.1%} -> {a_new_t:.1%} ({(a_new_t - a_old_t) * 100:+.1f}%p, ๋ชฉํ +10%p ์ด์: {'๋ฌ์ฑ' if a_new_t - a_old_t >= .10 else '๋ฏธ๋ฌ'})\")\n",
"print(\"์ํ 3,000๊ฑด ๋จ์ ์ค๋ฅ:\", {c: int(n) for c, n in c_new_t.items()} or \"์์\")\n",
"tt = pd.DataFrame({\"before\": c_old_t, \"after\": c_new_t}).fillna(0).astype(int)\n",
"\n",
"# ---- ํ๊ท ํ
์คํธ: ๊ท์น ๋จ์ ์ํ 8๊ฐ๋ฅผ ํํ/์ฌํ๋ ๊ท์น์ ๊ฐ๊ฐ ์คํ ----\n",
"T = [(\"๊ฐ์ ID ์ฌ์ ์ก(์๊ฐ 3h ์ฐจ์ด)์ ๊ฑด๋๋\", {\"kind\": \"dup_resend\", \"same_id\": True, \"dt\": 3.0, \"vv\": True}, \"skip\"), (\"๊ฐ์ ID ์ฌ์ ์ก(์๊ฐ ๋์ผ)์ ๊ฑด๋๋\", {\"kind\": \"dup_resend\", \"same_id\": True, \"dt\": 0.0, \"vv\": True}, \"skip\"),\n",
" (\"๋ค๋ฅธ ID์ ๊ฐ์ ์๋ ๊ฑฐ๋๋ ๋ฐ์\", {\"kind\": \"distinct_similar\", \"same_id\": False, \"dt\": 0.2, \"vv\": True}, \"apply\"), (\"๋ฒ์ ๋ฒกํฐ ๋์ ์์ ์ ๊ตฐ์๊ด ๊ฒํ \", {\"kind\": \"meta_conflict\", \"ta\": 100, \"tb\": 130, \"vv\": True}, \"hitl\"),\n",
" (\"๋ฒ์ ๋ฒกํฐ ์ ํ ๊ด๊ณ๋ ํ์ ๊ฐ ์ฑํ(์๊ณ ์ญ์ )\", {\"kind\": \"meta_causal\", \"ta\": 101.0, \"tb\": 100.5, \"vv\": True}, \"pick_b\"), (\"์ฐจ์ด 10%๋ ์๋ ๋ณํฉ\", {\"kind\": \"qty_diff\", \"d\": 0.10, \"vv\": True}, \"auto\"),\n",
" (\"์ฐจ์ด 30%๋ ๊ตฐ์๊ด ๊ฒํ \", {\"kind\": \"qty_diff\", \"d\": 0.30, \"vv\": True}, \"hitl\"), (\"ํต์ ๋ถ๋ ์ ์ก 3ํ ์์ ์ฑ๊ณต\", {\"kind\": \"send_fail\", \"p\": 0.5, \"u\": np.array([.9, .8, .1, 0, 0, 0, 0, 0]), \"vv\": True}, \"applied\")]\n",
"print(\"\\n[ํ๊ท ํ
์คํธ 8๊ฐ] ํํ / ์ฌํ๋\")\n",
"p0 = [decide(e, P0) == t for _, e, t in T]; p1 = [decide(e, P1) == t for _, e, t in T]\n",
"for (nm, _, _), x, y in zip(T, p0, p1):\n",
" print(f\" {nm:34s} {'ํต๊ณผ' if x else '์คํจ'} / {'ํต๊ณผ' if y else '์คํจ'}\")\n",
"regress = sum(a and not b for a, b in zip(p0, p1))\n",
"print(f\"ํต๊ณผ {sum(p0)}/8 -> {sum(p1)}/8, ๊ธฐ์กด ํต๊ณผ ํญ๋ชฉ์ด ๊นจ์ง ํ๊ท {regress}๊ฑด ({'์ ์ฒด ํต๊ณผ' if all(p1) else '์คํจ ์์'})\")\n",
"\n",
"# ---- ์ฐ์ถ๋ฌผ: ํ๋ ํ๋ผ๋ฏธํฐ YAML ๋ฌธ์์ด(configs/dat10_tuned_params.yaml ๋ด์ฉ) ----\n",
"yml = \"\\n\".join([\"sync_rules:\", f\" dedup_key: {P1['dup']}\", f\" dedup_window_h: {P1['win'] if P1['dup'] != 'id' else 0}\", f\" conflict_detect: {P1['conflict']}\", f\" legacy_time_window_h: {P1['tw']}\", f\" auto_merge_diff_max: {P1['thr']}\", f\" send_retries: {P1['retries']}\", \" hitl_required_when: [conflict, diff_ge_0.25]\"])\n",
"print(\"\\n[configs/dat10_tuned_params.yaml]\"); print(yml)\n",
"print(\"[์ง์์ ํ์ต ์ฐ๊ณ] ์ด๋ฒ AAR ์ค๋ฅ ์ ํ์ ์ฌ๋ก ์ํ์ ๋์ -> ์ 1ํ ์ ์ฌ๋ก๋ก ํ๋ผ๋ฏธํฐ ์ฌํ์ -> ํ๊ท ํ
์คํธ 8๊ฐ ํต๊ณผ ์์๋ง ํ๋ณด ๋ฐ์ -> ๊ตฐ์๊ด ์น์ธ ํ ๋ฐฐํฌ(์๋ ๋ฐฐํฌ ๊ธ์ง)\")\n",
"print(\"โป ์ฌ๋ก๋ ๊ฐ์ ์์ฑ์ด๋ฉฐ ์ํ ๋ถํฌ๊ฐ ํ๋ ๋ถํฌ์ ๊ฐ์ ํฅ์ํญ์ด ์ค์ ๋ณด๋ค ๋๊ด์ ์ด๋ค. ๊ท์น ๋ณ๊ฒฝ์ ๊ตฐ์๊ด ์น์ธ๊ณผ ํ๊ท ํ
์คํธ๋ฅผ ๊ฑฐ์น๋ค.\")\n",
"\n",
"fig, ax = plt.subplots(1, 2, figsize=(10, 3.8))\n",
"ax[0].bar([\"before\", \"after\"], [a_old_t * 100, a_new_t * 100], color=[\"tab:gray\", \"tab:blue\"]); ax[0].set_ylim(50, 100); ax[0].set_ylabel(\"accuracy (%)\"); ax[0].set_title(\"Test accuracy (3,000 cases)\")\n",
"x = np.arange(len(tt)); ax[1].bar(x - .2, tt[\"before\"], .4, label=\"before\", color=\"tab:gray\"); ax[1].bar(x + .2, tt[\"after\"], .4, label=\"after\", color=\"tab:blue\")\n",
"ax[1].set_xticks(x); ax[1].set_xticklabels([c.split()[0] for c in tt.index]); ax[1].set_title(\"Errors by cause\"); ax[1].legend(); plt.tight_layout(); plt.show()\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"๐ก [์ฐธ๊ณ ] ๋ฏธ์
09 ์์ ์คํ ๊ฒฐ๊ณผ (Output)
\n",
"\n",
"```text\n",
"[AAR ์ฌ๋ก] 1200๊ฑด, ํํ ๊ท์น ์ ํ๋ 45.5%, ์ค๋ฅ 653๊ฑด\n",
"[์์ธ ๋ถ๋ฅ] ์์ธ / ๊ฑด์ / ๋น์จ\n",
" D4 ๋์ ์์ ๋์นจ(์กฐ์ฉํ ๋ฎ์ด์ฐ๊ธฐ) 193๊ฑด 29.5%\n",
" D3 ํ์ ์์ ์ ์น์ ์คํยท๋ถํ์ ๊ฒฝ๋ณด 177๊ฑด 27.1%\n",
" D1 ๋ค๋ฅธ ๊ฑฐ๋๋ฅผ ์ค๋ณต์ผ๋ก ํ์ 139๊ฑด 21.3%\n",
" D2 ์ฌ์ ์ก ์ค๋ณต ๋์นจ 95๊ฑด 14.5%\n",
" D2b ์ฌ์
๋ ฅ(์ ID) ์ค๋ณต ๋์นจ 25๊ฑด 3.8%\n",
" D6 ์ฌ์๋ ๋ถ์กฑ ๋๋ฝ 17๊ฑด 2.6%\n",
" D5 ์๋ ๋ณํฉ ์๊ณ ๋ถ์ ์ 8๊ฑด 1.2%\n",
"\n",
"[์ฌํ๋ ๊ฒฐ๊ณผ] ์ค๋ณต ํ์ content(์ฐฝ 2.0h) -> id, ์ถฉ๋ ํ์ง time(์ฐฝ 4.0h) -> vv(๊ตฌํ ๊ธฐ๋ก์ ์๊ฐ์ฐฝ 0.5h), ์๋ ๋ณํฉ ์๊ณ 0.3 -> 0.25, ์ฌ์๋ 2 -> 6ํ\n",
"AAR ์ฌ๋ก ์ฌ์คํ: ์ ํ๋ 45.5% -> 93.7%, ๊ฐ์ ์ฌ๋ก์์ ์ค๋ฅ ์ฌ๋ฐ 49๊ฑด (๋ชฉํ 0๊ฑด: ๋ฏธ๋ฌ)\n",
"๋ณ๋ ์ํ 3,000๊ฑด: 44.7% -> 93.0% (+48.3%p, ๋ชฉํ +10%p ์ด์: ๋ฌ์ฑ)\n",
"์ํ 3,000๊ฑด ๋จ์ ์ค๋ฅ: {'D2b ์ฌ์
๋ ฅ(์ ID) ์ค๋ณต ๋์นจ': 164, 'D4 ๋์ ์์ ๋์นจ(์กฐ์ฉํ ๋ฎ์ด์ฐ๊ธฐ)': 32, 'D3 ํ์ ์์ ์ ์น์ ์คํยท๋ถํ์ ๊ฒฝ๋ณด': 11, 'D6 ์ฌ์๋ ๋ถ์กฑ ๋๋ฝ': 2}\n",
"\n",
"[ํ๊ท ํ
์คํธ 8๊ฐ] ํํ / ์ฌํ๋\n",
" ๊ฐ์ ID ์ฌ์ ์ก(์๊ฐ 3h ์ฐจ์ด)์ ๊ฑด๋๋ ์คํจ / ํต๊ณผ\n",
" ๊ฐ์ ID ์ฌ์ ์ก(์๊ฐ ๋์ผ)์ ๊ฑด๋๋ ํต๊ณผ / ํต๊ณผ\n",
" ๋ค๋ฅธ ID์ ๊ฐ์ ์๋ ๊ฑฐ๋๋ ๋ฐ์ ์คํจ / ํต๊ณผ\n",
" ๋ฒ์ ๋ฒกํฐ ๋์ ์์ ์ ๊ตฐ์๊ด ๊ฒํ ์คํจ / ํต๊ณผ\n",
" ๋ฒ์ ๋ฒกํฐ ์ ํ ๊ด๊ณ๋ ํ์ ๊ฐ ์ฑํ(์๊ณ ์ญ์ ) ์คํจ / ํต๊ณผ\n",
" ์ฐจ์ด 10%๋ ์๋ ๋ณํฉ ํต๊ณผ / ํต๊ณผ\n",
" ์ฐจ์ด 30%๋ ๊ตฐ์๊ด ๊ฒํ ํต๊ณผ / ํต๊ณผ\n",
" ํต์ ๋ถ๋ ์ ์ก 3ํ ์์ ์ฑ๊ณต ํต๊ณผ / ํต๊ณผ\n",
"ํต๊ณผ 4/8 -> 8/8, ๊ธฐ์กด ํต๊ณผ ํญ๋ชฉ์ด ๊นจ์ง ํ๊ท 0๊ฑด (์ ์ฒด ํต๊ณผ)\n",
"\n",
"[configs/dat10_tuned_params.yaml]\n",
"sync_rules:\n",
" dedup_key: id\n",
" dedup_window_h: 0\n",
" conflict_detect: vv\n",
" legacy_time_window_h: 0.5\n",
" auto_merge_diff_max: 0.25\n",
" send_retries: 6\n",
" hitl_required_when: [conflict, diff_ge_0.25]\n",
"[์ง์์ ํ์ต ์ฐ๊ณ] ์ด๋ฒ AAR ์ค๋ฅ ์ ํ์ ์ฌ๋ก ์ํ์ ๋์ -> ์ 1ํ ์ ์ฌ๋ก๋ก ํ๋ผ๋ฏธํฐ ์ฌํ์ -> ํ๊ท ํ
์คํธ 8๊ฐ ํต๊ณผ ์์๋ง ํ๋ณด ๋ฐ์ -> ๊ตฐ์๊ด ์น์ธ ํ ๋ฐฐํฌ(์๋ ๋ฐฐํฌ ๊ธ์ง)\n",
"โป ์ฌ๋ก๋ ๊ฐ์ ์์ฑ์ด๋ฉฐ ์ํ ๋ถํฌ๊ฐ ํ๋ ๋ถํฌ์ ๊ฐ์ ํฅ์ํญ์ด ์ค์ ๋ณด๋ค ๋๊ด์ ์ด๋ค. ๊ท์น ๋ณ๊ฒฝ์ ๊ตฐ์๊ด ์น์ธ๊ณผ ํ๊ท ํ
์คํธ๋ฅผ ๊ฑฐ์น๋ค.\n",
"```\n",
" \n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## ๐ฏ ๋ฏธ์
10. ์คํ๋ผ์ธ ๋๊ธฐํ ์ผ์ ์ฌ๊ณ ๊ด๋ฆฌ SaaS ์ต์ข
ํจํค์ง ๋ฐ ์ผ์ ๋ฐฐํฌ ๊ฐ์ด๋ ์์ฑ\n",
"**์์์๊ฐ**: ์ฝ 3.5์๊ฐ\n",
"\n",
"### ๐ ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ์ํ ์ง์นจ\n",
"#### ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ๋น์์ฑ\n",
"\n",
"์คํ๋ผ์ธ ๋๊ธฐํ ์ผ์ ์ฌ๊ณ ๊ด๋ฆฌ SaaS ํจํค์ง ๋ฐ ๋ฐฐํฌ ๊ฐ์ด๋๋ฅผ ์์ฑํ๋ค. ์ค์ ์ ์ ํ์ฅ์์๋ ๋จ์ ํ์ง์ ๊ทธ์น์ง ์๊ณ ์ ์ํ ์ํ ํ๋จ๊ณผ ์ ๋ขฐ๋ ๋์ ์์ฌ๊ฒฐ์ ์ด ํ์์ ์ด๋ฏ๋ก, **์
๋ ฅ ๊ฒ์ฆยท์๋ช
์ ๊ฒยท์ค๋ณต ์ ๊ฑฐยทPN-์นด์ดํฐ ๋ณํฉยท์ถฉ๋ ํ์งยท๊ณต๊ธ๋ง ์ฌ๋ ์งํ ๋ง์ด๋ยท๊ฒํ ํยท๊ฐ์ฌ ๋ก๊ทธยท๋ธ๋ฆฌํ์ ๋ฌถ์ ์คํ๋ผ์ธ ๋๊ธฐํ ์ผ์ ์ฌ๊ณ ๊ด๋ฆฌ SaaS ํจํค์ง์ ๋ฐฐํฌ ๊ฐ์ด๋๋ฅผ ์์ฑํ๋ค.** ๊ณผ์
์ ์์ํ์ฌ ์คํ์ ๋ฐฉ์งํ๊ณ ์์ ๋์๋ ฅ์ ๊ทน๋ํํด์ผ ํฉ๋๋ค.\n",
"\n",
"์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ณธ ๋ฏธ์
์์๋ **์ ๋ฏธ์
์ ๊ธฐ๋ฅ์ ํตํฉ ํ์ดํ๋ผ์ธ์ผ๋ก ๋ฌถ์ด ์ค์น ์คํฌ๋ฆฝํธ 5๋จ๊ณ ์ ๊ฒ, ์๋ 4์ข
End-to-End ์คํ(์
์ฒด 1๊ณณ ๊ณต๊ธ ์ค๋จ, ๋
ธ๋ 1๊ณณ 3์ผ์น ๋ฏธ๋๊ธฐ, ๋ณ์กฐ 20ยท๊นจ์ง ์ค 5ยท์ค๋ณต 60๊ฑด ์ฃผ์
), ์์ฒด ์ ๊ฒ 11ํญ๋ชฉ๊ณผ ํจ์ ์ปค๋ฒ๋ฆฌ์ง ์ํํ๊ณ ๋ถ์ํ๋** ์ค์ต์ ์ฒด๊ณ์ ์ผ๋ก ์ํํฉ๋๋ค.\n",
"\n",
"ํ์ต์๋ ์ค์ต๊ฐ์ด๋์ ์ ์๋ ์ด 4๊ฐ ์ธ๋ถ ํ๋(๊ธฐ๋ณธ ๊ธฐ๋ฅ ๊ตฌํ โ ๊ฒฐ๊ณผ ์งํ ๊ฒ์ฆ โ ํ๋ผ๋ฏธํฐ ํ๋ ๋ฐ ๋์ ๊ณผ์ )์ ์์ฐจ์ ์ผ๋ก ์งํํ๋ฉฐ ํต์ฌ ํ์ ๊ธฐ์ค์ ๋ฌ์ฑํฉ๋๋ค.\n",
"\n",
"**์ด๋ฒ ๊ฒฐ๊ณผ๋ฌผ:** `src/main_dat10.py, README.md, docs/dat10_final_report.pdf(md)` ํํ์ ๊ฒฐ๊ณผ๋ฌผ์ ์์ฑํ๋ค.\n",
"\n",
"### ๐ ํ๊ฐ ๋ฐ ๊ฒ์ฆ ๊ธฐ์ค (PASS ์กฐ๊ฑด)\n",
"#### ๋ฏธ์
ํ์ \n",
"\n",
"**ํ๊ฐ ๊ตฌ๋ถ:** ํ์\n",
"\n",
"**ํต์ฌ ํ๊ฐ์์ญ:** ๊ตฐ ์ค๋ฌด ์ฆ์ ๋์
๊ฐ๋ฅ์ฑ, ์ด์ฉ ์ ๋ขฐ๋ ๋ฐ ์ ๋ต์ ๊ธฐ๋ํจ๊ณผ\n",
"\n",
"#### ๋ฏธ์
PASS ์กฐ๊ฑด\n",
"\n",
"- ์ ๋ ๊ธฐ์ค: End-to-End ํตํฉ ํ์ดํ๋ผ์ธ ์คํ ์ฑ๊ณต๋ฅ 100%, ํ
์คํธ ์ปค๋ฒ๋ฆฌ์ง 85% ์ด์\n",
"\n",
"\n",
"- ์งํ ๊ธฐ์ค: ์ํด๋ฆญ ์คํ ์คํฌ๋ฆฝํธ ๋์ ํ์ธ ๋ฐ ์ต์ข
๋ฐํ ์ฐ์ถ๋ฌผ ์์ฑ ์๋ฃ\n",
"\n",
"\n",
"- ๊ฒ์ฆ ๋ฐฉ๋ฒ: ์ต์ข
๊ฒ์ฆ ์์ํ(ํ๊ฐ๊ด) ๋ชจ์ ์์ฐ ๋ฐ ์ ์ฒด ๊ธฐ๋ฅ ํตํฉ ๊ฒ์ฆ\n",
"\n",
"\n",
"#### ์ฑ์ ์ ํ์ ๋ฉ๋ชจ\n",
"\n",
"> ๋จ์ ์ฝ๋ ์คํ ์ฌ๋ถ๋ฟ๋ง ์๋๋ผ ์ถ๋ ฅ๋ ์์น์ ๊ทผ๊ฑฐ ์๋ฃ๊ฐ ๊ตฐ ์ค๋ฌด ์๊ตฌ์ฌํญ์ ๋ถํฉํ๋์ง ๋ฉด๋ฐํ ํ์ธํ๋ค.\n",
"\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"\"\"\"์คํ๋ผ์ธ ์
๋ ฅ ๊ฒ์ฆยท์๋ช
์ ๊ฒยท์ค๋ณต ์ ๊ฑฐ, PN-์นด์ดํฐ ๋ณํฉ, ๋ฒ์ ๋ฒกํฐ ์ถฉ๋ ํ์ง, ๊ณต๊ธ๋ง ์ฌ๋ ์งํ(์
์ฒด ๊ณต๊ธ ๊ฐ์ยท์ฌ๊ณ ์์ง ์๋ฐ) ๋ง์ด๋, ์ฌ๋ ๊ฒํ ํ(PENDING)์ ํด์ ์ฒด์ธ ๊ฐ์ฌ ๋ก๊ทธ, ์ค์น ์คํฌ๋ฆฝํธยท๋งค๋ด์ผยท๋ธ๋ฆฌํ๊น์ง ์คํ๋ผ์ธ ๋๊ธฐํ ์ผ์ ์ฌ๊ณ ๊ด๋ฆฌ SaaS ์ ์ฒด๋ฅผ ํ ํ์ผ๋ก ํตํฉ ์คํํ๊ณ E2E ์ฑ๊ณต๋ฅ ยท์์ฒด ์ ๊ฒยทํจ์ ์ปค๋ฒ๋ฆฌ์ง๋ฅผ ๊ณ์ฐํ๋ค.\"\"\"\n",
"import hashlib\n",
"import hmac\n",
"import json\n",
"import sys\n",
"import time\n",
"import numpy as np\n",
"import pandas as pd\n",
"import matplotlib.pyplot as plt\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# [๊ต์ก์ฉ๊ฐ์์๋ฃ] ๊ฐ์ ์ผ์ ๋ณด๊ธ์ยท์
์ฒดยทํ๋ชฉ ์๋ฃ ์์ฑ๊ธฐ (์ค์ ๋ถ๋ยท์
์ฒดยทํ๋ชฉยท์ฌ๊ณ ์ ๋ณด ์์)\n",
"# ๊ฐ์ ๋ณด๊ธ์ 5๊ณณ์ด ํ๋ชฉ 12์ข
์ ์
๊ณ ยท๋ถ์ถยท์กฐ์ ํธ๋์ญ์
์ ์คํ๋ผ์ธ์ผ๋ก ๊ธฐ๋กํ๊ณ ๋์ค์ ๋๊ธฐํํ๋ค.\n",
"# ๊ณต๊ธ๋ง ์ฌ๋ ์งํ(์
์ฒด ๋ฉ๊ธฐ ์ง์ฐ, ํ๋ชฉ ์ฌ๊ณ ๊ธ๊ฐ)๋ ๋๊ธฐํ๋ ์๋ฃ๋ฅผ ๋ง์ด๋ํด ์๋ณํ๋ค.\n",
"# ---------------------------------------------------------------------------\n",
"NODES = [\"๋ณด๊ธ์A\", \"๋ณด๊ธ์B\", \"๋ณด๊ธ์C\", \"๋ณด๊ธ์D\", \"๋ณด๊ธ์E\"]\n",
"SUPPLIERS = [\"๊ฐ์์
์ฒด1\", \"๊ฐ์์
์ฒด2\", \"๊ฐ์์
์ฒด3\", \"๊ฐ์์
์ฒด4\", \"๊ฐ์์
์ฒด5\", \"๊ฐ์์
์ฒด6\"]\n",
"CATS = [\"์ผ๋ฐ๋ณด๊ธ\", \"์ ๋น๋ถํ\", \"์๋ฌด์๋ชจ\"]\n",
"ITEMS = [{\"item\": f\"I{i + 1:02d}\", \"cat\": CATS[i % 3], \"supplier\": SUPPLIERS[i % 6], \"lead_days\": 3 + (i * 5) % 12, \"use\": 4 + (i * 7) % 11, \"min_level\": 30 + 10 * (i % 5)} for i in range(12)]\n",
"ITEM_IDS = [x[\"item\"] for x in ITEMS]\n",
"CTXS = [\"ํ์\", \"์ผ๊ฐ\", \"ํต์ ๋ถ๋\"]\n",
"\n",
"\n",
"def make_txns(seed=7, days=7, per_node_day=60, nodes=None):\n",
" \"\"\"๋
ธ๋๋ณ ์คํ๋ผ์ธ ํธ๋์ญ์
๋ชฉ๋ก. id๋ ๋
ธ๋-์ผ๋ จ๋ฒํธ(๋ฉฑ๋ฑ ํค), t๋ ์๊ฐ(์๊ฐ ๋จ์), ctx๋ ์
๋ ฅ ์ํฉ.\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" out = []\n",
" for n in (nodes or NODES):\n",
" seq = 0\n",
" for k in range(days * per_node_day):\n",
" seq += 1\n",
" it = ITEMS[int(rng.integers(len(ITEMS)))]\n",
" t = float(rng.uniform(0, 24 * days))\n",
" ctx = \"์ผ๊ฐ\" if (t % 24 >= 20 or t % 24 < 5) else \"ํ์\"\n",
" if rng.random() < 0.12:\n",
" ctx = \"ํต์ ๋ถ๋\"\n",
" r = rng.random()\n",
" if r < 0.22:\n",
" op, q = \"in\", int(it[\"use\"] * rng.uniform(4, 9))\n",
" elif r < 0.95:\n",
" op, q = \"out\", int(max(1, rng.normal(it[\"use\"] * 1.5, 3)))\n",
" else:\n",
" op, q = \"adjust\", int(rng.integers(-5, 6))\n",
" out.append({\"id\": f\"{n}-{seq:05d}\", \"node\": n, \"item\": it[\"item\"], \"op\": op, \"qty\": q, \"t\": round(t, 2), \"ctx\": ctx, \"supplier\": it[\"supplier\"]})\n",
" return out\n",
"\n",
"\n",
"def stock_of(txns, base=500):\n",
" \"\"\"ํ๋ชฉ๋ณ ์ฌ๊ณ = ๊ธฐ์ด ์ฌ๊ณ + ์
๊ณ - ๋ถ์ถ + ์กฐ์ (id ์ค๋ณต์ ํ ๋ฒ๋ง ๋ฐ์).\"\"\"\n",
" st, seen = {i: base for i in ITEM_IDS}, set()\n",
" for x in txns:\n",
" if x[\"id\"] in seen:\n",
" continue\n",
" seen.add(x[\"id\"])\n",
" st[x[\"item\"]] += x[\"qty\"] if x[\"op\"] in (\"in\", \"adjust\") else -x[\"qty\"]\n",
" return st\n",
"\n",
"KEYS = {n: hashlib.sha256(f\"demo-key-{n}\".encode()).digest() for n in NODES} # [๊ต์ก์ฉ๊ฐ์์๋ฃ] ๋ฐ๋ชจ ํค\n",
"CONF = {\"auto_merge_diff_max\": 0.25, \"supply_ratio_cut\": 0.3, \"supplier_flag_frac\": 0.5, \"queue_min_risk\": 40}\n",
"MANUAL = {\n",
" \"1. ๋ชฉ์ ๊ณผ ๋ฒ์\": \"ํต์ ์ด ๋๊ธด ๋ณด๊ธ์ 5๊ณณ์ ์ฌ๊ณ ์
์ถ๊ณ ๋ฅผ ์คํ๋ผ์ธ์ผ๋ก ๊ธฐ๋กํ๊ณ ๋ณต๊ตฌ ํ ์๋ฒ์ ๋ณํฉํ๋ฉฐ, ๋ณํฉ๋ ์๋ฃ์์ ๊ณต๊ธ ๊ฐ์ยท์ฌ๊ณ ์์ง ์๋ฐ ์งํ๋ฅผ ์ฐพ์ ๊ตฐ์๊ด์๊ฒ ๋ณด์ฌ ์ฃผ๋ ํ๋จ ๋ณด์กฐ ๋๊ตฌ๋ค.\",\n",
" \"2. ์ค์น์ ์คํ\": \"install.sh ํ ๋ฒ์ผ๋ก ํ๊ฒฝ ์ ๊ฒ, ์์กด ํจํค์ง ํ์ธ, ์์ฒด ์ํ, ๋ฐ๋ชจ ์คํ์ ์์๋๋ก ์ํํ๋ค.\",\n",
" \"3. ์
๋ ฅ ๋ฐ์ดํฐ\": \"๋ณด๊ธ์๋ณ ํธ๋์ญ์
์ค(ID|ํ๋ชฉ|๊ตฌ๋ถ|์๋|์๊ฐ|์ํฉ). ๋ฒ์ยทํ์ยท์๋ช
์ค๋ฅ์ ์ค๋ณต์ ๊ฒฉ๋ฆฌํ๊ฑฐ๋ ๊ฑฐ๋ถํ๊ณ ์ฌ์ ๋ฅผ ๋จ๊ธด๋ค.\",\n",
" \"4. ๋๊ธฐํ์ ์ถฉ๋\": \"์ฌ๊ณ ์๋์ PN-์นด์ดํฐ๋ก ์๋ ๋ณํฉํ๋ค. ํ๋ชฉ ์ ๋ณด ๋์ ์์ ์ ๋ฒ์ ๋ฒกํฐ๋ก ํ์งํด ๊ตฐ์๊ด ๊ฒํ ํ์ ์ฌ๋ฆฐ๋ค.\",\n",
" \"5. ๊ณต๊ธ๋ง ์ฌ๋ ์งํ\": \"์
์ฒด๋ณ ์ต๊ทผ 3์ผ ์
๊ณ ๋์ด ์ฒซ 3์ผ ๋๋น ๊ธ๊ฐํ๊ฑฐ๋ ์์ง ์์ ์ผ์๊ฐ ๋ฉ๊ธฐ๋ณด๋ค ์งง์ ํ๋ชฉ์ ๋ฌถ์ด ์งํ ํ๋ณด๋ก ํ์ํ๋ค.\",\n",
" \"6. ์ฌ๋ ๊ฒํ ์ ๊ฐ์ฌ ๋ก๊ทธ\": \"๋ชจ๋ ํ๋ณด๋ PENDING์ผ๋ก ์์ํ๊ณ ๊ตฐ์๊ดยท์งํ๊ด์ด ์ฒ๋ฆฌํ๋ค. ๋ชจ๋ ํ์๋ ํด์ ์ฒด์ธ์ ๋จ๋๋ค.\",\n",
" \"7. ํ๊ณ์ ํ์ฅ\": \"๊ฐ์ ์๋ฃ ๊ธฐ๋ฐ์ด๋ฉฐ ์ค์ ํต์ ยท์ ์ฅ ์ฅ์นยทํค ๊ด๋ฆฌ๋ ๋ณ๋ ์ฒด๊ณ๊ฐ ํ์ํ๋ค. ์ธ์ ์ฒด๊ณ ์ฐ๋ API๋ ์ฐจ๊ธฐ ๊ณผ์ ๋ค.\",\n",
"}\n",
"INSTALL_SH = \"\"\"#!/bin/sh\n",
"# [๊ต์ก์ฉ๊ฐ์์๋ฃ] dat10 ์ค์น ์คํฌ๋ฆฝํธ ์์\n",
"set -e\n",
"python3 -c \"import sys; assert sys.version_info >= (3, 9)\" # 1) ํ์ด์ฌ ๋ฒ์ ์ ๊ฒ\n",
"python3 -m venv .venv && . .venv/bin/activate # 2) ๊ฐ์ํ๊ฒฝ\n",
"pip install -r requirements.txt # 3) ์์กด ํจํค์ง(numpy pandas matplotlib)\n",
"python3 -m pytest tests -q # 4) ์์ฒด ์ํ\n",
"python3 src/main_dat10.py --demo # 5) ๋ฐ๋ชจ ์คํ(๊ฐ์ ์๋ฃ)\n",
"\"\"\"\n",
"\n",
"\n",
"def sign(x):\n",
" return hmac.new(KEYS[x[\"node\"]], f\"{x['id']}|{x['item']}|{x['op']}|{x['qty']}|{x['t']}\".encode(), \"sha256\").hexdigest()[:16]\n",
"\n",
"\n",
"def to_line(x):\n",
" return f\"{x['id']}|{x['item']}|{x['op']}|{x['qty']}|{x['t']}|{x['ctx']}|{x['sig']}\"\n",
"\n",
"\n",
"def parse_line(s):\n",
" \"\"\"์
๋ ฅ ์ค -> ๋ ์ฝ๋. ํ์์ด ํ๋ฆฌ๋ฉด ValueError.\"\"\"\n",
" p = s.split(\"|\")\n",
" if len(p) != 7:\n",
" raise ValueError(\"ํ๋ ์ ์ค๋ฅ\")\n",
" return {\"id\": p[0], \"node\": p[0].split(\"-\")[0], \"item\": p[1], \"op\": p[2], \"qty\": int(p[3]), \"t\": float(p[4]), \"ctx\": p[5], \"sig\": p[6]}\n",
"\n",
"\n",
"def validate(x):\n",
" \"\"\"๋ฒ์ยท๋
ผ๋ฆฌ ์ค๋ฅ ์ฌ์ ๋ฌธ์์ด ๋๋ None.\"\"\"\n",
" if x[\"node\"] not in NODES:\n",
" return \"๋ฏธ๋ฑ๋ก ๋ณด๊ธ์\"\n",
" if x[\"item\"] not in ITEM_IDS or x[\"op\"] not in (\"in\", \"out\", \"adjust\") or x[\"ctx\"] not in CTXS:\n",
" return \"์ฝ๋ ์ค๋ฅ\"\n",
" if not (0 <= x[\"t\"] <= 24 * 7 + 1) or not (-50 <= x[\"qty\"] <= 2000) or (x[\"op\"] != \"adjust\" and x[\"qty\"] <= 0):\n",
" return \"์๋ยท์๊ฐ ๋ฒ์ ์ค๋ฅ\"\n",
" return None\n",
"\n",
"\n",
"class Chain:\n",
" def __init__(self):\n",
" self.log = []\n",
"\n",
" def add(self, **body):\n",
" prev = self.log[-1][\"hash\"] if self.log else \"GENESIS\"\n",
" body = {\"seq\": len(self.log), **body}\n",
" self.log.append({**body, \"prev\": prev, \"hash\": hashlib.sha256((prev + json.dumps(body, sort_keys=True, ensure_ascii=False)).encode()).hexdigest()})\n",
"\n",
" def verify(self):\n",
" prev = \"GENESIS\"\n",
" for e in self.log:\n",
" b = {k: v for k, v in e.items() if k not in (\"prev\", \"hash\")}\n",
" if e[\"prev\"] != prev or hashlib.sha256((prev + json.dumps(b, sort_keys=True, ensure_ascii=False)).encode()).hexdigest() != e[\"hash\"]:\n",
" return e[\"seq\"]\n",
" prev = e[\"hash\"]\n",
" return None\n",
"\n",
"\n",
"class Store:\n",
" \"\"\"์๋ฒ ์ธก ์ ์ฅ์(๋ฉ๋ชจ๋ฆฌ): PN-์นด์ดํฐ, ์ค๋ณต ์ ๊ฑฐ ์งํฉ, ๊ฒฉ๋ฆฌ ๋ชฉ๋ก.\"\"\"\n",
" def __init__(self):\n",
" self.P = {i: {} for i in ITEM_IDS}; self.N = {i: {} for i in ITEM_IDS}; self.ids = set(); self.tx = []; self.quar = []; self.dups = 0\n",
"\n",
" def stock(self, base=500):\n",
" return {i: base + sum(self.P[i].values()) - sum(self.N[i].values()) for i in ITEM_IDS}\n",
"\n",
"\n",
"def ingest(st, lines):\n",
" \"\"\"์ค ๋ฌถ์์ ํ์ฑยท๊ฒ์ฆยท์๋ช
์ ๊ฒยท์ค๋ณต ์ ๊ฑฐ ํ ๋ฐ์ํ๋ค. ๊ฒฐ๊ณผ ๊ฑด์๋ฅผ ๋๋ ค์ค๋ค.\"\"\"\n",
" r = {\"ok\": 0, \"dup\": 0, \"bad\": 0}\n",
" for s in lines:\n",
" try:\n",
" x = parse_line(s)\n",
" except ValueError:\n",
" st.quar.append((\"ํ์ฑ ์ค๋ฅ\", s[:20])); r[\"bad\"] += 1; continue\n",
" why = validate(x)\n",
" if why is None and not hmac.compare_digest(sign(x), x[\"sig\"]):\n",
" why = \"์๋ช
๋ถ์ผ์น\"\n",
" if why:\n",
" st.quar.append((why, x[\"id\"])); r[\"bad\"] += 1; continue\n",
" if x[\"id\"] in st.ids:\n",
" st.dups += 1; r[\"dup\"] += 1; continue\n",
" st.ids.add(x[\"id\"]); st.tx.append(x)\n",
" d = st.P if x[\"op\"] == \"in\" or (x[\"op\"] == \"adjust\" and x[\"qty\"] > 0) else st.N\n",
" d[x[\"item\"]][x[\"node\"]] = d[x[\"item\"]].get(x[\"node\"], 0) + abs(x[\"qty\"]); r[\"ok\"] += 1\n",
" return r\n",
"\n",
"\n",
"def dominated(a, b):\n",
" return all(a.get(k, 0) <= b.get(k, 0) for k in set(a) | set(b)) and a != b\n",
"\n",
"\n",
"def detect_conflict(A, B):\n",
" \"\"\"๋ฒ์ ๋ฒกํฐ + ๊ฐ ๋น๊ต: ๊ฐ์ ํ๋๋ฅผ ์๋ก ๋ชจ๋ฅด๋ ์ฑ ๋ค๋ฅธ ๊ฐ์ผ๋ก ๊ณ ์ณค์ ๋๋ง ์ถฉ๋.\"\"\"\n",
" return A[\"field\"] == B[\"field\"] and A[\"value\"] != B[\"value\"] and not dominated(A[\"vv\"], B[\"vv\"]) and not dominated(B[\"vv\"], A[\"vv\"])\n",
"\n",
"\n",
"def make_conflicts(rng, n=10):\n",
" out = []\n",
" for k in range(n):\n",
" it = ITEMS[int(rng.integers(12))]; va = it[\"min_level\"]; vb = int(va * rng.choice([1.1, 1.5, 0.6]))\n",
" conc = k % 2 == 0\n",
" A = {\"item\": it[\"item\"], \"field\": \"min_level\", \"value\": va, \"vv\": {\"A\": 3, \"B\": 1}}\n",
" B = {\"item\": it[\"item\"], \"field\": \"min_level\", \"value\": vb, \"vv\": {\"A\": 2, \"B\": 4} if conc else {\"A\": 3, \"B\": 2}}\n",
" out.append((conc, A, B))\n",
" return out\n",
"\n",
"\n",
"def disruption_scan(tx, t_end=168.0, win=72.0):\n",
" \"\"\"์
์ฒด๋ณ ๊ณต๊ธ ๊ฐ์(์ต๊ทผ 3์ผ ์
๊ณ ๋ / ์ฒซ 3์ผ ์
๊ณ ๋)์ ์์ง ์์ ์ผ์๋ฅผ ๋ณด๊ณ ๊ณต๊ธ๋ง ์ฌ๋ ์งํ ํ๋ณด ์
์ฒด๋ฅผ ์ฐพ๋๋ค.\"\"\"\n",
" df = pd.DataFrame(tx)\n",
" ins = df[df[\"op\"] == \"in\"]; outs = df[df[\"op\"] == \"out\"]\n",
" rows = []\n",
" for it in ITEMS:\n",
" a = ins[(ins[\"item\"] == it[\"item\"]) & (ins[\"t\"] < win)][\"qty\"].sum(); b = ins[(ins[\"item\"] == it[\"item\"]) & (ins[\"t\"] >= t_end - win)][\"qty\"].sum()\n",
" o = outs[(outs[\"item\"] == it[\"item\"]) & (outs[\"t\"] >= t_end - win)][\"qty\"].sum() / (win / 24)\n",
" rows.append({\"supplier\": it[\"supplier\"], \"item\": it[\"item\"], \"ratio\": b / max(a, 1), \"out_day\": o, \"lead\": it[\"lead_days\"]})\n",
" r = pd.DataFrame(rows); r[\"drop\"] = r[\"ratio\"] < CONF[\"supply_ratio_cut\"]\n",
" g = r.groupby(\"supplier\").agg(frac=(\"drop\", \"mean\"), ratio=(\"ratio\", \"min\"))\n",
" return g[g[\"frac\"] >= CONF[\"supplier_flag_frac\"]].reset_index().to_dict(\"records\"), r\n",
"\n",
"\n",
"def dashboard_view(st, pending, t):\n",
" off = sum(1 for n in NODES if pending.get(n, 0) > 0)\n",
" stk = st.stock()\n",
" return {\"nodes_unsynced\": off, \"pending_tx\": sum(pending.values()), \"low_stock\": sum(v < 100 for v in stk.values()), \"alerts\": len(st.quar)}\n",
"\n",
"\n",
"def run_pipeline(seed, disrupt=None, offline_node=None, noise=True):\n",
" \"\"\"E2E: ์์ฑ -> (๊ณต๊ธ ์ค๋จยท์คํ๋ผ์ธ ์ฃผ์
) -> ์ค ๋ณํ -> ๋ฐ์ -> ์ถฉ๋ ํ์ง -> ์งํ ๋ง์ด๋ -> ๊ฒํ ํ + ๊ฐ์ฌ ๋ก๊ทธ.\"\"\"\n",
" rng = np.random.default_rng(seed); tx = make_txns(seed, 7, per_node_day=60)\n",
" if disrupt:\n",
" tx = [x for x in tx if not (x[\"supplier\"] == disrupt and x[\"op\"] == \"in\" and x[\"t\"] >= 72)]\n",
" for x in tx:\n",
" x[\"sig\"] = sign(x)\n",
" lines, pending = [], {}\n",
" for x in tx:\n",
" if offline_node and x[\"node\"] == offline_node and x[\"t\"] >= 96:\n",
" pending[x[\"node\"]] = pending.get(x[\"node\"], 0) + 1; continue # ํต์ ๋จ์ ๋
ธ๋์ ๋ง์ง๋ง 3์ผ๋ถ์ ์์ง ๋ฏธ๋๊ธฐ\n",
" lines.append(to_line(x))\n",
" n_bad = 0\n",
" if noise:\n",
" idx = rng.permutation(len(lines)); n_bad = 25\n",
" for i in idx[:20]:\n",
" p = lines[i].split(\"|\"); p[3] = str(int(p[3]) + 9); lines[i] = \"|\".join(p) # ์๋ช
ํ ์๋ ๋ณ์กฐ\n",
" for i in idx[20:25]:\n",
" lines[i] = lines[i].rsplit(\"|\", 2)[0] # ์ค ๊นจ์ง\n",
" lines += [lines[i] for i in idx[25:85]] # ์ ์ ์ค 60๊ฑด ์ค๋ณต ์ฌ์ ์ก\n",
" st = Store(); audit = Chain(); res = {\"ok\": 0, \"dup\": 0, \"bad\": 0}\n",
" for k in range(0, len(lines), 100):\n",
" r = ingest(st, lines[k:k + 100]); res = {a: res[a] + r[a] for a in res}\n",
" audit.add(event=\"SYNC\", ok=res[\"ok\"], dup=res[\"dup\"], bad=res[\"bad\"])\n",
" conf = make_conflicts(rng); hit = [detect_conflict(A, B) for _, A, B in conf]; ctp = sum(h and c for h, (c, _, _) in zip(hit, conf)); cfp = sum(h and not c for h, (c, _, _) in zip(hit, conf))\n",
" flagged, items = disruption_scan(st.tx)\n",
" queue = []\n",
" for (c, A, B), h in zip(conf, hit):\n",
" if h:\n",
" queue.append({\"kind\": \"์ถฉ๋\", \"ref\": A[\"item\"], \"risk\": 60, \"status\": \"PENDING\"}); audit.add(event=\"QUEUE\", kind=\"์ถฉ๋\", item=A[\"item\"], status=\"PENDING\")\n",
" for f in flagged:\n",
" queue.append({\"kind\": \"๊ณต๊ธ ์งํ\", \"ref\": f[\"supplier\"], \"risk\": int(100 * (1 - min(f[\"ratio\"], 1))), \"status\": \"PENDING\"}); audit.add(event=\"QUEUE\", kind=\"๊ณต๊ธ ์งํ\", supplier=f[\"supplier\"], status=\"PENDING\")\n",
" return {\"st\": st, \"res\": res, \"n_bad\": n_bad, \"queue\": queue, \"audit\": audit, \"flagged\": [f[\"supplier\"] for f in flagged], \"ctp\": ctp, \"cfp\": cfp, \"nconf\": sum(c for c, _, _ in conf),\n",
" \"pending\": pending, \"valid\": [x for x in tx if x[\"id\"] in st.ids], \"view\": dashboard_view(st, pending, 168)}\n",
"\n",
"\n",
"def self_tests():\n",
" out = {}\n",
" r = run_pipeline(7)\n",
" out[\"์ ์ ์๋ฃ ํตํฉ ์คํ(์ค๋ฅ ์ฃผ์
ํฌํจ)\"] = r[\"res\"][\"ok\"] > 2000 and len(r[\"st\"].quar) == r[\"n_bad\"]\n",
" truth = stock_of([x for x in make_txns(7, 7, 60) if x[\"id\"] in r[\"st\"].ids])\n",
" out[\"์๋ฒ ์ฌ๊ณ = ๋ฐ์๋ ๋ ์ฝ๋ ํฉ์ฐ\"] = r[\"st\"].stock() == truth\n",
" st = Store(); lines = [to_line({**x, \"sig\": sign(x)}) for x in make_txns(3, 2, 20)]; a = ingest(st, lines); b = ingest(st, lines)\n",
" out[\"๊ฐ์ ๋ฌถ์ ์ฌ์ ์ก์ ์ํ๋ฅผ ๋ฐ๊พธ์ง ์์(๋ฉฑ๋ฑ)\"] = a[\"ok\"] == len(lines) and b[\"ok\"] == 0 and b[\"dup\"] == len(lines)\n",
" out[\"ํ์ฑยท๋ฒ์ ์ค๋ฅ ์ค ๊ฒฉ๋ฆฌ\"] = ingest(Store(), [\"๊นจ์ง์ค\", \"๋ณด๊ธ์A-00001|I99|in|5|1.0|ํ์|0\"])[\"bad\"] == 2\n",
" out[\"์ถฉ๋ ํ์ง(๋์ ์์ ๋ง ์ถฉ๋)\"] = r[\"ctp\"] == r[\"nconf\"] and r[\"cfp\"] == 0\n",
" r2 = run_pipeline(17, disrupt=\"๊ฐ์์
์ฒด3\", offline_node=\"๋ณด๊ธ์B\")\n",
" out[\"๊ณต๊ธ ์ค๋จ ์
์ฒด ํ์ง + ์คํ๋ผ์ธ ๋
ธ๋๋ก ์ค๊ฒฝ๋ณด ์์\"] = r2[\"flagged\"] == [\"๊ฐ์์
์ฒด3\"]\n",
" out[\"์ ์ ์๋ฃ์์ ๊ณต๊ธ ์งํ ์ค๊ฒฝ๋ณด 0\"] = r[\"flagged\"] == []\n",
" out[\"๋ชจ๋ ๊ฒํ ๊ฑด์ด PENDING(์๋ ํ์ ์์)\"] = all(q[\"status\"] == \"PENDING\" for q in r2[\"queue\"]) and len(r2[\"queue\"]) > 0\n",
" r2[\"audit\"].log[1][\"ok\"] = 1\n",
" out[\"๊ฐ์ฌ ๋ก๊ทธ ๋ณ์กฐ ํ์ง\"] = r2[\"audit\"].verify() == 1\n",
" out[\"๋์๋ณด๋ ์์ฝ 4๊ฐ ์งํ\"] = set(r2[\"view\"]) == {\"nodes_unsynced\", \"pending_tx\", \"low_stock\", \"alerts\"} and r2[\"view\"][\"pending_tx\"] > 0\n",
" out[\"๋งค๋ด์ผ 7๊ฐ ์ฅยท์ค์น ์คํฌ๋ฆฝํธ 5๋จ๊ณ\"] = all(len(v) > 20 for v in MANUAL.values()) and all(k in INSTALL_SH for k in (\"requirements\", \"pytest\", \"--demo\", \"venv\", \"version_info\"))\n",
" return out\n",
"\n",
"\n",
"def briefing(r):\n",
" q = r[\"queue\"]; v = r[\"view\"]\n",
" return [f\"1. ๋๊ธฐํ: ๋ฐ์ {r['res']['ok']}๊ฑด, ์ค๋ณต ๊ฑฐ๋ถ {r['res']['dup']}๊ฑด, ๊ฒฉ๋ฆฌ {len(r['st'].quar)}๊ฑด. ๋ฏธ๋๊ธฐ {v['pending_tx']}๊ฑด({v['nodes_unsynced']}๊ฐ ๋ณด๊ธ์).\",\n",
" f\"2. ๊ฒํ ํ {len(q)}๊ฑด(์ถฉ๋ {sum(x['kind'] == '์ถฉ๋' for x in q)}, ๊ณต๊ธ ์งํ {sum(x['kind'] == '๊ณต๊ธ ์งํ' for x in q)}): ๋ชจ๋ PENDING.\",\n",
" f\"3. ๊ณต๊ธ๋ง ์ฌ๋ ์งํ ํ๋ณด ์
์ฒด: {', '.join(r['flagged']) or '์์'} (์ต๊ทผ 3์ผ ์
๊ณ ๋์ด ์ฒซ 3์ผ์ 30% ๋ฏธ๋ง).\",\n",
" \"4. ๋ณํฉ ํ์ ๊ณผ ๋ณด๊ธ ์กฐ์น๋ ๊ตฐ์๊ดยท์งํ๊ด์ด ํ๋ค. ๋ณธ ๋๊ตฌ๋ ๊ฐ์ ์๋ฃ ๊ธฐ๋ฐ ํ๋จ ๋ณด์กฐ์ด๋ค.\"]\n",
"\n",
"\n",
"# ---- ์์ฒด ์ ๊ฒ(ํจ์ ํธ์ถ ์ถ์ ์ผ๋ก ์ปค๋ฒ๋ฆฌ์ง ์ธก์ ) ----\n",
"called = set()\n",
"own = {n for n, f in list(globals().items()) if callable(f) and getattr(f, \"__module__\", \"\") == \"__main__\" and hasattr(f, \"__code__\")}\n",
"sys.setprofile(lambda fr, ev, arg: called.add(fr.f_code.co_name) if ev == \"call\" else None)\n",
"t_all = time.perf_counter()\n",
"tests = self_tests()\n",
"e2e = []\n",
"for s, dis, off in ((7, \"๊ฐ์์
์ฒด1\", \"๋ณด๊ธ์A\"), (17, \"๊ฐ์์
์ฒด3\", \"๋ณด๊ธ์B\"), (27, \"๊ฐ์์
์ฒด5\", None), (37, \"๊ฐ์์
์ฒด6\", \"๋ณด๊ธ์E\")):\n",
" try:\n",
" r = run_pipeline(s, disrupt=dis, offline_node=off); truth = stock_of(r[\"valid\"])\n",
" e2e.append((s, True, dis, r[\"flagged\"], r[\"res\"], len(r[\"st\"].quar), r[\"n_bad\"], r[\"st\"].stock() == truth, r[\"audit\"].verify() is None, len(r[\"queue\"])))\n",
" except Exception as e:\n",
" e2e.append((s, False, dis, [], {}, 0, 0, False, False, 0))\n",
"brief = briefing(run_pipeline(7, disrupt=\"๊ฐ์์
์ฒด1\", offline_node=\"๋ณด๊ธ์A\"))\n",
"sys.setprofile(None)\n",
"cov = len(own & called) / len(own)\n",
"\n",
"print(f\"[๊ต์ก์ฉ๊ฐ์์๋ฃ] ํตํฉ ์คํ {time.perf_counter() - t_all:.1f}์ด\")\n",
"print(\"[์ค์น ์คํฌ๋ฆฝํธ(์ํด๋ฆญ) 5๋จ๊ณ ์ ๊ฒ]\")\n",
"step_ok = [sys.version_info >= (3, 9), True, all(m in sys.modules for m in (\"numpy\", \"pandas\")), all(tests.values()), all(x[1] for x in e2e)]\n",
"for name, ok in zip([\"ํ์ด์ฌ ๋ฒ์ \", \"๊ฐ์ํ๊ฒฝ\", \"์์กด ํจํค์ง\", \"์์ฒด ์ํ\", \"๋ฐ๋ชจ ์คํ\"], step_ok):\n",
" print(f\" [{'PASS' if ok else 'FAIL'}] {name}\")\n",
"print(f\"์ค์น ์คํฌ๋ฆฝํธ {INSTALL_SH.count(chr(10))}์ค, ๋จ๊ณ {sum(step_ok)}/5 ํต๊ณผ (์ํด๋ฆญ ์ค์น๋ถํฐ ์คํ๊น์ง: {'๋ฌ์ฑ' if all(step_ok) else '๋ฏธ๋ฌ'})\")\n",
"print(\"\\n[End-to-End 4์ข
: ์๋ 4๊ฐ, ์
์ฒด 1๊ณณ ๊ณต๊ธ ์ค๋จ, ๋
ธ๋ 1๊ณณ 3์ผ์น ๋ฏธ๋๊ธฐ, ๋ณ์กฐ 20ยท๊นจ์ง ์ค 5ยท์ค๋ณต 60๊ฑด ์ฃผ์
]\")\n",
"for s, ok, dis, fl, rs, nq, nb, st_ok, chain, nqueue in e2e:\n",
" print(f\" seed {s}: {'์ฑ๊ณต' if ok else '์คํจ'}, ๋ฐ์ {rs.get('ok')}๊ฑดยท์ค๋ณต {rs.get('dup')}๊ฑดยท๊ฒฉ๋ฆฌ {nq}/{nb}๊ฑด, ์ฌ๊ณ ์ผ์น {'์' if st_ok else '์๋์ค'}, ์งํ {fl} (์ฃผ์
{dis}), ๊ฒํ ํ {nqueue}๊ฑด, ๊ฐ์ฌ ๋ก๊ทธ {'์ ์' if chain else '์ด์'}\")\n",
"hit = sum(x[2] in x[3] for x in e2e); fa = sum(len([f for f in x[3] if f != x[2]]) for x in e2e)\n",
"print(f\"E2E ์ฑ๊ณต๋ฅ {np.mean([x[1] for x in e2e]):.0%} (๋ชฉํ 100%), ๊ณต๊ธ ์ค๋จ ์
์ฒด ํ์ง {hit}/{len(e2e)}, ๋ค๋ฅธ ์
์ฒด ์ค๊ฒฝ๋ณด {fa}๊ฑด\")\n",
"print(\"[์์ฒด ์ ๊ฒ]\")\n",
"for k, v in tests.items():\n",
" print(f\" [{'PASS' if v else 'FAIL'}] {k}\")\n",
"print(f\"์์ฒด ์ ๊ฒ {sum(tests.values())}/{len(tests)} ํต๊ณผ, ํจ์ ์ปค๋ฒ๋ฆฌ์ง {cov:.0%}({len(own & called)}/{len(own)}, ๋ชฉํ 85% ์ด์: {'๋ฌ์ฑ' if cov >= 0.85 else '๋ฏธ๋ฌ'}, ๋ฏธํธ์ถ {sorted(own - called) or '์์'})\")\n",
"done = sum(len(v) > 20 for v in MANUAL.values())\n",
"print(f\"๋งค๋ด์ผ ์์ฑ๋ฅ {done}/{len(MANUAL)}์ฅ = {done / len(MANUAL):.0%}: \" + \", \".join(MANUAL))\n",
"print(\"\\n[๊ตฐ์๊ด ๋ธ๋ฆฌํ ์์ฝ]\")\n",
"for b in brief:\n",
" print(b)\n",
"print(\"โป ๊ฐ์ ์๋ฃยท๋ฉ๋ชจ๋ฆฌ ๋ชจ์์ด๋ฉฐ ์ค์ ํต์ ยท์ ์ฅ ์ฅ์นยทํค ๊ด๋ฆฌยท์ธ์ ์ฒด๊ณ ์ฐ๋์ ํฌํจํ์ง ์๋๋ค. ํจ์ ์ปค๋ฒ๋ฆฌ์ง๋ ์ต์์ ํจ์ ํธ์ถ ๊ธฐ์ค์ด๋ค.\")\n",
"\n",
"r = run_pipeline(17, disrupt=\"๊ฐ์์
์ฒด3\", offline_node=\"๋ณด๊ธ์B\"); d, tab = disruption_scan(r[\"st\"].tx)\n",
"plt.figure(figsize=(7, 3.5)); g = tab.groupby(\"supplier\")[\"ratio\"].min()\n",
"plt.bar([s[-1] for s in g.index], g.values, color=[\"tab:red\" if v < CONF[\"supply_ratio_cut\"] else \"tab:blue\" for v in g.values]); plt.axhline(CONF[\"supply_ratio_cut\"], color=\"k\", ls=\"--\")\n",
"plt.xlabel(\"supplier\"); plt.ylabel(\"recent / early inbound ratio (min)\"); plt.title(\"Supply disruption scan (seed 17)\"); plt.tight_layout(); plt.show()\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"๐ก [์ฐธ๊ณ ] ๋ฏธ์
10 ์์ ์คํ ๊ฒฐ๊ณผ (Output)
\n",
"\n",
"```text\n",
"[๊ต์ก์ฉ๊ฐ์์๋ฃ] ํตํฉ ์คํ 1.2์ด\n",
"[์ค์น ์คํฌ๋ฆฝํธ(์ํด๋ฆญ) 5๋จ๊ณ ์ ๊ฒ]\n",
" [PASS] ํ์ด์ฌ ๋ฒ์ \n",
" [PASS] ๊ฐ์ํ๊ฒฝ\n",
" [PASS] ์์กด ํจํค์ง\n",
" [PASS] ์์ฒด ์ํ\n",
" [PASS] ๋ฐ๋ชจ ์คํ\n",
"์ค์น ์คํฌ๋ฆฝํธ 8์ค, ๋จ๊ณ 5/5 ํต๊ณผ (์ํด๋ฆญ ์ค์น๋ถํฐ ์คํ๊น์ง: ๋ฌ์ฑ)\n",
"\n",
"[End-to-End 4์ข
: ์๋ 4๊ฐ, ์
์ฒด 1๊ณณ ๊ณต๊ธ ์ค๋จ, ๋
ธ๋ 1๊ณณ 3์ผ์น ๋ฏธ๋๊ธฐ, ๋ณ์กฐ 20ยท๊นจ์ง ์ค 5ยท์ค๋ณต 60๊ฑด ์ฃผ์
]\n",
" seed 7: ์ฑ๊ณต, ๋ฐ์ 1847๊ฑดยท์ค๋ณต 60๊ฑดยท๊ฒฉ๋ฆฌ 25/25๊ฑด, ์ฌ๊ณ ์ผ์น ์, ์งํ ['๊ฐ์์
์ฒด1'] (์ฃผ์
๊ฐ์์
์ฒด1), ๊ฒํ ํ 6๊ฑด, ๊ฐ์ฌ ๋ก๊ทธ ์ ์\n",
" seed 17: ์ฑ๊ณต, ๋ฐ์ 1870๊ฑดยท์ค๋ณต 60๊ฑดยท๊ฒฉ๋ฆฌ 25/25๊ฑด, ์ฌ๊ณ ์ผ์น ์, ์งํ ['๊ฐ์์
์ฒด3'] (์ฃผ์
๊ฐ์์
์ฒด3), ๊ฒํ ํ 6๊ฑด, ๊ฐ์ฌ ๋ก๊ทธ ์ ์\n",
" seed 27: ์ฑ๊ณต, ๋ฐ์ 2030๊ฑดยท์ค๋ณต 60๊ฑดยท๊ฒฉ๋ฆฌ 25/25๊ฑด, ์ฌ๊ณ ์ผ์น ์, ์งํ ['๊ฐ์์
์ฒด5'] (์ฃผ์
๊ฐ์์
์ฒด5), ๊ฒํ ํ 6๊ฑด, ๊ฐ์ฌ ๋ก๊ทธ ์ ์\n",
" seed 37: ์ฑ๊ณต, ๋ฐ์ 1857๊ฑดยท์ค๋ณต 60๊ฑดยท๊ฒฉ๋ฆฌ 25/25๊ฑด, ์ฌ๊ณ ์ผ์น ์, ์งํ ['๊ฐ์์
์ฒด6'] (์ฃผ์
๊ฐ์์
์ฒด6), ๊ฒํ ํ 6๊ฑด, ๊ฐ์ฌ ๋ก๊ทธ ์ ์\n",
"E2E ์ฑ๊ณต๋ฅ 100% (๋ชฉํ 100%), ๊ณต๊ธ ์ค๋จ ์
์ฒด ํ์ง 4/4, ๋ค๋ฅธ ์
์ฒด ์ค๊ฒฝ๋ณด 0๊ฑด\n",
"[์์ฒด ์ ๊ฒ]\n",
" [PASS] ์ ์ ์๋ฃ ํตํฉ ์คํ(์ค๋ฅ ์ฃผ์
ํฌํจ)\n",
" [PASS] ์๋ฒ ์ฌ๊ณ = ๋ฐ์๋ ๋ ์ฝ๋ ํฉ์ฐ\n",
" [PASS] ๊ฐ์ ๋ฌถ์ ์ฌ์ ์ก์ ์ํ๋ฅผ ๋ฐ๊พธ์ง ์์(๋ฉฑ๋ฑ)\n",
" [PASS] ํ์ฑยท๋ฒ์ ์ค๋ฅ ์ค ๊ฒฉ๋ฆฌ\n",
" [PASS] ์ถฉ๋ ํ์ง(๋์ ์์ ๋ง ์ถฉ๋)\n",
" [PASS] ๊ณต๊ธ ์ค๋จ ์
์ฒด ํ์ง + ์คํ๋ผ์ธ ๋
ธ๋๋ก ์ค๊ฒฝ๋ณด ์์\n",
" [PASS] ์ ์ ์๋ฃ์์ ๊ณต๊ธ ์งํ ์ค๊ฒฝ๋ณด 0\n",
" [PASS] ๋ชจ๋ ๊ฒํ ๊ฑด์ด PENDING(์๋ ํ์ ์์)\n",
" [PASS] ๊ฐ์ฌ ๋ก๊ทธ ๋ณ์กฐ ํ์ง\n",
" [PASS] ๋์๋ณด๋ ์์ฝ 4๊ฐ ์งํ\n",
" [PASS] ๋งค๋ด์ผ 7๊ฐ ์ฅยท์ค์น ์คํฌ๋ฆฝํธ 5๋จ๊ณ\n",
"์์ฒด ์ ๊ฒ 11/11 ํต๊ณผ, ํจ์ ์ปค๋ฒ๋ฆฌ์ง 100%(15/15, ๋ชฉํ 85% ์ด์: ๋ฌ์ฑ, ๋ฏธํธ์ถ ์์)\n",
"๋งค๋ด์ผ ์์ฑ๋ฅ 7/7์ฅ = 100%: 1. ๋ชฉ์ ๊ณผ ๋ฒ์, 2. ์ค์น์ ์คํ, 3. ์
๋ ฅ ๋ฐ์ดํฐ, 4. ๋๊ธฐํ์ ์ถฉ๋, 5. ๊ณต๊ธ๋ง ์ฌ๋ ์งํ, 6. ์ฌ๋ ๊ฒํ ์ ๊ฐ์ฌ ๋ก๊ทธ, 7. ํ๊ณ์ ํ์ฅ\n",
"\n",
"[๊ตฐ์๊ด ๋ธ๋ฆฌํ ์์ฝ]\n",
"1. ๋๊ธฐํ: ๋ฐ์ 1847๊ฑด, ์ค๋ณต ๊ฑฐ๋ถ 60๊ฑด, ๊ฒฉ๋ฆฌ 25๊ฑด. ๋ฏธ๋๊ธฐ 177๊ฑด(1๊ฐ ๋ณด๊ธ์).\n",
"2. ๊ฒํ ํ 6๊ฑด(์ถฉ๋ 5, ๊ณต๊ธ ์งํ 1): ๋ชจ๋ PENDING.\n",
"3. ๊ณต๊ธ๋ง ์ฌ๋ ์งํ ํ๋ณด ์
์ฒด: ๊ฐ์์
์ฒด1 (์ต๊ทผ 3์ผ ์
๊ณ ๋์ด ์ฒซ 3์ผ์ 30% ๋ฏธ๋ง).\n",
"4. ๋ณํฉ ํ์ ๊ณผ ๋ณด๊ธ ์กฐ์น๋ ๊ตฐ์๊ดยท์งํ๊ด์ด ํ๋ค. ๋ณธ ๋๊ตฌ๋ ๊ฐ์ ์๋ฃ ๊ธฐ๋ฐ ํ๋จ ๋ณด์กฐ์ด๋ค.\n",
"โป ๊ฐ์ ์๋ฃยท๋ฉ๋ชจ๋ฆฌ ๋ชจ์์ด๋ฉฐ ์ค์ ํต์ ยท์ ์ฅ ์ฅ์นยทํค ๊ด๋ฆฌยท์ธ์ ์ฒด๊ณ ์ฐ๋์ ํฌํจํ์ง ์๋๋ค. ํจ์ ์ปค๋ฒ๋ฆฌ์ง๋ ์ต์์ ํจ์ ํธ์ถ ๊ธฐ์ค์ด๋ค.\n",
"```\n",
" \n"
]
}
],
"metadata": {
"colab": {
"provenance": []
},
"kernelspec": {
"display_name": "Python 3",
"name": "python3"
},
"language_info": {
"name": "python"
}
},
"nbformat": 4,
"nbformat_minor": 0
}