{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# ๐ŸŽฏ [DAT10] ๋ฐ์ดํ„ฐ ๋งˆ์ด๋‹ ๊ธฐ๋ฐ˜ ๊ณต๊ธ‰๋ง ์žฌ๋‚œ ์‹๋ณ„ SaaS\n", "### Google Colab ์‹ค์Šต ํ™˜๊ฒฝ (๋ฐ์ดํ„ฐยท์ž๋™ํ™”ยท์—…๋ฌดํ”Œ๋žซํผ / Level 3)\n", "\n", "[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/edunlab/milicontent2/blob/main/uploads/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 }