{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# ๐ŸŽฏ [DAT09] ๊ตฐ์ˆ˜๋ฐ์ดํ„ฐ ์ „๋žต๊ธฐํšยทํ™˜๊ฒฝํ‰๊ฐ€ ๋Œ€์‹œ๋ณด๋“œ\n", "### Google Colab ์‹ค์Šต ํ™˜๊ฒฝ (๋ฐ์ดํ„ฐยท์ž๋™ํ™”ยท์—…๋ฌดํ”Œ๋žซํผ / Level 2)\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/DAT09/DAT09_solution.ipynb)\n", "\n", "---\n", "\n", "## ๐Ÿ“Œ 1. ํ”„๋กœ์ ํŠธ ๊ฐœ์š” ๋ฐ ๋ฐฐ๊ฒฝ\n", "\n", "| ํ•ญ๋ชฉ | ๋‚ด์šฉ |\n", "| :--- | :--- |\n", "| **๊ณผ์ • ๋ถ„๋ฅ˜** | ๋ฐ์ดํ„ฐยท์ž๋™ํ™”ยท์—…๋ฌดํ”Œ๋žซํผ (Level 2) |\n", "| **ํ•™์Šต ์†Œ์š” ์‹œ๊ฐ„** | 10๊ฐœ ๋ฏธ์…˜ (5์ผ / ์ด 27.0์‹œ๊ฐ„) |\n", "| **ํ™˜๊ฒฝ ์‚ฌ์–‘** | Python 3.10+, numpy, pandas, matplotlib, scipy, scikit-learn |\n", "\n", "### ๐Ÿ“– ํ”„๋กœ์ ํŠธ ๋ชฉํ‘œ\n", "๋ฏธ๊ตญ ๊ตญ๋ฐฉ๋ถ€(DoD)๋Š” ๊ตฐ์ˆ˜ ๋ฐ์ดํ„ฐ ์ „๋žต๊ณผ ํ™˜๊ฒฝ ํ‰๊ฐ€๋ฅผ ํ†ตํ•ด ์„œ๋กœ ๋‹ค๋ฅธ ์ฒด๊ณ„์— ํฉ์–ด์ง„ ๋ฐ์ดํ„ฐ์˜ ์ค‘๋ณต์„ ์—†์• ๊ณ  ํ‘œ์ค€ ํ‚ค์™€ ์ฝ”๋“œ๋ฅผ ์ •ํ•ด ์—…๋ฌด ์ค‘์‹ฌ์œผ๋กœ ํ†ตํ•ฉํ•˜๋ ค๋Š” ์ ‘๊ทผ์„ ๊ณต๊ฐœํ•ด ์™”๋‹ค. ์ด ์‚ฌ๋ก€์˜ ํ•™์Šต ์ดˆ์ ์€ ์ค‘๋ณต ์ œ๊ฑฐ, ํ‘œ์ค€ ํ‚คยท์ฝ”๋“œ, ์—…๋ฌด ์ค‘์‹ฌ ํ†ตํ•ฉ, ์—ญํ• ๋ณ„ ํ™”๋ฉด, ์ž๋™ ๊ฐฑ์‹ , ์žฌ์‚ฌ์šฉ์„ฑ์ด๋‹ค. ๋ณธ ํ”„๋กœ์ ํŠธ๋Š” ์ด๋ฅผ ๋ฐ”ํƒ•์œผ๋กœ ๊ต์œก์šฉ ๊ฐ€์ƒ ๋ณด๊ธ‰ยท์ •๋น„ยทํƒ„์•ฝ ์ฒด๊ณ„ 3์ข…์˜ ์žฌ๊ณ  ๋ณด๊ณ ๋ฅผ ์ฝ”๋“œ๋กœ ๋งŒ๋“ค์–ด ํ†ตํ•ฉ ๋Œ€์‹œ๋ณด๋“œ๋กœ ๊ฐ€์‹œํ™”ํ•œ๋‹ค. ํ•™์Šต์ž๋Š” ์—ฐ๋™ ๊ทœ๊ฒฉ ๋ถ„์„, ์ฝ”๋“œ ํ‘œ์ค€ํ™”์™€ ๊ฒฐ์ธก ์ „์ฒ˜๋ฆฌ, ํ†ตํ•ฉ ์ €์žฅ์†Œ์™€ ๊ฒ€์ฆ ๋ฃฐ, ๋ณ€๊ฒฝ ์ด๋ฒคํŠธ ๋™๊ธฐํ™”, ๋ถˆ์ผ์น˜ ์‹๋ณ„๊ณผ ํ’ˆ์งˆ ์ง€์ˆ˜, ์—ญํ• ๋ณ„ ์ƒํ™ฉํŒ, ๋ฐ์ดํ„ฐ๊ด€๋ฆฌ๊ด€ ์Šน์ธ, ๋Œ€๋Ÿ‰ ํŠธ๋žœ์žญ์…˜ ์‹œํ—˜, 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. ์ด๊ธฐ์ข… ๊ตฐ์ˆ˜ ์ •๋ณด์ฒด๊ณ„(DELIIS, ์žฅ๋น„์ •๋น„์ •๋ณด์ฒด๊ณ„, ํƒ„์•ฝ์ฒด๊ณ„) ๋ฐ์ดํ„ฐ ์—ฐ๋™ ๊ทœ๊ฒฉ ๋ถ„์„\n", "**์†Œ์š”์‹œ๊ฐ„**: ์•ฝ 1.5์‹œ๊ฐ„\n", "\n", "### ๐Ÿ“‹ ๋ฏธ์…˜ ๋ฐฐ๊ฒฝ ๋ฐ ์ˆ˜ํ–‰ ์ง€์นจ\n", "#### ๋ฏธ์…˜ ๋ฐฐ๊ฒฝ ๋ฐ ๋‹น์œ„์„ฑ\n", "\n", "์ด๊ธฐ์ข… ๊ตฐ์ˆ˜ ์ •๋ณด์ฒด๊ณ„(DELIIS, ์žฅ๋น„์ •๋น„, ํƒ„์•ฝ์ฒด๊ณ„) ๋ฐ์ดํ„ฐ ์—ฐ๋™ ๊ทœ๊ฒฉ์„ ๋ถ„์„ํ•œ๋‹ค. ์‹ค์ œ ์ „์ˆ  ํ˜„์žฅ์—์„œ๋Š” ๋‹จ์ˆœ ํƒ์ง€์— ๊ทธ์น˜์ง€ ์•Š๊ณ  ์‹ ์†ํ•œ ์œ„ํ˜‘ ํŒ๋‹จ๊ณผ ์‹ ๋ขฐ๋„ ๋†’์€ ์˜์‚ฌ๊ฒฐ์ •์ด ํ•„์ˆ˜์ ์ด๋ฏ€๋กœ, **์ด๊ธฐ์ข… ๊ตฐ์ˆ˜ ์ •๋ณด์ฒด๊ณ„ 3์ข…์˜ ํ’ˆ๋ชฉ์ฝ”๋“œยท๋ถ€๋Œ€์ฝ”๋“œยท๋‚ ์งœยท์ˆ˜๋Ÿ‰ ์—ฐ๋™ ๊ทœ๊ฒฉ์„ ๋น„๊ต ๋ถ„์„ํ•˜๊ณ , ํ†ตํ•ฉ ๊ตฐ์ˆ˜ ์ƒํ™ฉ ํŒŒ์•… ์—…๋ฌด๋ฅผ AS-IS/TO-BE๋กœ ๋น„๊ตํ•œ๋‹ค.** ๊ณผ์—…์„ ์™„์ˆ˜ํ•˜์—ฌ ์˜คํŒ์„ ๋ฐฉ์ง€ํ•˜๊ณ  ์ž‘์ „ ๋Œ€์‘๋ ฅ์„ ๊ทน๋Œ€ํ™”ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.\n", "\n", "์ด ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด ๋ณธ ๋ฏธ์…˜์—์„œ๋Š” **๊ฐ€์ƒ์˜ ๋ณด๊ธ‰ยท์ •๋น„ยทํƒ„์•ฝ ์ฒด๊ณ„๊ฐ€ ๊ฐ™์€ ์žฌ๊ณ ๋ฅผ ์„œ๋กœ ๋‹ค๋ฅธ ์ฝ”๋“œ์™€ ๋‚ ์งœ ํ˜•์‹์œผ๋กœ ๋ณด๊ณ ํ•˜๋Š” ์ž๋ฃŒ๋ฅผ ์ฝ”๋“œ๋กœ ์ž‘์„ฑํ•˜์—ฌ ์›์ธ์„ ๋ถ„์„ํ•˜๋Š”** ์‹ค์Šต์„ ์ฒด๊ณ„์ ์œผ๋กœ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค.\n", "\n", "ํ•™์Šต์ž๋Š” ์‹ค์Šต๊ฐ€์ด๋“œ์— ์ œ์‹œ๋œ ์ด 4๊ฐœ ์„ธ๋ถ€ ํ™œ๋™(๊ธฐ๋ณธ ๊ธฐ๋Šฅ ๊ตฌํ˜„ โž” ๊ฒฐ๊ณผ ์ง€ํ‘œ ๊ฒ€์ฆ โž” ํŒŒ๋ผ๋ฏธํ„ฐ ํŠœ๋‹ ๋ฐ ๋„์ „๊ณผ์ œ)์„ ์ˆœ์ฐจ์ ์œผ๋กœ ์ง„ํ–‰ํ•˜๋ฉฐ ํ•ต์‹ฌ ํŒ์ • ๊ธฐ์ค€์„ ๋‹ฌ์„ฑํ•ฉ๋‹ˆ๋‹ค.\n", "\n", "**์ด๋ฒˆ ๊ฒฐ๊ณผ๋ฌผ:** `docs/dat09_as_is_to_be_spec.md` ํ˜•ํƒœ์˜ ๊ฒฐ๊ณผ๋ฌผ์„ ์™„์„ฑํ•œ๋‹ค.\n", "\n", "### ๐Ÿ” ํ‰๊ฐ€ ๋ฐ ๊ฒ€์ฆ ๊ธฐ์ค€ (PASS ์กฐ๊ฑด)\n", "#### ๋ฏธ์…˜ ํŒ์ •\n", "\n", "**ํ‰๊ฐ€ ๊ตฌ๋ถ„:** ํ•„์ˆ˜\n", "\n", "**ํ•ต์‹ฌ ํ‰๊ฐ€์˜์—ญ:** ํ˜„ํ–‰ ์—…๋ฌด(AS-IS)์™€ ๊ฐœ์„  ์—…๋ฌด(TO-BE) ๋น„๊ต์˜ ๋ช…ํ™•์„ฑ\n", "\n", "#### ๋ฏธ์…˜ PASS ์กฐ๊ฑด\n", "\n", "- ์ •๋Ÿ‰ ๊ธฐ์ค€: ์—…๋ฌด ๋‹จ๊ณ„ ๋ถ„ํ•ด 10๋‹จ๊ณ„ ์ด์ƒ, ๋ณ‘๋ชฉ ๊ตฌ๊ฐ„ 3๊ฐœ์†Œ ์ด์ƒ ๋„์ถœ\n", "\n", "\n", "- ์ง„ํ–‰ ๊ธฐ์ค€: ์—…๋ฌด ์ •์˜์„œ ๋ฐ ์ž…์ถœ๋ ฅ ๋ฐ์ดํ„ฐ ๋ช…์„ธ ์ž‘์„ฑ ์™„๋ฃŒ\n", "\n", "\n", "- ๊ฒ€์ฆ ๋ฐฉ๋ฒ•: ์‹ค๋ฌด ํ”„๋กœ์„ธ์Šค์™€์˜ ์ผ์น˜์„ฑ ๊ฒ€์ฆ\n", "\n", "\n", "#### ์ฑ„์ ์ž ํŒ์ • ๋ฉ”๋ชจ\n", "\n", "> ๋‹จ์ˆœ ์ฝ”๋“œ ์‹คํ–‰ ์—ฌ๋ถ€๋ฟ๋งŒ ์•„๋‹ˆ๋ผ ์ถœ๋ ฅ๋œ ์ˆ˜์น˜์™€ ๊ทผ๊ฑฐ ์ž๋ฃŒ๊ฐ€ ๊ตฐ ์‹ค๋ฌด ์š”๊ตฌ์‚ฌํ•ญ์— ๋ถ€ํ•ฉํ•˜๋Š”์ง€ ๋ฉด๋ฐ€ํžˆ ํ™•์ธํ•œ๋‹ค.\n", "\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "\"\"\"๊ฐ€์ƒ ์ด๊ธฐ์ข… ๊ตฐ์ˆ˜์ •๋ณด์ฒด๊ณ„ 3์ข…(๋ณด๊ธ‰ยท์ •๋น„ยทํƒ„์•ฝ)์˜ ํ’ˆ๋ชฉ์ฝ”๋“œยท๋ถ€๋Œ€์ฝ”๋“œยท๋‚ ์งœยท์ˆ˜๋Ÿ‰ ์—ฐ๋™ ๊ทœ๊ฒฉ์„ ๋น„๊ต ๋ถ„์„ํ•ด ์ฝ”๋“œ ๋ณ€ํ™˜ ๊ทœ์น™์„ ์ •์˜ํ•˜๊ณ , ํ†ตํ•ฉ ๊ตฐ์ˆ˜ ์ƒํ™ฉ ํŒŒ์•… ์—…๋ฌด๋ฅผ AS-IS/TO-BE ๋‹จ๊ณ„๋กœ ๋ถ„ํ•ดํ•ด ๋ณ‘๋ชฉ ๊ตฌ๊ฐ„์„ ๋„์ถœํ•œ๋‹ค.\"\"\"\n", "import datetime\n", "import numpy as np\n", "import pandas as pd\n", "\n", "# ---------------------------------------------------------------------------\n", "# [๊ต์œก์šฉ๊ฐ€์ƒ์ž๋ฃŒ] ๊ฐ€์ƒ ์ด๊ธฐ์ข… ๊ตฐ์ˆ˜์ •๋ณด์ฒด๊ณ„ 3์ข…(A ๋ณด๊ธ‰, B ์ •๋น„, C ํƒ„์•ฝ)์˜ ์žฌ๊ณ  ๋ณด๊ณ  ์ƒ์„ฑ๊ธฐ\n", "# ์‹ค์ œ ์ฒด๊ณ„๋ช…ยท๋ถ€๋Œ€ยทํ’ˆ๋ชฉยท์ˆ˜๋Ÿ‰ ์—†์Œ. ๊ฐ™์€ (ํ’ˆ๋ชฉ, ๋ถ€๋Œ€) ์žฌ๊ณ ๋ฅผ ์„ธ ์ฒด๊ณ„๊ฐ€ ์„œ๋กœ ๋‹ค๋ฅธ ์ฝ”๋“œยท๋‚ ์งœ ํ˜•์‹์œผ๋กœ ๋ณด๊ณ ํ•œ๋‹ค.\n", "# ๊ธฐ์ค€ ๋‚ ์งœ: 2026-09-30(๊ฐ€์ƒ). ๋งˆ์Šคํ„ฐ(์ •๋‹ต) ์žฌ๊ณ  TRUE ์™€ ์ฒด๊ณ„๋ณ„ ๋ณด๊ณ ์—๋Š” ์˜๋„์  ๋ถˆ์ผ์น˜ยท๊ฒฐ์ธก์ด ์„ž์—ฌ ์žˆ๋‹ค.\n", "# ---------------------------------------------------------------------------\n", "N_PART, N_UNIT = 60, 12\n", "CATS = [\"๊ธ‰์‹\", \"ํ”ผ๋ณต\", \"์œ ๋ฅ˜\", \"์ •๋น„์†Œ๋ชจ\", \"์ˆ˜๋ฆฌ๋ถ€์†\", \"ํƒ„์•ฝ\"]\n", "PARTS = {p: {\"name\": f\"๊ฐ€์ƒํ’ˆ๋ชฉ{p:02d}\", \"cat\": CATS[(p - 1) % 6]} for p in range(1, N_PART + 1)}\n", "FMT = {\"A\": dict(part=lambda p: f\"AP-{p:05d}\", unit=lambda u: f\"U{u:02d}\", date=lambda d: d.strftime(\"%Y%m%d\")),\n", " \"B\": dict(part=lambda p: f\"{p:05d}-X\", unit=lambda u: f\"๊ฐ€์ƒ{u:02d}๋Œ€๋Œ€\", date=lambda d: d.strftime(\"%Y-%m-%d\")),\n", " \"C\": dict(part=lambda p: f\"P{p}\", unit=lambda u: str(u), date=lambda d: d.strftime(\"%d/%m/%Y\"))}\n", "BASE_DATE = datetime.date(2026, 9, 30)\n", "\n", "\n", "def make_systems(seed=9, mismatch=0.10, missing=0.03):\n", " \"\"\"TRUE(์ •๋‹ต ์žฌ๊ณ )์™€ ์ฒด๊ณ„๋ณ„ ์›๋ณธ ๋ณด๊ณ  ๋ ˆ์ฝ”๋“œ(์ฒด๊ณ„ ๊ณ ์œ  ํ˜•์‹)๋ฅผ ๋งŒ๋“ ๋‹ค.\"\"\"\n", " rng = np.random.default_rng(seed)\n", " true = {(p, u): int(rng.integers(20, 500)) for p in PARTS for u in range(1, N_UNIT + 1)}\n", " out = {s: [] for s in \"ABC\"}\n", " truth_diff = {} # (์ฒด๊ณ„, p, u) -> ๋ถˆ์ผ์น˜ ์›์ธ(์ •๋‹ต)\n", " for (p, u), q in true.items():\n", " for s in \"ABC\":\n", " qty, cause = q, None\n", " r = rng.random()\n", " if r < mismatch / 3: # ์ž…๋ ฅ ์˜ค๋ฅ˜(์ž๋ฆฟ์ˆ˜/์˜คํƒ€)\n", " qty, cause = q + int(rng.choice([-1, 1]) * rng.integers(10, 60)), \"์ž…๋ ฅ ์˜ค๋ฅ˜\"\n", " elif r < 2 * mismatch / 3: # ๋ฐ˜์˜ ์ง€์—ฐ(์ „์ผ ๊ฐ’)\n", " qty, cause = q + int(rng.integers(5, 40)), \"๋ฐ˜์˜ ์ง€์—ฐ\"\n", " elif r < mismatch: # ๋‹จ์œ„ ์˜ค๋ฅ˜(10๋ฐฐ)\n", " qty, cause = q * 10, \"๋‹จ์œ„ ์˜ค๋ฅ˜\"\n", " d = BASE_DATE - datetime.timedelta(days=int(cause == \"๋ฐ˜์˜ ์ง€์—ฐ\"))\n", " if rng.random() < missing: # ๊ฒฐ์ธก\n", " qty, cause = None, \"๊ฒฐ์ธก\"\n", " if cause:\n", " truth_diff[(s, p, u)] = cause\n", " out[s].append({\"part\": FMT[s][\"part\"](p), \"unit\": FMT[s][\"unit\"](u), \"date\": FMT[s][\"date\"](d), \"qty\": qty})\n", " return true, out, truth_diff\n", "\n", "\n", "def parse_part(code):\n", " return int(code[3:]) if code.startswith(\"AP-\") else int(code[:-2]) if code.endswith(\"-X\") else int(code[1:])\n", "\n", "\n", "def parse_unit(code):\n", " return int(code[1:]) if code.startswith(\"U\") else int(code[2:4]) if code.startswith(\"๊ฐ€์ƒ\") else int(code)\n", "\n", "\n", "def parse_date(s):\n", " if \"/\" in s:\n", " d, m, y = s.split(\"/\"); return datetime.date(int(y), int(m), int(d))\n", " if \"-\" in s:\n", " return datetime.date(*map(int, s.split(\"-\")))\n", " return datetime.date(int(s[:4]), int(s[4:6]), int(s[6:]))\n", "\n", "true, raw, tdiff = make_systems()\n", "\n", "# 1) ์ฒด๊ณ„๋ณ„ ์—ฐ๋™ ๊ทœ๊ฒฉ ๋น„๊ตํ‘œ(ํ•„๋“œ ํ˜•์‹ ์‹ค์ œ ์ƒ˜ํ”Œ์—์„œ ์ถ”์ถœ)\n", "spec = pd.DataFrame({\"์ฒด๊ณ„\": [\"A(๋ณด๊ธ‰)\", \"B(์ •๋น„)\", \"C(ํƒ„์•ฝ)\"],\n", " \"ํ’ˆ๋ชฉ์ฝ”๋“œ ์˜ˆ\": [raw[s][0][\"part\"] for s in \"ABC\"], \"๋ถ€๋Œ€์ฝ”๋“œ ์˜ˆ\": [raw[s][0][\"unit\"] for s in \"ABC\"],\n", " \"๋‚ ์งœ ์˜ˆ\": [raw[s][0][\"date\"] for s in \"ABC\"], \"์ˆ˜๋Ÿ‰ ํ•„๋“œ\": [\"qty(์ •์ˆ˜)\"] * 3,\n", " \"๊ฐฑ์‹  ์ฃผ๊ธฐ\": [\"์ผ 1ํšŒ\", \"์ˆ˜์‹œ\", \"์ฃผ 1ํšŒ\"], \"๊ฒฐ์ธก ํ‘œ๊ธฐ\": [\"๋นˆ์นธ\", \"null\", \"-1\"]})\n", "print(\"[์ฒด๊ณ„๋ณ„ ์—ฐ๋™ ๊ทœ๊ฒฉ ๋น„๊ต] [๊ต์œก์šฉ๊ฐ€์ƒ์ž๋ฃŒ]\")\n", "print(spec.to_string(index=False))\n", "\n", "# 2) ๋ณ€ํ™˜ ๊ทœ์น™ ๊ฒ€์ฆ: ์„ธ ์ฒด๊ณ„์˜ ์ฝ”๋“œ๊ฐ€ ๋งˆ์Šคํ„ฐ ํ‚ค๋กœ ์ •ํ™•ํžˆ 1:1 ๋ณ€ํ™˜๋˜๋Š”์ง€\n", "rule_ok = all(parse_part(FMT[s][\"part\"](p)) == p and parse_unit(FMT[s][\"unit\"](u)) == u for s in \"ABC\" for p in PARTS for u in range(1, N_UNIT + 1))\n", "date_ok = all(parse_date(FMT[s][\"date\"](BASE_DATE)) == BASE_DATE for s in \"ABC\")\n", "print(f\"\\n์ฝ”๋“œ ๋ณ€ํ™˜ ๊ทœ์น™ ์™•๋ณต ๊ฒ€์ฆ: ํ’ˆ๋ชฉ {N_PART}์ข… x ๋ถ€๋Œ€ {N_UNIT}๊ฐœ x 3์ฒด๊ณ„ {'์ „๋ถ€ ์ผ์น˜' if rule_ok else '๋ถˆ์ผ์น˜'}, ๋‚ ์งœ ํ˜•์‹ 3์ข… {'์ผ์น˜' if date_ok else '๋ถˆ์ผ์น˜'}\")\n", "\n", "# 3) ์—ฐ๋™ ํ˜„ํ™ฉ: ํ‚ค ์ผ์น˜๋„ยท๋ถˆ์ผ์น˜ยท๊ฒฐ์ธก(์ฒด๊ณ„ ๊ฐ„ ๋น„๊ต)\n", "rows = []\n", "for s in \"ABC\":\n", " for r in raw[s]:\n", " rows.append({\"sys\": s, \"p\": parse_part(r[\"part\"]), \"u\": parse_unit(r[\"unit\"]), \"date\": parse_date(r[\"date\"]), \"qty\": r[\"qty\"]})\n", "df = pd.DataFrame(rows)\n", "wide = df.pivot_table(index=[\"p\", \"u\"], columns=\"sys\", values=\"qty\", aggfunc=\"first\")\n", "miss = wide.isna().sum()\n", "diff = (wide.max(axis=1) != wide.min(axis=1)) & wide.notna().all(axis=1)\n", "print(f\"์„ธ ์ฒด๊ณ„ ๋ณด๊ณ  {len(df)}๊ฑด / ๋น„๊ต ํ‚ค {len(wide)}๊ฐœ: ๊ฒฐ์ธก A {miss['A']}ยทB {miss['B']}ยทC {miss['C']}๊ฑด, ์ฒด๊ณ„ ๊ฐ„ ์ˆ˜๋Ÿ‰ ๋ถˆ์ผ์น˜ ํ‚ค {int(diff.sum())}๊ฐœ({diff.mean():.1%})\")\n", "print(\"๋‚ ์งœ ์ง€์—ฐ(๊ธฐ์ค€์ผ ์ด์ „ ๋ณด๊ณ ) ๊ฑด์ˆ˜:\", int((df[\"date\"] < BASE_DATE).sum()))\n", "\n", "# 4) ํ†ตํ•ฉ ๊ตฐ์ˆ˜ ์ƒํ™ฉ ํŒŒ์•… ์—…๋ฌด AS-IS / TO-BE (๊ฐ€์ƒ ์ธก์ • ์†Œ์š” ๋ถ„)\n", "steps = [(\"์ฒด๊ณ„ A ์žฌ๊ณ  ์กฐํšŒยท๋‚ด๋ ค๋ฐ›๊ธฐ\", 25, \"์ •ํ•ด์ง„ ์‹œ๊ฐ ์ž๋™ ์ˆ˜์ง‘\", 2), (\"์ฒด๊ณ„ B ์ •๋น„ ํ˜„ํ™ฉ ์กฐํšŒยท๋‚ด๋ ค๋ฐ›๊ธฐ\", 30, \"์ž๋™ ์ˆ˜์ง‘\", 2),\n", " (\"์ฒด๊ณ„ C ํƒ„์•ฝ ํ˜„ํ™ฉ ์š”์ฒญ(์ „ํ™”ยท๋ฉ”์ผ)\", 45, \"์—ฐ๋™ ์š”์ฒญ ๊ทœ๊ฒฉํ™”ยท์ž๋™ ์ˆ˜์ง‘\", 5), (\"์—‘์…€๋กœ 3๊ฐœ ์ž๋ฃŒ ํ•ฉ์น˜๊ธฐ\", 60, \"ํ‘œ์ค€ ํ‚ค๋กœ ์ž๋™ ๋ณ‘ํ•ฉ\", 1),\n", " (\"ํ’ˆ๋ชฉ์ฝ”๋“œ ์ˆ˜๊ธฐ ๋Œ€์กฐ\", 70, \"์ฝ”๋“œ ๋ณ€ํ™˜ํ‘œ ์ž๋™ ์ ์šฉ\", 1), (\"๋ถ€๋Œ€๋ช…ยท๋‚ ์งœ ํ˜•์‹ ํ†ต์ผ\", 35, \"๋ณ€ํ™˜ ๊ทœ์น™ ์ž๋™ ์ ์šฉ\", 1),\n", " (\"๊ฒฐ์ธกยท์ด์ƒ๊ฐ’ ํ™•์ธ(์ „ํ™” ๋ฌธ์˜)\", 55, \"๊ฒฐ์ธกยท๋ถˆ์ผ์น˜ ํ›„๋ณด ์ž๋™ ํ‘œ์‹œ\", 10), (\"์ฒด๊ณ„ ๊ฐ„ ๋ถˆ์ผ์น˜ ์›์ธ ํ™•์ธ\", 50, \"์›์ธ ํ›„๋ณด ์ž๋™ ์ถ”์ฒœ\", 12),\n", " (\"์ง€ํ‘œ(์žฌ๊ณ ยท๊ฐ€๋™ยท์†Œ๋ชจ) ์ง‘๊ณ„\", 40, \"์ง‘๊ณ„ ์ฟผ๋ฆฌ ์ž๋™ ์‹คํ–‰\", 1), (\"๊ตฐ์ˆ˜์ฐธ๋ชจ ๋ณด๊ณ ์šฉ ํ‘œยท๊ทธ๋ž˜ํ”„ ์ž‘์„ฑ\", 65, \"์ƒํ™ฉํŒ ์ž๋™ ๊ฐฑ์‹ \", 2),\n", " (\"๋งˆ์Šคํ„ฐ ๋ฐ์ดํ„ฐ ์ˆ˜์ • ์š”์ฒญยทํ™•์ •\", 45, \"๋ฐ์ดํ„ฐ๊ด€๋ฆฌ๊ด€ ์Šน์ธ ํ™”๋ฉด\", 10), (\"๋ณด๊ณ  ํ›„ ์ˆ˜์ •ยท์žฌ๋ฐฐํฌ\", 30, \"๋‹จ์ผ ๋ฐ์ดํ„ฐ ์›์ฒœ ์‚ฌ์šฉ\", 3)]\n", "proc = pd.DataFrame(steps, columns=[\"์—…๋ฌด ๋‹จ๊ณ„\", \"AS-IS(๋ถ„)\", \"TO-BE ๊ฐœ์„ \", \"TO-BE(๋ถ„)\"])\n", "proc.index = range(1, len(proc) + 1)\n", "thr = proc[\"AS-IS(๋ถ„)\"].mean() + 0.5 * proc[\"AS-IS(๋ถ„)\"].std()\n", "proc[\"๋ณ‘๋ชฉ\"] = np.where(proc[\"AS-IS(๋ถ„)\"] >= thr, \"๋ณ‘๋ชฉ\", \"\")\n", "print(f\"\\n[AS-IS / TO-BE ์—…๋ฌด ๋‹จ๊ณ„ {len(proc)}๋‹จ๊ณ„] (๋ณ‘๋ชฉ ๊ธฐ์ค€: ํ‰๊ท +0.5ํ‘œ์ค€ํŽธ์ฐจ = {thr:.0f}๋ถ„ ์ด์ƒ)\")\n", "print(proc[[\"์—…๋ฌด ๋‹จ๊ณ„\", \"AS-IS(๋ถ„)\", \"TO-BE(๋ถ„)\", \"๋ณ‘๋ชฉ\"]].to_string())\n", "nb = int((proc[\"๋ณ‘๋ชฉ\"] == \"๋ณ‘๋ชฉ\").sum())\n", "a, t = proc[\"AS-IS(๋ถ„)\"].sum(), proc[\"TO-BE(๋ถ„)\"].sum()\n", "print(f\"\\n์—…๋ฌด ๋‹จ๊ณ„ {len(proc)}๋‹จ๊ณ„(๋ชฉํ‘œ 10๋‹จ๊ณ„ ์ด์ƒ: {'๋‹ฌ์„ฑ' if len(proc) >= 10 else '๋ฏธ๋‹ฌ'}), ๋ณ‘๋ชฉ {nb}๊ฐœ์†Œ(๋ชฉํ‘œ 3๊ฐœ์†Œ ์ด์ƒ: {'๋‹ฌ์„ฑ' if nb >= 3 else '๋ฏธ๋‹ฌ'})\")\n", "print(f\"1ํšŒ ์ƒํ™ฉ ํŒŒ์•… AS-IS {a}๋ถ„ -> TO-BE {t}๋ถ„ ({1 - t / a:.0%} ๋‹จ์ถ• ์˜ˆ์ƒ, ๊ฐ€์ •๊ฐ’)\")\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "
\n", "๐Ÿ’ก [์ฐธ๊ณ ] ๋ฏธ์…˜ 01 ์˜ˆ์ƒ ์‹คํ–‰ ๊ฒฐ๊ณผ (Output)\n", "\n", "```text\n", "[์ฒด๊ณ„๋ณ„ ์—ฐ๋™ ๊ทœ๊ฒฉ ๋น„๊ต] [๊ต์œก์šฉ๊ฐ€์ƒ์ž๋ฃŒ]\n", " ์ฒด๊ณ„ ํ’ˆ๋ชฉ์ฝ”๋“œ ์˜ˆ ๋ถ€๋Œ€์ฝ”๋“œ ์˜ˆ ๋‚ ์งœ ์˜ˆ ์ˆ˜๋Ÿ‰ ํ•„๋“œ ๊ฐฑ์‹  ์ฃผ๊ธฐ ๊ฒฐ์ธก ํ‘œ๊ธฐ\n", "A(๋ณด๊ธ‰) AP-00001 U01 20260930 qty(์ •์ˆ˜) ์ผ 1ํšŒ ๋นˆ์นธ\n", "B(์ •๋น„) 00001-X ๊ฐ€์ƒ01๋Œ€๋Œ€ 2026-09-30 qty(์ •์ˆ˜) ์ˆ˜์‹œ null\n", "C(ํƒ„์•ฝ) P1 1 30/09/2026 qty(์ •์ˆ˜) ์ฃผ 1ํšŒ -1\n", "\n", "์ฝ”๋“œ ๋ณ€ํ™˜ ๊ทœ์น™ ์™•๋ณต ๊ฒ€์ฆ: ํ’ˆ๋ชฉ 60์ข… x ๋ถ€๋Œ€ 12๊ฐœ x 3์ฒด๊ณ„ ์ „๋ถ€ ์ผ์น˜, ๋‚ ์งœ ํ˜•์‹ 3์ข… ์ผ์น˜\n", "์„ธ ์ฒด๊ณ„ ๋ณด๊ณ  2160๊ฑด / ๋น„๊ต ํ‚ค 720๊ฐœ: ๊ฒฐ์ธก A 18ยทB 14ยทC 14๊ฑด, ์ฒด๊ณ„ ๊ฐ„ ์ˆ˜๋Ÿ‰ ๋ถˆ์ผ์น˜ ํ‚ค 176๊ฐœ(24.4%)\n", "๋‚ ์งœ ์ง€์—ฐ(๊ธฐ์ค€์ผ ์ด์ „ ๋ณด๊ณ ) ๊ฑด์ˆ˜: 73\n", "\n", "[AS-IS / TO-BE ์—…๋ฌด ๋‹จ๊ณ„ 12๋‹จ๊ณ„] (๋ณ‘๋ชฉ ๊ธฐ์ค€: ํ‰๊ท +0.5ํ‘œ์ค€ํŽธ์ฐจ = 53๋ถ„ ์ด์ƒ)\n", " ์—…๋ฌด ๋‹จ๊ณ„ AS-IS(๋ถ„) TO-BE(๋ถ„) ๋ณ‘๋ชฉ\n", "1 ์ฒด๊ณ„ A ์žฌ๊ณ  ์กฐํšŒยท๋‚ด๋ ค๋ฐ›๊ธฐ 25 2 \n", "2 ์ฒด๊ณ„ B ์ •๋น„ ํ˜„ํ™ฉ ์กฐํšŒยท๋‚ด๋ ค๋ฐ›๊ธฐ 30 2 \n", "3 ์ฒด๊ณ„ C ํƒ„์•ฝ ํ˜„ํ™ฉ ์š”์ฒญ(์ „ํ™”ยท๋ฉ”์ผ) 45 5 \n", "4 ์—‘์…€๋กœ 3๊ฐœ ์ž๋ฃŒ ํ•ฉ์น˜๊ธฐ 60 1 ๋ณ‘๋ชฉ\n", "5 ํ’ˆ๋ชฉ์ฝ”๋“œ ์ˆ˜๊ธฐ ๋Œ€์กฐ 70 1 ๋ณ‘๋ชฉ\n", "6 ๋ถ€๋Œ€๋ช…ยท๋‚ ์งœ ํ˜•์‹ ํ†ต์ผ 35 1 \n", "7 ๊ฒฐ์ธกยท์ด์ƒ๊ฐ’ ํ™•์ธ(์ „ํ™” ๋ฌธ์˜) 55 10 ๋ณ‘๋ชฉ\n", "8 ์ฒด๊ณ„ ๊ฐ„ ๋ถˆ์ผ์น˜ ์›์ธ ํ™•์ธ 50 12 \n", "9 ์ง€ํ‘œ(์žฌ๊ณ ยท๊ฐ€๋™ยท์†Œ๋ชจ) ์ง‘๊ณ„ 40 1 \n", "10 ๊ตฐ์ˆ˜์ฐธ๋ชจ ๋ณด๊ณ ์šฉ ํ‘œยท๊ทธ๋ž˜ํ”„ ์ž‘์„ฑ 65 2 ๋ณ‘๋ชฉ\n", "11 ๋งˆ์Šคํ„ฐ ๋ฐ์ดํ„ฐ ์ˆ˜์ • ์š”์ฒญยทํ™•์ • 45 10 \n", "12 ๋ณด๊ณ  ํ›„ ์ˆ˜์ •ยท์žฌ๋ฐฐํฌ 30 3 \n", "\n", "์—…๋ฌด ๋‹จ๊ณ„ 12๋‹จ๊ณ„(๋ชฉํ‘œ 10๋‹จ๊ณ„ ์ด์ƒ: ๋‹ฌ์„ฑ), ๋ณ‘๋ชฉ 4๊ฐœ์†Œ(๋ชฉํ‘œ 3๊ฐœ์†Œ ์ด์ƒ: ๋‹ฌ์„ฑ)\n", "1ํšŒ ์ƒํ™ฉ ํŒŒ์•… AS-IS 550๋ถ„ -> TO-BE 50๋ถ„ (91% ๋‹จ์ถ• ์˜ˆ์ƒ, ๊ฐ€์ •๊ฐ’)\n", "```\n", "
\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "---\n", "\n", "## ๐ŸŽฏ ๋ฏธ์…˜ 02. ์„œ๋กœ ๋‹ค๋ฅธ ๋ถ€ํ’ˆ์ฝ”๋“œ, ๋ถ€๋Œ€์ฝ”๋“œ, ๋‚ ์งœ ํฌ๋งท ํ†ตํ•ฉ ๋ฐ ๊ฒฐ์ธก ๋ฐ์ดํ„ฐ ์ „์ฒ˜๋ฆฌ ํŒŒ์ดํ”„๋ผ์ธ ๊ตฌ์ถ•\n", "**์†Œ์š”์‹œ๊ฐ„**: ์•ฝ 2.0์‹œ๊ฐ„\n", "\n", "### ๐Ÿ“‹ ๋ฏธ์…˜ ๋ฐฐ๊ฒฝ ๋ฐ ์ˆ˜ํ–‰ ์ง€์นจ\n", "#### ๋ฏธ์…˜ ๋ฐฐ๊ฒฝ ๋ฐ ๋‹น์œ„์„ฑ\n", "\n", "๋ถ€ํ’ˆ์ฝ”๋“œ, ๋ถ€๋Œ€์ฝ”๋“œ, ๋‚ ์งœ ํฌ๋งท ํ†ตํ•ฉ ๋ฐ ๊ฒฐ์ธก ๋ฐ์ดํ„ฐ ์ „์ฒ˜๋ฆฌ ํŒŒ์ดํ”„๋ผ์ธ์„ ๊ตฌ์ถ•ํ•œ๋‹ค. ์‹ค์ œ ์ „์ˆ  ํ˜„์žฅ์—์„œ๋Š” ๋‹จ์ˆœ ํƒ์ง€์— ๊ทธ์น˜์ง€ ์•Š๊ณ  ์‹ ์†ํ•œ ์œ„ํ˜‘ ํŒ๋‹จ๊ณผ ์‹ ๋ขฐ๋„ ๋†’์€ ์˜์‚ฌ๊ฒฐ์ •์ด ํ•„์ˆ˜์ ์ด๋ฏ€๋กœ, **์ฒด๊ณ„๋งˆ๋‹ค ๋‹ค๋ฅธ ํŒŒ์ผ ํ˜•์‹(CSV, JSON, ํƒญ ๊ตฌ๋ถ„ ์—‘์…€ ๋‚ด๋ณด๋‚ด๊ธฐ)๊ณผ ํ’ˆ๋ชฉ์ฝ”๋“œยท๋ถ€๋Œ€์ฝ”๋“œยท๋‚ ์งœ ํ˜•์‹์„ ํ‘œ์ค€ ํ‚ค๋กœ ํ†ตํ•ฉํ•˜๊ณ  ๊ฒฐ์ธก ๋ฐ์ดํ„ฐ๋ฅผ ์ „์ฒ˜๋ฆฌํ•˜๋Š” ํŒŒ์ดํ”„๋ผ์ธ์„ ๊ตฌ์ถ•ํ•œ๋‹ค.** ๊ณผ์—…์„ ์™„์ˆ˜ํ•˜์—ฌ ์˜คํŒ์„ ๋ฐฉ์ง€ํ•˜๊ณ  ์ž‘์ „ ๋Œ€์‘๋ ฅ์„ ๊ทน๋Œ€ํ™”ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.\n", "\n", "์ด ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด ๋ณธ ๋ฏธ์…˜์—์„œ๋Š” **์„ธ ์ฒด๊ณ„์˜ ๊ฐ€์ƒ ๋ณด๊ณ ๋ฅผ ๋ฉ”๋ชจ๋ฆฌ ์•ˆ์˜ ๋ฌธ์ž์—ด๋กœ ๋งŒ๋“ค์–ด ํ˜•์‹๋ณ„ ํŒŒ์„œ๋กœ ์ฝ๊ณ , ๋จธ๋ฆฌ๊ธ€ ๋ณ„์นญ ์‚ฌ์ „์œผ๋กœ ์–‘์‹ ๋ณ€๊ฒฝ์— ๋Œ€์‘ ์ˆ˜ํ–‰ํ•˜๊ณ  ๊ฒ€์ฆํ•˜๋Š”** ์‹ค์Šต์„ ์ฒด๊ณ„์ ์œผ๋กœ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค.\n", "\n", "ํ•™์Šต์ž๋Š” ์‹ค์Šต๊ฐ€์ด๋“œ์— ์ œ์‹œ๋œ ์ด 4๊ฐœ ์„ธ๋ถ€ ํ™œ๋™(๊ธฐ๋ณธ ๊ธฐ๋Šฅ ๊ตฌํ˜„ โž” ๊ฒฐ๊ณผ ์ง€ํ‘œ ๊ฒ€์ฆ โž” ํŒŒ๋ผ๋ฏธํ„ฐ ํŠœ๋‹ ๋ฐ ๋„์ „๊ณผ์ œ)์„ ์ˆœ์ฐจ์ ์œผ๋กœ ์ง„ํ–‰ํ•˜๋ฉฐ ํ•ต์‹ฌ ํŒ์ • ๊ธฐ์ค€์„ ๋‹ฌ์„ฑํ•ฉ๋‹ˆ๋‹ค.\n", "\n", "**์ด๋ฒˆ ๊ฒฐ๊ณผ๋ฌผ:** `src/connectors/dat09_data_loader.py ๋ฐ ์Šคํ‚ค๋งˆ ์ •์˜์„œ` ํ˜•ํƒœ์˜ ๊ฒฐ๊ณผ๋ฌผ์„ ์™„์„ฑํ•œ๋‹ค.\n", "\n", "### ๐Ÿ” ํ‰๊ฐ€ ๋ฐ ๊ฒ€์ฆ ๊ธฐ์ค€ (PASS ์กฐ๊ฑด)\n", "#### ๋ฏธ์…˜ ํŒ์ •\n", "\n", "**ํ‰๊ฐ€ ๊ตฌ๋ถ„:** ํ•„์ˆ˜\n", "\n", "**ํ•ต์‹ฌ ํ‰๊ฐ€์˜์—ญ:** ๊ตญ๋ฐฉ ์„œ์‹/์–‘์‹ ๋ณ€๊ฒฝ์— ๋Œ€ํ•œ ์œ ์—ฐํ•œ ํ™•์žฅ์„ฑ\n", "\n", "#### ๋ฏธ์…˜ PASS ์กฐ๊ฑด\n", "\n", "- ์ •๋Ÿ‰ ๊ธฐ์ค€: ์ง€์› ํŒŒ์ผ ํฌ๋งท(CSV, Excel, JSON ๋“ฑ) ํŒŒ์‹ฑ ์„ฑ๊ณต๋ฅ  100%\n", "\n", "\n", "- ์ง„ํ–‰ ๊ธฐ์ค€: ๋ฐ์ดํ„ฐ ๋กœ๋” ๋ฐ ์Šคํ‚ค๋งˆ ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ ๋ชจ๋“ˆ ์™„์„ฑ\n", "\n", "\n", "- ๊ฒ€์ฆ ๋ฐฉ๋ฒ•: ๋‹ค์–‘ํ•œ ํ˜•ํƒœ์˜ ์ƒ˜ํ”Œ ๊ตฐ์ˆ˜ ๋ฌธ์„œ ๋Œ€์ƒ ํŒŒ์‹ฑ ํ…Œ์ŠคํŠธ\n", "\n", "\n", "#### ์ฑ„์ ์ž ํŒ์ • ๋ฉ”๋ชจ\n", "\n", "> ๋‹จ์ˆœ ์ฝ”๋“œ ์‹คํ–‰ ์—ฌ๋ถ€๋ฟ๋งŒ ์•„๋‹ˆ๋ผ ์ถœ๋ ฅ๋œ ์ˆ˜์น˜์™€ ๊ทผ๊ฑฐ ์ž๋ฃŒ๊ฐ€ ๊ตฐ ์‹ค๋ฌด ์š”๊ตฌ์‚ฌํ•ญ์— ๋ถ€ํ•ฉํ•˜๋Š”์ง€ ๋ฉด๋ฐ€ํžˆ ํ™•์ธํ•œ๋‹ค.\n", "\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "\"\"\"์ฒด๊ณ„๋ณ„๋กœ ๋‹ค๋ฅธ ํ˜•์‹(CSVยทJSONยทํƒญ ๊ตฌ๋ถ„ ์—‘์…€ ๋‚ด๋ณด๋‚ด๊ธฐ)์˜ ๊ฐ€์ƒ ์žฌ๊ณ  ๋ณด๊ณ ๋ฅผ ๋ฉ”๋ชจ๋ฆฌ ์•ˆ์—์„œ ํŒŒ์‹ฑํ•˜๊ณ , ํ’ˆ๋ชฉ์ฝ”๋“œยท๋ถ€๋Œ€์ฝ”๋“œยท๋‚ ์งœ ํ˜•์‹์„ ํ‘œ์ค€ ํ‚ค๋กœ ํ†ตํ•ฉํ•˜๋ฉฐ ๊ฒฐ์ธก๊ฐ’์„ ์ฒด๊ณ„ ๊ฐ„ ๊ต์ฐจ ๊ฐ’์œผ๋กœ ๋ณด์™„ํ•˜๋Š” ์ „์ฒ˜๋ฆฌ ํŒŒ์ดํ”„๋ผ์ธ์„ ๊ตฌ์ถ•ํ•œ๋‹ค.\"\"\"\n", "import csv\n", "import datetime\n", "import io\n", "import json\n", "import numpy as np\n", "import pandas as pd\n", "\n", "# ---------------------------------------------------------------------------\n", "# [๊ต์œก์šฉ๊ฐ€์ƒ์ž๋ฃŒ] ๊ฐ€์ƒ ์ด๊ธฐ์ข… ๊ตฐ์ˆ˜์ •๋ณด์ฒด๊ณ„ 3์ข…(A ๋ณด๊ธ‰, B ์ •๋น„, C ํƒ„์•ฝ)์˜ ์žฌ๊ณ  ๋ณด๊ณ  ์ƒ์„ฑ๊ธฐ\n", "# ์‹ค์ œ ์ฒด๊ณ„๋ช…ยท๋ถ€๋Œ€ยทํ’ˆ๋ชฉยท์ˆ˜๋Ÿ‰ ์—†์Œ. ๊ฐ™์€ (ํ’ˆ๋ชฉ, ๋ถ€๋Œ€) ์žฌ๊ณ ๋ฅผ ์„ธ ์ฒด๊ณ„๊ฐ€ ์„œ๋กœ ๋‹ค๋ฅธ ์ฝ”๋“œยท๋‚ ์งœ ํ˜•์‹์œผ๋กœ ๋ณด๊ณ ํ•œ๋‹ค.\n", "# ๊ธฐ์ค€ ๋‚ ์งœ: 2026-09-30(๊ฐ€์ƒ). ๋งˆ์Šคํ„ฐ(์ •๋‹ต) ์žฌ๊ณ  TRUE ์™€ ์ฒด๊ณ„๋ณ„ ๋ณด๊ณ ์—๋Š” ์˜๋„์  ๋ถˆ์ผ์น˜ยท๊ฒฐ์ธก์ด ์„ž์—ฌ ์žˆ๋‹ค.\n", "# ---------------------------------------------------------------------------\n", "N_PART, N_UNIT = 60, 12\n", "CATS = [\"๊ธ‰์‹\", \"ํ”ผ๋ณต\", \"์œ ๋ฅ˜\", \"์ •๋น„์†Œ๋ชจ\", \"์ˆ˜๋ฆฌ๋ถ€์†\", \"ํƒ„์•ฝ\"]\n", "PARTS = {p: {\"name\": f\"๊ฐ€์ƒํ’ˆ๋ชฉ{p:02d}\", \"cat\": CATS[(p - 1) % 6]} for p in range(1, N_PART + 1)}\n", "FMT = {\"A\": dict(part=lambda p: f\"AP-{p:05d}\", unit=lambda u: f\"U{u:02d}\", date=lambda d: d.strftime(\"%Y%m%d\")),\n", " \"B\": dict(part=lambda p: f\"{p:05d}-X\", unit=lambda u: f\"๊ฐ€์ƒ{u:02d}๋Œ€๋Œ€\", date=lambda d: d.strftime(\"%Y-%m-%d\")),\n", " \"C\": dict(part=lambda p: f\"P{p}\", unit=lambda u: str(u), date=lambda d: d.strftime(\"%d/%m/%Y\"))}\n", "BASE_DATE = datetime.date(2026, 9, 30)\n", "\n", "\n", "def make_systems(seed=9, mismatch=0.10, missing=0.03):\n", " \"\"\"TRUE(์ •๋‹ต ์žฌ๊ณ )์™€ ์ฒด๊ณ„๋ณ„ ์›๋ณธ ๋ณด๊ณ  ๋ ˆ์ฝ”๋“œ(์ฒด๊ณ„ ๊ณ ์œ  ํ˜•์‹)๋ฅผ ๋งŒ๋“ ๋‹ค.\"\"\"\n", " rng = np.random.default_rng(seed)\n", " true = {(p, u): int(rng.integers(20, 500)) for p in PARTS for u in range(1, N_UNIT + 1)}\n", " out = {s: [] for s in \"ABC\"}\n", " truth_diff = {} # (์ฒด๊ณ„, p, u) -> ๋ถˆ์ผ์น˜ ์›์ธ(์ •๋‹ต)\n", " for (p, u), q in true.items():\n", " for s in \"ABC\":\n", " qty, cause = q, None\n", " r = rng.random()\n", " if r < mismatch / 3: # ์ž…๋ ฅ ์˜ค๋ฅ˜(์ž๋ฆฟ์ˆ˜/์˜คํƒ€)\n", " qty, cause = q + int(rng.choice([-1, 1]) * rng.integers(10, 60)), \"์ž…๋ ฅ ์˜ค๋ฅ˜\"\n", " elif r < 2 * mismatch / 3: # ๋ฐ˜์˜ ์ง€์—ฐ(์ „์ผ ๊ฐ’)\n", " qty, cause = q + int(rng.integers(5, 40)), \"๋ฐ˜์˜ ์ง€์—ฐ\"\n", " elif r < mismatch: # ๋‹จ์œ„ ์˜ค๋ฅ˜(10๋ฐฐ)\n", " qty, cause = q * 10, \"๋‹จ์œ„ ์˜ค๋ฅ˜\"\n", " d = BASE_DATE - datetime.timedelta(days=int(cause == \"๋ฐ˜์˜ ์ง€์—ฐ\"))\n", " if rng.random() < missing: # ๊ฒฐ์ธก\n", " qty, cause = None, \"๊ฒฐ์ธก\"\n", " if cause:\n", " truth_diff[(s, p, u)] = cause\n", " out[s].append({\"part\": FMT[s][\"part\"](p), \"unit\": FMT[s][\"unit\"](u), \"date\": FMT[s][\"date\"](d), \"qty\": qty})\n", " return true, out, truth_diff\n", "\n", "\n", "def parse_part(code):\n", " return int(code[3:]) if code.startswith(\"AP-\") else int(code[:-2]) if code.endswith(\"-X\") else int(code[1:])\n", "\n", "\n", "def parse_unit(code):\n", " return int(code[1:]) if code.startswith(\"U\") else int(code[2:4]) if code.startswith(\"๊ฐ€์ƒ\") else int(code)\n", "\n", "\n", "def parse_date(s):\n", " if \"/\" in s:\n", " d, m, y = s.split(\"/\"); return datetime.date(int(y), int(m), int(d))\n", " if \"-\" in s:\n", " return datetime.date(*map(int, s.split(\"-\")))\n", " return datetime.date(int(s[:4]), int(s[4:6]), int(s[6:]))\n", "\n", "true, raw, tdiff = make_systems()\n", "ALIAS = {\"part\": [\"part\", \"๋ถ€ํ’ˆ์ฝ”๋“œ\", \"ํ’ˆ๋ชฉ์ฝ”๋“œ\"], \"unit\": [\"unit\", \"๋ถ€๋Œ€\", \"๋ถ€๋Œ€์ฝ”๋“œ\"], \"date\": [\"date\", \"๊ธฐ์ค€์ผ\", \"์ผ์ž\"], \"qty\": [\"qty\", \"์ˆ˜๋Ÿ‰\", \"์žฌ๊ณ \"]}\n", "H2F = {h: f for f, hs in ALIAS.items() for h in hs}\n", "\n", "# --- ๊ฐ€์ƒ ์›๋ณธ ๋ฌธ์ž์—ด 3์ข…: ์ฒด๊ณ„๋ณ„ ํ˜•์‹๊ณผ ๊ฒฐ์ธก ํ‘œ๊ธฐ๊ฐ€ ๋‹ค๋ฅด๋‹ค ---\n", "txt_a = \"\\n\".join([\"part,unit,date,qty\"] + [f\"{r['part']},{r['unit']},{r['date']},{'' if r['qty'] is None else r['qty']}\" for r in raw[\"A\"]])\n", "txt_b = json.dumps({\"system\": \"B\", \"items\": [{\"๋ถ€ํ’ˆ์ฝ”๋“œ\": r[\"part\"], \"๋ถ€๋Œ€\": r[\"unit\"], \"๊ธฐ์ค€์ผ\": r[\"date\"], \"์ˆ˜๋Ÿ‰\": r[\"qty\"]} for r in raw[\"B\"]]}, ensure_ascii=False)\n", "txt_c = \"\\n\".join([\"ํ’ˆ๋ชฉ์ฝ”๋“œ\\t๋ถ€๋Œ€์ฝ”๋“œ\\t์ผ์ž\\t์žฌ๊ณ \"] + [f\"{r['part']}\\t{r['unit']}\\t{r['date']}\\t{-1 if r['qty'] is None else r['qty']}\" for r in raw[\"C\"]])\n", "TEXT = {\"A\": txt_a, \"B\": txt_b, \"C\": txt_c}\n", "# ํŒŒ์‹ฑ ์‹คํŒจ ์œ ๋ฐœ ์ค„ ์ถ”๊ฐ€(๋ณด๋ฅ˜ ๋Œ€์ƒ)\n", "TEXT[\"A\"] += \"\\nAP-9999x,U01,20260930,12\\nAP-00001,U99,20260930,12\"\n", "\n", "def norm(row, system):\n", " r = {H2F[k.strip()]: v for k, v in row.items() if k.strip() in H2F}\n", " q = r[\"qty\"]\n", " if system == \"A\":\n", " q = None if q in (\"\", None) else int(q)\n", " elif system == \"B\":\n", " q = None if q is None else int(q)\n", " else:\n", " q = None if int(q) == -1 else int(q)\n", " return {\"sys\": system, \"p\": parse_part(r[\"part\"]), \"u\": parse_unit(r[\"unit\"]), \"date\": parse_date(r[\"date\"]), \"qty\": q}\n", "\n", "READ = {\"A\": lambda s: list(csv.DictReader(io.StringIO(s))), \"B\": lambda s: json.loads(s)[\"items\"],\n", " \"C\": lambda s: list(csv.DictReader(io.StringIO(s), delimiter=\"\\t\"))}\n", "ok_fmt, recs, hold = {}, [], []\n", "for sysname, text in TEXT.items():\n", " try:\n", " rows = READ[sysname](text); ok_fmt[sysname] = True\n", " except Exception:\n", " rows = []; ok_fmt[sysname] = False\n", " for r in rows:\n", " try:\n", " n = norm(r, sysname)\n", " if not (1 <= n[\"p\"] <= N_PART and 1 <= n[\"u\"] <= N_UNIT):\n", " raise ValueError(\"ํ‚ค ๋ฒ”์œ„ ๋ฐ–\")\n", " recs.append(n)\n", " except Exception as ex:\n", " hold.append((sysname, str(r)[:60], str(ex)[:20]))\n", "df = pd.DataFrame(recs)\n", "print(f\"[ํ˜•์‹๋ณ„ ํŒŒ์‹ฑ] A(CSV) {'์„ฑ๊ณต' if ok_fmt['A'] else '์‹คํŒจ'}, B(JSON) {'์„ฑ๊ณต' if ok_fmt['B'] else '์‹คํŒจ'}, C(ํƒญ ๊ตฌ๋ถ„) {'์„ฑ๊ณต' if ok_fmt['C'] else '์‹คํŒจ'}\")\n", "print(f\"ํ‘œ์ค€ํ™” {len(df)}๊ฑด(์ฒด๊ณ„๋ณ„ {df['sys'].value_counts().to_dict()}), ๋ฐ˜์˜ ๋ณด๋ฅ˜ {len(hold)}๊ฑด\")\n", "for h in hold:\n", " print(\" ๋ณด๋ฅ˜:\", h)\n", "rate = sum(ok_fmt.values()) / len(ok_fmt)\n", "\n", "# --- ํ‘œ์ค€ ํ‚ค ํ†ตํ•ฉ: (ํ’ˆ๋ชฉ, ๋ถ€๋Œ€) ๊ธฐ์ค€์œผ๋กœ ์„ธ ์ฒด๊ณ„๋ฅผ ํ•œ ํ–‰์— ---\n", "wide = df.pivot_table(index=[\"p\", \"u\"], columns=\"sys\", values=\"qty\", aggfunc=\"first\")\n", "print(f\"\\nํ†ตํ•ฉ ํ‚ค {len(wide)}๊ฐœ(๊ธฐ๋Œ€ {N_PART * N_UNIT}), ๊ฒฐ์ธก ๊ฐ’ {int(wide.isna().sum().sum())}๊ฑด (A {wide['A'].isna().sum()}, B {wide['B'].isna().sum()}, C {wide['C'].isna().sum()})\")\n", "# --- ๊ฒฐ์ธก ์ „์ฒ˜๋ฆฌ: ๋‹ค๋ฅธ ๋‘ ์ฒด๊ณ„ ๊ฐ’ ์ค‘ ์ž‘์€ ๊ฐ’(๋‹จ์œ„ 10๋ฐฐ ์˜ค๋ฅ˜ ๋ฐฉ์ง€)์„ ํ›„๋ณด๋กœ ๋ณด์™„ํ•˜๊ณ  ํ‘œ์‹œ, ๋‘˜ ๋‹ค ์—†์œผ๋ฉด ๋ฏธํ•ด๊ฒฐ ---\n", "filled = wide.copy(); flag = pd.DataFrame(\"\", index=wide.index, columns=wide.columns)\n", "for s in \"ABC\":\n", " others = [c for c in \"ABC\" if c != s]\n", " m = wide[s].isna()\n", " vals = wide.loc[m, others]\n", " fill = vals.min(axis=1)\n", " filled.loc[m, s] = fill\n", " flag.loc[m & fill.notna(), s] = \"๋ณด์™„\"\n", " flag.loc[m & fill.isna(), s] = \"๋ฏธํ•ด๊ฒฐ\"\n", "n_fill = int((flag == \"๋ณด์™„\").sum().sum()); n_un = int((flag == \"๋ฏธํ•ด๊ฒฐ\").sum().sum())\n", "print(f\"๊ฒฐ์ธก ๋ณด์™„ {n_fill}๊ฑด(๋‹ค๋ฅธ ์ฒด๊ณ„ ๊ฐ’ ๊ธฐ๋ฐ˜ ํ›„๋ณด๊ฐ’, ํ™•์ • ์•„๋‹˜), ๋ฏธํ•ด๊ฒฐ {n_un}๊ฑด\")\n", "t = true\n", "exact = np.mean([filled.loc[(p, u), s] == t[(p, u)] for (p, u) in list(wide.index)[:720] for s in \"ABC\" if flag.loc[(p, u), s] == \"๋ณด์™„\"])\n", "print(f\"๋ณด์™„๊ฐ’์ด ๋งˆ์Šคํ„ฐ์™€ ๊ฐ™์€ ๋น„์œจ(๊ฐ€์ƒ ์ •๋‹ต ๋Œ€์กฐ) {exact:.0%} -> ๋ณด์™„๊ฐ’์€ 'ํ›„๋ณด'๋กœ ํ‘œ์‹œํ•˜๊ณ  ๋งˆ์Šคํ„ฐ ํ™•์ •์€ ์‚ฌ๋žŒ์ด ํ•œ๋‹ค\")\n", "print(\"\\nํ‘œ์ค€ ํ†ตํ•ฉํ‘œ ๋ฏธ๋ฆฌ๋ณด๊ธฐ(3ํ–‰):\")\n", "print(filled.head(3).astype(\"float\").to_string())\n", "print(f\"\\n๋ชฉํ‘œ(3๊ฐœ ํ˜•์‹ ํŒŒ์‹ฑ ์„ฑ๊ณต๋ฅ  100%, ์–‘์‹ ๋ณ€๊ฒฝ ๋Œ€์‘, ๊ฒฐ์ธก ์ „์ฒ˜๋ฆฌ): {'๋‹ฌ์„ฑ' if rate == 1 and len(wide) == 720 and len(hold) == 2 else '๋ฏธ๋‹ฌ'}\")\n", "print(\"์ƒˆ ๋จธ๋ฆฌ๊ธ€ ๋Œ€์‘์€ ALIAS ์‚ฌ์ „์— ๋ณ„์นญ๋งŒ ์ถ”๊ฐ€ํ•˜๋ฉด ๋œ๋‹ค(์ฝ”๋“œ ์ˆ˜์ • ๋ถˆํ•„์š”).\")\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "
\n", "๐Ÿ’ก [์ฐธ๊ณ ] ๋ฏธ์…˜ 02 ์˜ˆ์ƒ ์‹คํ–‰ ๊ฒฐ๊ณผ (Output)\n", "\n", "```text\n", "[ํ˜•์‹๋ณ„ ํŒŒ์‹ฑ] A(CSV) ์„ฑ๊ณต, B(JSON) ์„ฑ๊ณต, C(ํƒญ ๊ตฌ๋ถ„) ์„ฑ๊ณต\n", "ํ‘œ์ค€ํ™” 2160๊ฑด(์ฒด๊ณ„๋ณ„ {'A': 720, 'B': 720, 'C': 720}), ๋ฐ˜์˜ ๋ณด๋ฅ˜ 2๊ฑด\n", " ๋ณด๋ฅ˜: ('A', \"{'part': 'AP-9999x', 'unit': 'U01', 'date': '20260930', 'qty\", 'invalid literal for ')\n", " ๋ณด๋ฅ˜: ('A', \"{'part': 'AP-00001', 'unit': 'U99', 'date': '20260930', 'qty\", 'ํ‚ค ๋ฒ”์œ„ ๋ฐ–')\n", "\n", "ํ†ตํ•ฉ ํ‚ค 720๊ฐœ(๊ธฐ๋Œ€ 720), ๊ฒฐ์ธก ๊ฐ’ 46๊ฑด (A 18, B 14, C 14)\n", "๊ฒฐ์ธก ๋ณด์™„ 46๊ฑด(๋‹ค๋ฅธ ์ฒด๊ณ„ ๊ฐ’ ๊ธฐ๋ฐ˜ ํ›„๋ณด๊ฐ’, ํ™•์ • ์•„๋‹˜), ๋ฏธํ•ด๊ฒฐ 0๊ฑด\n", "๋ณด์™„๊ฐ’์ด ๋งˆ์Šคํ„ฐ์™€ ๊ฐ™์€ ๋น„์œจ(๊ฐ€์ƒ ์ •๋‹ต ๋Œ€์กฐ) 100% -> ๋ณด์™„๊ฐ’์€ 'ํ›„๋ณด'๋กœ ํ‘œ์‹œํ•˜๊ณ  ๋งˆ์Šคํ„ฐ ํ™•์ •์€ ์‚ฌ๋žŒ์ด ํ•œ๋‹ค\n", "\n", "ํ‘œ์ค€ ํ†ตํ•ฉํ‘œ ๋ฏธ๋ฆฌ๋ณด๊ธฐ(3ํ–‰):\n", "sys A B C\n", "p u \n", "1 1 222.0 222.0 222.0\n", " 2 437.0 437.0 437.0\n", " 3 444.0 481.0 481.0\n", "\n", "๋ชฉํ‘œ(3๊ฐœ ํ˜•์‹ ํŒŒ์‹ฑ ์„ฑ๊ณต๋ฅ  100%, ์–‘์‹ ๋ณ€๊ฒฝ ๋Œ€์‘, ๊ฒฐ์ธก ์ „์ฒ˜๋ฆฌ): ๋‹ฌ์„ฑ\n", "์ƒˆ ๋จธ๋ฆฌ๊ธ€ ๋Œ€์‘์€ ALIAS ์‚ฌ์ „์— ๋ณ„์นญ๋งŒ ์ถ”๊ฐ€ํ•˜๋ฉด ๋œ๋‹ค(์ฝ”๋“œ ์ˆ˜์ • ๋ถˆํ•„์š”).\n", "```\n", "
\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "---\n", "\n", "## ๐ŸŽฏ ๋ฏธ์…˜ 03. ํ†ตํ•ฉ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค 1์ฐจ ๊ตฌ์ถ• ๋ฐ ํ•ต์‹ฌ ๊ตฐ์ˆ˜ ์ง€ํ‘œ ์ง‘๊ณ„ Baseline ์ฟผ๋ฆฌ ๋ชจ๋“ˆ ๊ตฌํ˜„\n", "**์†Œ์š”์‹œ๊ฐ„**: ์•ฝ 2.5์‹œ๊ฐ„\n", "\n", "### ๐Ÿ“‹ ๋ฏธ์…˜ ๋ฐฐ๊ฒฝ ๋ฐ ์ˆ˜ํ–‰ ์ง€์นจ\n", "#### ๋ฏธ์…˜ ๋ฐฐ๊ฒฝ ๋ฐ ๋‹น์œ„์„ฑ\n", "\n", "ํ†ตํ•ฉ DB ๊ตฌ์ถ• ๋ฐ ํ•ต์‹ฌ ๊ตฐ์ˆ˜ ์ง€ํ‘œ ์ง‘๊ณ„ Baseline ์ฟผ๋ฆฌ ๋ชจ๋“ˆ์„ ๊ตฌํ˜„ํ•œ๋‹ค. ์‹ค์ œ ์ „์ˆ  ํ˜„์žฅ์—์„œ๋Š” ๋‹จ์ˆœ ํƒ์ง€์— ๊ทธ์น˜์ง€ ์•Š๊ณ  ์‹ ์†ํ•œ ์œ„ํ˜‘ ํŒ๋‹จ๊ณผ ์‹ ๋ขฐ๋„ ๋†’์€ ์˜์‚ฌ๊ฒฐ์ •์ด ํ•„์ˆ˜์ ์ด๋ฏ€๋กœ, **์„ธ ์ฒด๊ณ„์˜ ํ‘œ์ค€ํ™” ์žฌ๊ณ ๋ฅผ ํ†ตํ•ฉ ๋ฐ์ดํ„ฐ ์ €์žฅ์†Œ์— ๊ตฌ์ถ•ํ•˜๊ณ , ๊ฒ€์ฆ ๋ฃฐ 15์ข…๊ณผ ํ•ต์‹ฌ ๊ตฐ์ˆ˜ ์ง€ํ‘œ ์ง‘๊ณ„ Baseline ์ฟผ๋ฆฌ๋ฅผ ๊ตฌํ˜„ํ•œ๋‹ค.** ๊ณผ์—…์„ ์™„์ˆ˜ํ•˜์—ฌ ์˜คํŒ์„ ๋ฐฉ์ง€ํ•˜๊ณ  ์ž‘์ „ ๋Œ€์‘๋ ฅ์„ ๊ทน๋Œ€ํ™”ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.\n", "\n", "์ด ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด ๋ณธ ๋ฏธ์…˜์—์„œ๋Š” **ํ‘œ์ค€ํ™”ํ•œ 2,160๊ฑด์„ ํ•˜๋‚˜์˜ ํ‘œ๋กœ ์Œ“์•„ ํ†ตํ•ฉ ์ €์žฅ์†Œ๋กœ ์‚ผ๊ณ , ๋ฃฐ 15์ข…์„ ๊ฐ€์ƒ ์กฐํ•ญ ๋ฒˆํ˜ธ๊ฐ€ ๋ถ™์€ ์„ค์ • ๋ฌธ์ž์—ด๋กœ ๋ถ„๋ฆฌํ•ด ์œ„๋ฐ˜ ๊ฑด์ˆ˜์™€ ์ฒ˜๋ฆฌ ์ง€์—ฐ์‹œ๊ฐ„์„ ์ธก์ • ์ˆ˜ํ–‰ํ•˜๊ณ  ๊ฒ€์ฆํ•˜๋Š”** ์‹ค์Šต์„ ์ฒด๊ณ„์ ์œผ๋กœ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค.\n", "\n", "ํ•™์Šต์ž๋Š” ์‹ค์Šต๊ฐ€์ด๋“œ์— ์ œ์‹œ๋œ ์ด 4๊ฐœ ์„ธ๋ถ€ ํ™œ๋™(๊ธฐ๋ณธ ๊ธฐ๋Šฅ ๊ตฌํ˜„ โž” ๊ฒฐ๊ณผ ์ง€ํ‘œ ๊ฒ€์ฆ โž” ํŒŒ๋ผ๋ฏธํ„ฐ ํŠœ๋‹ ๋ฐ ๋„์ „๊ณผ์ œ)์„ ์ˆœ์ฐจ์ ์œผ๋กœ ์ง„ํ–‰ํ•˜๋ฉฐ ํ•ต์‹ฌ ํŒ์ • ๊ธฐ์ค€์„ ๋‹ฌ์„ฑํ•ฉ๋‹ˆ๋‹ค.\n", "\n", "**์ด๋ฒˆ ๊ฒฐ๊ณผ๋ฌผ:** `src/engine/dat09_rule_engine.py` ํ˜•ํƒœ์˜ ๊ฒฐ๊ณผ๋ฌผ์„ ์™„์„ฑํ•œ๋‹ค.\n", "\n", "### ๐Ÿ” ํ‰๊ฐ€ ๋ฐ ๊ฒ€์ฆ ๊ธฐ์ค€ (PASS ์กฐ๊ฑด)\n", "#### ๋ฏธ์…˜ ํŒ์ •\n", "\n", "**ํ‰๊ฐ€ ๊ตฌ๋ถ„:** ํ•„์ˆ˜\n", "\n", "**ํ•ต์‹ฌ ํ‰๊ฐ€์˜์—ญ:** ๊ตฐ ๊ทœ์ • ๋ฐ ์ง€์นจ ์กฐํ•ญ๊ณผ์˜ ์ •ํ™•ํ•œ ์ผ์น˜์„ฑ\n", "\n", "#### ๋ฏธ์…˜ PASS ์กฐ๊ฑด\n", "\n", "- ์ •๋Ÿ‰ ๊ธฐ์ค€: ์—…๋ฌด ๊ฒ€์ฆ ๋ฃฐ 15์ข… ์ด์ƒ ์ฝ”๋”ฉ, ๋ฃฐ ์ฒ˜๋ฆฌ ์ง€์—ฐ์‹œ๊ฐ„ 50ms ์ดํ•˜\n", "\n", "\n", "- ์ง„ํ–‰ ๊ธฐ์ค€: ๋ฃฐ ์—”์ง„ ๋‹จ์œ„ ํ…Œ์ŠคํŠธ 100% ํ†ต๊ณผ\n", "\n", "\n", "- ๊ฒ€์ฆ ๋ฐฉ๋ฒ•: ๊ฒฝ๊ณ„ ์กฐ๊ฑด(Boundary condition)์— ๋Œ€ํ•œ ๋ฃฐ ํŒ์ • ๊ฒ€์ฆ\n", "\n", "\n", "#### ์ฑ„์ ์ž ํŒ์ • ๋ฉ”๋ชจ\n", "\n", "> ๋‹จ์ˆœ ์ฝ”๋“œ ์‹คํ–‰ ์—ฌ๋ถ€๋ฟ๋งŒ ์•„๋‹ˆ๋ผ ์ถœ๋ ฅ๋œ ์ˆ˜์น˜์™€ ๊ทผ๊ฑฐ ์ž๋ฃŒ๊ฐ€ ๊ตฐ ์‹ค๋ฌด ์š”๊ตฌ์‚ฌํ•ญ์— ๋ถ€ํ•ฉํ•˜๋Š”์ง€ ๋ฉด๋ฐ€ํžˆ ํ™•์ธํ•œ๋‹ค.\n", "\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "\"\"\"์„ธ ์ฒด๊ณ„์˜ ํ‘œ์ค€ํ™” ์žฌ๊ณ ๋ฅผ ํ•˜๋‚˜์˜ ํ†ตํ•ฉ ๋ฐ์ดํ„ฐ ์ €์žฅ์†Œ(ํ‘œ)๋กœ ๊ตฌ์ถ•ํ•˜๊ณ , ์„ค์ • ๋ฌธ์ž์—ด๋กœ ๋ถ„๋ฆฌํ•œ ๊ฒ€์ฆ ๋ฃฐ 15์ข…๊ณผ ํ•ต์‹ฌ ๊ตฐ์ˆ˜ ์ง€ํ‘œ(์žฌ๊ณ  ํ•ฉ๊ณ„ยท๋ถ€์กฑ ํ’ˆ๋ชฉยท์ฒด๊ณ„ ๊ฐ„ ์ผ์น˜์œจ) ์ง‘๊ณ„ Baseline ์ฟผ๋ฆฌ๋ฅผ ๊ตฌํ˜„ํ•ด ์ฒ˜๋ฆฌ ์ง€์—ฐ์‹œ๊ฐ„๊ณผ ๊ฒฝ๊ณ„ ์กฐ๊ฑด ํ…Œ์ŠคํŠธ๋ฅผ ํ™•์ธํ•œ๋‹ค.\"\"\"\n", "import datetime\n", "import time\n", "import numpy as np\n", "import pandas as pd\n", "\n", "# ---------------------------------------------------------------------------\n", "# [๊ต์œก์šฉ๊ฐ€์ƒ์ž๋ฃŒ] ๊ฐ€์ƒ ์ด๊ธฐ์ข… ๊ตฐ์ˆ˜์ •๋ณด์ฒด๊ณ„ 3์ข…(A ๋ณด๊ธ‰, B ์ •๋น„, C ํƒ„์•ฝ)์˜ ์žฌ๊ณ  ๋ณด๊ณ  ์ƒ์„ฑ๊ธฐ\n", "# ์‹ค์ œ ์ฒด๊ณ„๋ช…ยท๋ถ€๋Œ€ยทํ’ˆ๋ชฉยท์ˆ˜๋Ÿ‰ ์—†์Œ. ๊ฐ™์€ (ํ’ˆ๋ชฉ, ๋ถ€๋Œ€) ์žฌ๊ณ ๋ฅผ ์„ธ ์ฒด๊ณ„๊ฐ€ ์„œ๋กœ ๋‹ค๋ฅธ ์ฝ”๋“œยท๋‚ ์งœ ํ˜•์‹์œผ๋กœ ๋ณด๊ณ ํ•œ๋‹ค.\n", "# ๊ธฐ์ค€ ๋‚ ์งœ: 2026-09-30(๊ฐ€์ƒ). ๋งˆ์Šคํ„ฐ(์ •๋‹ต) ์žฌ๊ณ  TRUE ์™€ ์ฒด๊ณ„๋ณ„ ๋ณด๊ณ ์—๋Š” ์˜๋„์  ๋ถˆ์ผ์น˜ยท๊ฒฐ์ธก์ด ์„ž์—ฌ ์žˆ๋‹ค.\n", "# ---------------------------------------------------------------------------\n", "N_PART, N_UNIT = 60, 12\n", "CATS = [\"๊ธ‰์‹\", \"ํ”ผ๋ณต\", \"์œ ๋ฅ˜\", \"์ •๋น„์†Œ๋ชจ\", \"์ˆ˜๋ฆฌ๋ถ€์†\", \"ํƒ„์•ฝ\"]\n", "PARTS = {p: {\"name\": f\"๊ฐ€์ƒํ’ˆ๋ชฉ{p:02d}\", \"cat\": CATS[(p - 1) % 6]} for p in range(1, N_PART + 1)}\n", "FMT = {\"A\": dict(part=lambda p: f\"AP-{p:05d}\", unit=lambda u: f\"U{u:02d}\", date=lambda d: d.strftime(\"%Y%m%d\")),\n", " \"B\": dict(part=lambda p: f\"{p:05d}-X\", unit=lambda u: f\"๊ฐ€์ƒ{u:02d}๋Œ€๋Œ€\", date=lambda d: d.strftime(\"%Y-%m-%d\")),\n", " \"C\": dict(part=lambda p: f\"P{p}\", unit=lambda u: str(u), date=lambda d: d.strftime(\"%d/%m/%Y\"))}\n", "BASE_DATE = datetime.date(2026, 9, 30)\n", "\n", "\n", "def make_systems(seed=9, mismatch=0.10, missing=0.03):\n", " \"\"\"TRUE(์ •๋‹ต ์žฌ๊ณ )์™€ ์ฒด๊ณ„๋ณ„ ์›๋ณธ ๋ณด๊ณ  ๋ ˆ์ฝ”๋“œ(์ฒด๊ณ„ ๊ณ ์œ  ํ˜•์‹)๋ฅผ ๋งŒ๋“ ๋‹ค.\"\"\"\n", " rng = np.random.default_rng(seed)\n", " true = {(p, u): int(rng.integers(20, 500)) for p in PARTS for u in range(1, N_UNIT + 1)}\n", " out = {s: [] for s in \"ABC\"}\n", " truth_diff = {} # (์ฒด๊ณ„, p, u) -> ๋ถˆ์ผ์น˜ ์›์ธ(์ •๋‹ต)\n", " for (p, u), q in true.items():\n", " for s in \"ABC\":\n", " qty, cause = q, None\n", " r = rng.random()\n", " if r < mismatch / 3: # ์ž…๋ ฅ ์˜ค๋ฅ˜(์ž๋ฆฟ์ˆ˜/์˜คํƒ€)\n", " qty, cause = q + int(rng.choice([-1, 1]) * rng.integers(10, 60)), \"์ž…๋ ฅ ์˜ค๋ฅ˜\"\n", " elif r < 2 * mismatch / 3: # ๋ฐ˜์˜ ์ง€์—ฐ(์ „์ผ ๊ฐ’)\n", " qty, cause = q + int(rng.integers(5, 40)), \"๋ฐ˜์˜ ์ง€์—ฐ\"\n", " elif r < mismatch: # ๋‹จ์œ„ ์˜ค๋ฅ˜(10๋ฐฐ)\n", " qty, cause = q * 10, \"๋‹จ์œ„ ์˜ค๋ฅ˜\"\n", " d = BASE_DATE - datetime.timedelta(days=int(cause == \"๋ฐ˜์˜ ์ง€์—ฐ\"))\n", " if rng.random() < missing: # ๊ฒฐ์ธก\n", " qty, cause = None, \"๊ฒฐ์ธก\"\n", " if cause:\n", " truth_diff[(s, p, u)] = cause\n", " out[s].append({\"part\": FMT[s][\"part\"](p), \"unit\": FMT[s][\"unit\"](u), \"date\": FMT[s][\"date\"](d), \"qty\": qty})\n", " return true, out, truth_diff\n", "\n", "\n", "def parse_part(code):\n", " return int(code[3:]) if code.startswith(\"AP-\") else int(code[:-2]) if code.endswith(\"-X\") else int(code[1:])\n", "\n", "\n", "def parse_unit(code):\n", " return int(code[1:]) if code.startswith(\"U\") else int(code[2:4]) if code.startswith(\"๊ฐ€์ƒ\") else int(code)\n", "\n", "\n", "def parse_date(s):\n", " if \"/\" in s:\n", " d, m, y = s.split(\"/\"); return datetime.date(int(y), int(m), int(d))\n", " if \"-\" in s:\n", " return datetime.date(*map(int, s.split(\"-\")))\n", " return datetime.date(int(s[:4]), int(s[4:6]), int(s[6:]))\n", "\n", "true, raw, tdiff = make_systems()\n", "rows = []\n", "for s in \"ABC\":\n", " for r in raw[s]:\n", " rows.append({\"sys\": s, \"p\": parse_part(r[\"part\"]), \"u\": parse_unit(r[\"unit\"]), \"date\": parse_date(r[\"date\"]), \"qty\": r[\"qty\"]})\n", "store = pd.DataFrame(rows) # ํ†ตํ•ฉ ๋ฐ์ดํ„ฐ ์ €์žฅ์†Œ(์ฒด๊ณ„ 3๊ฐœ x 720ํ‚ค)\n", "store[\"cat\"] = store[\"p\"].map(lambda p: PARTS[p][\"cat\"])\n", "store[\"qty_f\"] = store[\"qty\"].astype(\"float\")\n", "\n", "# 1) ๊ฒ€์ฆ ๋ฃฐ 15์ข…(์„ค์ •): (ID, ๊ฐ€์ƒ ์กฐํ•ญ, ์กฐ๊ฑด์‹(์ฐธ์ด๋ฉด ์ •์ƒ), ๋ฉ”์‹œ์ง€)\n", "RULES = [(\"D01\", \"๊ฐ€์ƒ์ง€์นจ 1์กฐ\", \"1 <= p <= 60\", \"ํ’ˆ๋ชฉ ํ‚ค ๋ฒ”์œ„ ๋ฐ–\"), (\"D02\", \"๊ฐ€์ƒ์ง€์นจ 1์กฐ\", \"1 <= u <= 12\", \"๋ถ€๋Œ€ ํ‚ค ๋ฒ”์œ„ ๋ฐ–\"),\n", " (\"D03\", \"๊ฐ€์ƒ์ง€์นจ 2์กฐ\", \"sys in ('A', 'B', 'C')\", \"์•Œ ์ˆ˜ ์—†๋Š” ์ฒด๊ณ„\"), (\"D04\", \"๊ฐ€์ƒ์ง€์นจ 3์กฐ\", \"qty is not None\", \"์ˆ˜๋Ÿ‰ ๊ฒฐ์ธก\"),\n", " (\"D05\", \"๊ฐ€์ƒ์ง€์นจ 3์กฐ\", \"qty is None or qty >= 0\", \"์ˆ˜๋Ÿ‰ ์Œ์ˆ˜\"), (\"D06\", \"๊ฐ€์ƒ์ง€์นจ 3์กฐ\", \"qty is None or qty <= 5000\", \"์ˆ˜๋Ÿ‰ ์ƒํ•œ ์ดˆ๊ณผ\"),\n", " (\"D07\", \"๊ฐ€์ƒ์ง€์นจ 4์กฐ\", \"isinstance(p, int) and isinstance(u, int)\", \"ํ‚ค ์ž๋ฃŒํ˜• ์˜ค๋ฅ˜\"), (\"D08\", \"๊ฐ€์ƒ์ง€์นจ 4์กฐ\", \"date <= TODAY\", \"๋ฏธ๋ž˜ ๋‚ ์งœ\"),\n", " (\"D09\", \"๊ฐ€์ƒ์ง€์นจ 5์กฐ\", \"(TODAY - date).days <= 3\", \"3์ผ ์ดˆ๊ณผ ์ง€์—ฐ ๋ณด๊ณ \"), (\"D10\", \"๊ฐ€์ƒ์ง€์นจ 5์กฐ\", \"(TODAY - date).days >= 0\", \"๊ธฐ์ค€์ผ ์ดํ›„ ๋‚ ์งœ\"),\n", " (\"D11\", \"๊ฐ€์ƒ์ง€์นจ 6์กฐ\", \"cat in CATS\", \"๋ถ„๋ฅ˜ ์ฝ”๋“œ ์˜ค๋ฅ˜\"), (\"D12\", \"๊ฐ€์ƒ์ง€์นจ 6์กฐ\", \"qty is None or qty % 1 == 0\", \"์ˆ˜๋Ÿ‰ ์†Œ์ˆ˜ ๋ถˆ๊ฐ€\"),\n", " (\"D13\", \"๊ฐ€์ƒ์ง€์นจ 7์กฐ\", \"not (sys == 'C' and cat != 'ํƒ„์•ฝ' and qty is not None and qty > 3000)\", \"ํƒ„์•ฝ์ฒด๊ณ„ ๋น„ํƒ„์•ฝ ๋Œ€๋Ÿ‰ ์ˆ˜๋Ÿ‰\"),\n", " (\"D14\", \"๊ฐ€์ƒ์ง€์นจ 7์กฐ\", \"qty is None or qty < 2000 or qty % 10 != 0\", \"10๋ฐฐ ๋‹จ์œ„ ์˜ค๋ฅ˜ ์˜์‹ฌ(2000 ์ด์ƒ 10์˜ ๋ฐฐ์ˆ˜)\"),\n", " (\"D15\", \"๊ฐ€์ƒ์ง€์นจ 8์กฐ\", \"u != 0\", \"๋ถ€๋Œ€ 0 ์‚ฌ์šฉ ๋ถˆ๊ฐ€\")]\n", "TODAY = BASE_DATE\n", "CODE = [(i, a, compile(e, i, \"eval\"), m) for i, a, e, m in RULES]\n", "SAFE = {\"__builtins__\": {\"isinstance\": isinstance, \"int\": int}}\n", "\n", "\n", "def check(rec):\n", " env = {\"CATS\": CATS, \"TODAY\": TODAY, **rec}\n", " bad = []\n", " for i, a, c, m in CODE:\n", " try:\n", " ok = eval(c, SAFE, env)\n", " except Exception:\n", " ok = False\n", " if not ok:\n", " bad.append((i, m))\n", " return bad\n", "\n", "\n", "recs = store[[\"sys\", \"p\", \"u\", \"date\", \"qty\", \"cat\"]].astype(object).where(store[[\"sys\", \"p\", \"u\", \"date\", \"qty\", \"cat\"]].notna(), None).to_dict(\"records\")\n", "lat, viol = [], {}\n", "for r in recs:\n", " t0 = time.perf_counter(); res = check(r); lat.append((time.perf_counter() - t0) * 1000)\n", " for i, m in res:\n", " viol[i] = viol.get(i, 0) + 1\n", "lat = np.array(lat)\n", "print(f\"[๊ฒ€์ฆ ๋ฃฐ {len(RULES)}์ข…] ๋Œ€์ƒ {len(recs)}๊ฑด, ๋ฃฐ ์œ„๋ฐ˜ {sum(viol.values())}๊ฑด [๊ต์œก์šฉ๊ฐ€์ƒ์ž๋ฃŒ]\")\n", "print(\"๋ฃฐ๋ณ„ ์œ„๋ฐ˜:\", dict(sorted(viol.items())))\n", "print(f\"๋ฃฐ ์ฒ˜๋ฆฌ ์ง€์—ฐ ํ‰๊ท  {lat.mean():.3f}ms, ์ตœ๋Œ€ {lat.max():.3f}ms (๋ชฉํ‘œ 50ms ์ดํ•˜: {'๋‹ฌ์„ฑ' if lat.max() <= 50 else '๋ฏธ๋‹ฌ'}, ํ™˜๊ฒฝ์— ๋”ฐ๋ผ ๋‹ค๋ฆ„)\")\n", "\n", "# 2) Baseline ์ง€ํ‘œ ์ฟผ๋ฆฌ: ํ•จ์ˆ˜๋กœ ๋ถ„๋ฆฌ(์ฒด๊ณ„ A๋ฅผ ๊ธฐ์ค€ ๊ฐ’์œผ๋กœ)\n", "def q_stock_by_cat(df, sysname=\"A\"):\n", " return df[df[\"sys\"] == sysname].groupby(\"cat\")[\"qty_f\"].sum()\n", "\n", "\n", "def q_low_stock(df, thr=60, sysname=\"A\"):\n", " d = df[(df[\"sys\"] == sysname) & (df[\"qty_f\"] < thr)]\n", " return d.groupby(\"p\")[\"u\"].count().sort_values(ascending=False)\n", "\n", "\n", "def q_agreement(df):\n", " w = df.pivot_table(index=[\"p\", \"u\"], columns=\"sys\", values=\"qty_f\")\n", " ok = w.notna().all(axis=1)\n", " return float((w[ok].nunique(axis=1) == 1).mean()), int(ok.sum())\n", "\n", "\n", "t0 = time.perf_counter()\n", "cat_sum, low, (agree, n_cmp) = q_stock_by_cat(store), q_low_stock(store), q_agreement(store)\n", "print(\"\\n[Baseline ์ง€ํ‘œ ์ฟผ๋ฆฌ] ์ฟผ๋ฆฌ 3๊ฐœ ์‹คํ–‰\", f\"{(time.perf_counter() - t0) * 1000:.1f}ms\")\n", "print(\"๋ถ„๋ฅ˜๋ณ„ ์žฌ๊ณ  ํ•ฉ๊ณ„(์ฒด๊ณ„ A ๊ธฐ์ค€):\", cat_sum.astype(int).to_dict())\n", "print(\"์žฌ๊ณ  60 ๋ฏธ๋งŒ ๋ถ€๋Œ€๊ฐ€ ๋งŽ์€ ํ’ˆ๋ชฉ ์ƒ์œ„ 3:\", low.head(3).to_dict())\n", "print(f\"์„ธ ์ฒด๊ณ„ ๋ชจ๋‘ ๋ณด๊ณ ํ•œ {n_cmp}๊ฐœ ํ‚ค ์ค‘ ์ˆ˜๋Ÿ‰์ด ์™„์ „ํžˆ ๊ฐ™์€ ๋น„์œจ(์ฒด๊ณ„ ๊ฐ„ ์ผ์น˜์œจ) {agree:.1%} -> ์ดํ›„ ๋ฏธ์…˜์—์„œ ๋ถˆ์ผ์น˜ ์›์ธ์„ ํ•ด๊ฒฐ\")\n", "\n", "# 3) ๊ฒฝ๊ณ„ ์กฐ๊ฑด ๋‹จ์œ„ ํ…Œ์ŠคํŠธ\n", "base = {\"sys\": \"A\", \"p\": 1, \"u\": 1, \"date\": TODAY, \"qty\": 100, \"cat\": \"๊ธ‰์‹\"}\n", "tests = [(\"์ •์ƒ\", base, []), (\"์ˆ˜๋Ÿ‰ 0 ํ—ˆ์šฉ\", {**base, \"qty\": 0}, []), (\"์ˆ˜๋Ÿ‰ ์ƒํ•œ 5000 ๊ฒฝ๊ณ„(D06 ํ†ต๊ณผ, D14 ์˜์‹ฌ)\", {**base, \"qty\": 5000}, [\"D14\"]),\n", " (\"์ˆ˜๋Ÿ‰ 5001\", {**base, \"qty\": 5001}, [\"D06\"]), (\"์ง€์—ฐ 3์ผ ํ—ˆ์šฉ\", {**base, \"date\": TODAY - datetime.timedelta(days=3)}, []),\n", " (\"์ง€์—ฐ 4์ผ\", {**base, \"date\": TODAY - datetime.timedelta(days=4)}, [\"D09\"]), (\"๋ฏธ๋ž˜ ๋‚ ์งœ\", {**base, \"date\": TODAY + datetime.timedelta(days=1)}, [\"D08\", \"D10\"]),\n", " (\"ํ’ˆ๋ชฉ ํ‚ค 61\", {**base, \"p\": 61}, [\"D01\"]), (\"์ˆ˜๋Ÿ‰ ๊ฒฐ์ธก\", {**base, \"qty\": None}, [\"D04\"]), (\"์ˆ˜๋Ÿ‰ ์†Œ์ˆ˜\", {**base, \"qty\": 10.5}, [\"D12\"])]\n", "ok = 0\n", "print(\"\\n[๊ฒฝ๊ณ„ ์กฐ๊ฑด ๋‹จ์œ„ ํ…Œ์ŠคํŠธ]\")\n", "for n, r, exp in tests:\n", " got = sorted(i for i, _ in check(r))\n", " p = got == exp\n", " ok += p\n", " print(f\" - {n}: ๊ธฐ๋Œ€ {exp or '์œ„๋ฐ˜ ์—†์Œ'} / ๊ฒฐ๊ณผ {got or '์œ„๋ฐ˜ ์—†์Œ'} -> {'PASS' if p else 'FAIL'}\")\n", "print(f\"๋‹จ์œ„ ํ…Œ์ŠคํŠธ {ok}/{len(tests)} ํ†ต๊ณผ({ok / len(tests):.0%})\")\n", "print(f\"๋ชฉํ‘œ(๋ฃฐ 15์ข… ์ด์ƒ, ์ง€์—ฐ 50ms ์ดํ•˜, ๋‹จ์œ„ ํ…Œ์ŠคํŠธ 100%): {'๋‹ฌ์„ฑ' if len(RULES) >= 15 and lat.max() <= 50 and ok == len(tests) else '๋ฏธ๋‹ฌ'}\")\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "
\n", "๐Ÿ’ก [์ฐธ๊ณ ] ๋ฏธ์…˜ 03 ์˜ˆ์ƒ ์‹คํ–‰ ๊ฒฐ๊ณผ (Output)\n", "\n", "```text\n", "[๊ฒ€์ฆ ๋ฃฐ 15์ข…] ๋Œ€์ƒ 2160๊ฑด, ๋ฃฐ ์œ„๋ฐ˜ 107๊ฑด [๊ต์œก์šฉ๊ฐ€์ƒ์ž๋ฃŒ]\n", "๋ฃฐ๋ณ„ ์œ„๋ฐ˜: {'D04': 46, 'D05': 3, 'D13': 11, 'D14': 47}\n", "๋ฃฐ ์ฒ˜๋ฆฌ ์ง€์—ฐ ํ‰๊ท  0.006ms, ์ตœ๋Œ€ 0.185ms (๋ชฉํ‘œ 50ms ์ดํ•˜: ๋‹ฌ์„ฑ, ํ™˜๊ฒฝ์— ๋”ฐ๋ผ ๋‹ค๋ฆ„)\n", "\n", "[Baseline ์ง€ํ‘œ ์ฟผ๋ฆฌ] ์ฟผ๋ฆฌ 3๊ฐœ ์‹คํ–‰ 32.7ms\n", "๋ถ„๋ฅ˜๋ณ„ ์žฌ๊ณ  ํ•ฉ๊ณ„(์ฒด๊ณ„ A ๊ธฐ์ค€): {'๊ธ‰์‹': 33087, '์ˆ˜๋ฆฌ๋ถ€์†': 42203, '์œ ๋ฅ˜': 40597, '์ •๋น„์†Œ๋ชจ': 38378, 'ํƒ„์•ฝ': 39855, 'ํ”ผ๋ณต': 39222}\n", "์žฌ๊ณ  60 ๋ฏธ๋งŒ ๋ถ€๋Œ€๊ฐ€ ๋งŽ์€ ํ’ˆ๋ชฉ ์ƒ์œ„ 3: {2: 3, 10: 3, 60: 3}\n", "์„ธ ์ฒด๊ณ„ ๋ชจ๋‘ ๋ณด๊ณ ํ•œ 677๊ฐœ ํ‚ค ์ค‘ ์ˆ˜๋Ÿ‰์ด ์™„์ „ํžˆ ๊ฐ™์€ ๋น„์œจ(์ฒด๊ณ„ ๊ฐ„ ์ผ์น˜์œจ) 74.0% -> ์ดํ›„ ๋ฏธ์…˜์—์„œ ๋ถˆ์ผ์น˜ ์›์ธ์„ ํ•ด๊ฒฐ\n", "\n", "[๊ฒฝ๊ณ„ ์กฐ๊ฑด ๋‹จ์œ„ ํ…Œ์ŠคํŠธ]\n", " - ์ •์ƒ: ๊ธฐ๋Œ€ ์œ„๋ฐ˜ ์—†์Œ / ๊ฒฐ๊ณผ ์œ„๋ฐ˜ ์—†์Œ -> PASS\n", " - ์ˆ˜๋Ÿ‰ 0 ํ—ˆ์šฉ: ๊ธฐ๋Œ€ ์œ„๋ฐ˜ ์—†์Œ / ๊ฒฐ๊ณผ ์œ„๋ฐ˜ ์—†์Œ -> PASS\n", " - ์ˆ˜๋Ÿ‰ ์ƒํ•œ 5000 ๊ฒฝ๊ณ„(D06 ํ†ต๊ณผ, D14 ์˜์‹ฌ): ๊ธฐ๋Œ€ ['D14'] / ๊ฒฐ๊ณผ ['D14'] -> PASS\n", " - ์ˆ˜๋Ÿ‰ 5001: ๊ธฐ๋Œ€ ['D06'] / ๊ฒฐ๊ณผ ['D06'] -> PASS\n", " - ์ง€์—ฐ 3์ผ ํ—ˆ์šฉ: ๊ธฐ๋Œ€ ์œ„๋ฐ˜ ์—†์Œ / ๊ฒฐ๊ณผ ์œ„๋ฐ˜ ์—†์Œ -> PASS\n", " - ์ง€์—ฐ 4์ผ: ๊ธฐ๋Œ€ ['D09'] / ๊ฒฐ๊ณผ ['D09'] -> PASS\n", " - ๋ฏธ๋ž˜ ๋‚ ์งœ: ๊ธฐ๋Œ€ ['D08', 'D10'] / ๊ฒฐ๊ณผ ['D08', 'D10'] -> PASS\n", " - ํ’ˆ๋ชฉ ํ‚ค 61: ๊ธฐ๋Œ€ ['D01'] / ๊ฒฐ๊ณผ ['D01'] -> PASS\n", " - ์ˆ˜๋Ÿ‰ ๊ฒฐ์ธก: ๊ธฐ๋Œ€ ['D04'] / ๊ฒฐ๊ณผ ['D04'] -> PASS\n", " - ์ˆ˜๋Ÿ‰ ์†Œ์ˆ˜: ๊ธฐ๋Œ€ ['D12'] / ๊ฒฐ๊ณผ ['D12'] -> PASS\n", "๋‹จ์œ„ ํ…Œ์ŠคํŠธ 10/10 ํ†ต๊ณผ(100%)\n", "๋ชฉํ‘œ(๋ฃฐ 15์ข… ์ด์ƒ, ์ง€์—ฐ 50ms ์ดํ•˜, ๋‹จ์œ„ ํ…Œ์ŠคํŠธ 100%): ๋‹ฌ์„ฑ\n", "```\n", "
\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "---\n", "\n", "## ๐ŸŽฏ ๋ฏธ์…˜ 04. ์‹ค์‹œ๊ฐ„ ๋ฐ์ดํ„ฐ ์ŠคํŠธ๋ฆฌ๋ฐ(Kafka/CDC) ๊ธฐ๋ฐ˜ ์ด๊ธฐ์ข… ๋ฐ์ดํ„ฐ ์ •ํ•ฉ์„ฑ ์œ ์ง€ ๋ฐ ์ž๋™ ๋™๊ธฐํ™” ๊ณ ๋„ํ™”\n", "**์†Œ์š”์‹œ๊ฐ„**: ์•ฝ 3.0์‹œ๊ฐ„\n", "\n", "### ๐Ÿ“‹ ๋ฏธ์…˜ ๋ฐฐ๊ฒฝ ๋ฐ ์ˆ˜ํ–‰ ์ง€์นจ\n", "#### ๋ฏธ์…˜ ๋ฐฐ๊ฒฝ ๋ฐ ๋‹น์œ„์„ฑ\n", "\n", "์‹ค์‹œ๊ฐ„ ๋ฐ์ดํ„ฐ ์ŠคํŠธ๋ฆฌ๋ฐ ๊ธฐ๋ฐ˜ ์ด๊ธฐ์ข… ๋ฐ์ดํ„ฐ ์ •ํ•ฉ์„ฑ ์œ ์ง€ ๋ฐ ์ž๋™ ๋™๊ธฐํ™”๋ฅผ ๊ณ ๋„ํ™”ํ•œ๋‹ค. ์‹ค์ œ ์ „์ˆ  ํ˜„์žฅ์—์„œ๋Š” ๋‹จ์ˆœ ํƒ์ง€์— ๊ทธ์น˜์ง€ ์•Š๊ณ  ์‹ ์†ํ•œ ์œ„ํ˜‘ ํŒ๋‹จ๊ณผ ์‹ ๋ขฐ๋„ ๋†’์€ ์˜์‚ฌ๊ฒฐ์ •์ด ํ•„์ˆ˜์ ์ด๋ฏ€๋กœ, **๋ณ€๊ฒฝ ์ด๋ฒคํŠธ ์ŠคํŠธ๋ฆผ(CDC ๋ฐฉ์‹์„ ๋ฉ”๋ชจ๋ฆฌ์—์„œ ๋ชจ์˜)์œผ๋กœ ์ฒด๊ณ„ ๊ฐ„ ์ •ํ•ฉ์„ฑ์„ ์œ ์ง€ํ•˜๊ณ  ๋ถˆ์ผ์น˜ ์›์ธ์„ ๋ถ„๋ฅ˜ํ•ด ๋งˆ์Šคํ„ฐ ๊ฐ’ ํ›„๋ณด๋ฅผ ์ถ”์ฒœํ•˜๋Š” ๋ชจ๋“ˆ์„ ๊ณ ๋„ํ™”ํ•œ๋‹ค.** ๊ณผ์—…์„ ์™„์ˆ˜ํ•˜์—ฌ ์˜คํŒ์„ ๋ฐฉ์ง€ํ•˜๊ณ  ์ž‘์ „ ๋Œ€์‘๋ ฅ์„ ๊ทน๋Œ€ํ™”ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.\n", "\n", "์ด ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด ๋ณธ ๋ฏธ์…˜์—์„œ๋Š” **์™ธ๋ถ€ ์ŠคํŠธ๋ฆฌ๋ฐ ๋„๊ตฌ ์—†์ด ๋ฉ”๋ชจ๋ฆฌ ์•ˆ์˜ ํ๋กœ ๋ณ€๊ฒฝ ์ด๋ฒคํŠธ๋ฅผ ๋งŒ๋“ค๊ณ , ์ค‘๋ณต ์ „์†ก๊ณผ ์ˆœ์„œ๊ฐ€ ๋’ค๋ฐ”๋€ ์˜ค๋ž˜๋œ ๊ฐ’์ด ์„ž์ธ ์ƒํƒœ์—์„œ ์ด๋ฒคํŠธ ID ์ค‘๋ณต ์ œ๊ฑฐ์™€ ํ‚ค๋ณ„ ์ตœ์‹  ์‹œ๊ฐ ์šฐ์„  ๊ทœ์น™์œผ๋กœ ๋™๊ธฐํ™” ์ˆ˜ํ–‰ํ•˜๊ณ  ๊ฒ€์ฆํ•˜๋Š”** ์‹ค์Šต์„ ์ฒด๊ณ„์ ์œผ๋กœ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค.\n", "\n", "ํ•™์Šต์ž๋Š” ์‹ค์Šต๊ฐ€์ด๋“œ์— ์ œ์‹œ๋œ ์ด 4๊ฐœ ์„ธ๋ถ€ ํ™œ๋™(๊ธฐ๋ณธ ๊ธฐ๋Šฅ ๊ตฌํ˜„ โž” ๊ฒฐ๊ณผ ์ง€ํ‘œ ๊ฒ€์ฆ โž” ํŒŒ๋ผ๋ฏธํ„ฐ ํŠœ๋‹ ๋ฐ ๋„์ „๊ณผ์ œ)์„ ์ˆœ์ฐจ์ ์œผ๋กœ ์ง„ํ–‰ํ•˜๋ฉฐ ํ•ต์‹ฌ ํŒ์ • ๊ธฐ์ค€์„ ๋‹ฌ์„ฑํ•ฉ๋‹ˆ๋‹ค.\n", "\n", "**์ด๋ฒˆ ๊ฒฐ๊ณผ๋ฌผ:** `src/ai/dat09_intelligence_module.py` ํ˜•ํƒœ์˜ ๊ฒฐ๊ณผ๋ฌผ์„ ์™„์„ฑํ•œ๋‹ค.\n", "\n", "### ๐Ÿ” ํ‰๊ฐ€ ๋ฐ ๊ฒ€์ฆ ๊ธฐ์ค€ (PASS ์กฐ๊ฑด)\n", "#### ๋ฏธ์…˜ ํŒ์ •\n", "\n", "**ํ‰๊ฐ€ ๊ตฌ๋ถ„:** ํ•„์ˆ˜\n", "\n", "**ํ•ต์‹ฌ ํ‰๊ฐ€์˜์—ญ:** ์ถ”์ฒœ ์‚ฌ์œ ์˜ ํ•ฉ๋ฆฌ์„ฑ ๋ฐ ์‹ค๋ฌด ์ˆ˜์šฉ์„ฑ\n", "\n", "#### ๋ฏธ์…˜ PASS ์กฐ๊ฑด\n", "\n", "- ์ •๋Ÿ‰ ๊ธฐ์ค€: ์ถ”์ฒœ/๋ถ„๋ฅ˜ ์ •ํ™•๋„ 85% ์ด์ƒ ๋‹ฌ์„ฑ\n", "\n", "\n", "- ์ง„ํ–‰ ๊ธฐ์ค€: AI ์ถ”๋ก  ๋ชจ๋“ˆ ๊ตฌํ˜„ ๋ฐ ๋ฒค์น˜๋งˆํฌ ํ…Œ์ŠคํŠธ ์™„๋ฃŒ\n", "\n", "\n", "- ๊ฒ€์ฆ ๋ฐฉ๋ฒ•: ๊ณผ๊ฑฐ ์Šน์ธ/์ฒ˜๋ฆฌ ์™„๋ฃŒ ๋ฐ์ดํ„ฐ์™€์˜ ์ถ”์ฒœ ์ผ์น˜๋„ ๊ฒ€์ฆ\n", "\n", "\n", "#### ์ฑ„์ ์ž ํŒ์ • ๋ฉ”๋ชจ\n", "\n", "> ๋‹จ์ˆœ ์ฝ”๋“œ ์‹คํ–‰ ์—ฌ๋ถ€๋ฟ๋งŒ ์•„๋‹ˆ๋ผ ์ถœ๋ ฅ๋œ ์ˆ˜์น˜์™€ ๊ทผ๊ฑฐ ์ž๋ฃŒ๊ฐ€ ๊ตฐ ์‹ค๋ฌด ์š”๊ตฌ์‚ฌํ•ญ์— ๋ถ€ํ•ฉํ•˜๋Š”์ง€ ๋ฉด๋ฐ€ํžˆ ํ™•์ธํ•œ๋‹ค.\n", "\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "\"\"\"๋ณ€๊ฒฝ ์ด๋ฒคํŠธ ์ŠคํŠธ๋ฆผ(CDC ๋ฐฉ์‹ ๋ชจ์˜)์„ ์ˆœ์„œ ๋’ค๋ฐ”๋€œยท์ค‘๋ณต ์ด๋ฒคํŠธ๊นŒ์ง€ ์ฒ˜๋ฆฌํ•ด ํ†ตํ•ฉ ์ €์žฅ์†Œ์— ์ž๋™ ๋™๊ธฐํ™”ํ•˜๊ณ , ์ฒด๊ณ„ ๊ฐ„ ๋ถˆ์ผ์น˜์˜ ์›์ธ์„ ๋ถ„๋ฅ˜ํ•ด ๋งˆ์Šคํ„ฐ ๊ฐ’ ํ›„๋ณด๋ฅผ ์ถ”์ฒœํ•˜๋Š” ๋ชจ๋“ˆ์„ ๊ตฌํ˜„ํ•ด ๋ถ„๋ฅ˜ยท์ถ”์ฒœ ์ •ํ™•๋„๋ฅผ ์ธก์ •ํ•œ๋‹ค.\"\"\"\n", "import collections\n", "import datetime\n", "import numpy as np\n", "import pandas as pd\n", "\n", "# ---------------------------------------------------------------------------\n", "# [๊ต์œก์šฉ๊ฐ€์ƒ์ž๋ฃŒ] ๊ฐ€์ƒ ์ด๊ธฐ์ข… ๊ตฐ์ˆ˜์ •๋ณด์ฒด๊ณ„ 3์ข…(A ๋ณด๊ธ‰, B ์ •๋น„, C ํƒ„์•ฝ)์˜ ์žฌ๊ณ  ๋ณด๊ณ  ์ƒ์„ฑ๊ธฐ\n", "# ์‹ค์ œ ์ฒด๊ณ„๋ช…ยท๋ถ€๋Œ€ยทํ’ˆ๋ชฉยท์ˆ˜๋Ÿ‰ ์—†์Œ. ๊ฐ™์€ (ํ’ˆ๋ชฉ, ๋ถ€๋Œ€) ์žฌ๊ณ ๋ฅผ ์„ธ ์ฒด๊ณ„๊ฐ€ ์„œ๋กœ ๋‹ค๋ฅธ ์ฝ”๋“œยท๋‚ ์งœ ํ˜•์‹์œผ๋กœ ๋ณด๊ณ ํ•œ๋‹ค.\n", "# ๊ธฐ์ค€ ๋‚ ์งœ: 2026-09-30(๊ฐ€์ƒ). ๋งˆ์Šคํ„ฐ(์ •๋‹ต) ์žฌ๊ณ  TRUE ์™€ ์ฒด๊ณ„๋ณ„ ๋ณด๊ณ ์—๋Š” ์˜๋„์  ๋ถˆ์ผ์น˜ยท๊ฒฐ์ธก์ด ์„ž์—ฌ ์žˆ๋‹ค.\n", "# ---------------------------------------------------------------------------\n", "N_PART, N_UNIT = 60, 12\n", "CATS = [\"๊ธ‰์‹\", \"ํ”ผ๋ณต\", \"์œ ๋ฅ˜\", \"์ •๋น„์†Œ๋ชจ\", \"์ˆ˜๋ฆฌ๋ถ€์†\", \"ํƒ„์•ฝ\"]\n", "PARTS = {p: {\"name\": f\"๊ฐ€์ƒํ’ˆ๋ชฉ{p:02d}\", \"cat\": CATS[(p - 1) % 6]} for p in range(1, N_PART + 1)}\n", "FMT = {\"A\": dict(part=lambda p: f\"AP-{p:05d}\", unit=lambda u: f\"U{u:02d}\", date=lambda d: d.strftime(\"%Y%m%d\")),\n", " \"B\": dict(part=lambda p: f\"{p:05d}-X\", unit=lambda u: f\"๊ฐ€์ƒ{u:02d}๋Œ€๋Œ€\", date=lambda d: d.strftime(\"%Y-%m-%d\")),\n", " \"C\": dict(part=lambda p: f\"P{p}\", unit=lambda u: str(u), date=lambda d: d.strftime(\"%d/%m/%Y\"))}\n", "BASE_DATE = datetime.date(2026, 9, 30)\n", "\n", "\n", "def make_systems(seed=9, mismatch=0.10, missing=0.03):\n", " \"\"\"TRUE(์ •๋‹ต ์žฌ๊ณ )์™€ ์ฒด๊ณ„๋ณ„ ์›๋ณธ ๋ณด๊ณ  ๋ ˆ์ฝ”๋“œ(์ฒด๊ณ„ ๊ณ ์œ  ํ˜•์‹)๋ฅผ ๋งŒ๋“ ๋‹ค.\"\"\"\n", " rng = np.random.default_rng(seed)\n", " true = {(p, u): int(rng.integers(20, 500)) for p in PARTS for u in range(1, N_UNIT + 1)}\n", " out = {s: [] for s in \"ABC\"}\n", " truth_diff = {} # (์ฒด๊ณ„, p, u) -> ๋ถˆ์ผ์น˜ ์›์ธ(์ •๋‹ต)\n", " for (p, u), q in true.items():\n", " for s in \"ABC\":\n", " qty, cause = q, None\n", " r = rng.random()\n", " if r < mismatch / 3: # ์ž…๋ ฅ ์˜ค๋ฅ˜(์ž๋ฆฟ์ˆ˜/์˜คํƒ€)\n", " qty, cause = q + int(rng.choice([-1, 1]) * rng.integers(10, 60)), \"์ž…๋ ฅ ์˜ค๋ฅ˜\"\n", " elif r < 2 * mismatch / 3: # ๋ฐ˜์˜ ์ง€์—ฐ(์ „์ผ ๊ฐ’)\n", " qty, cause = q + int(rng.integers(5, 40)), \"๋ฐ˜์˜ ์ง€์—ฐ\"\n", " elif r < mismatch: # ๋‹จ์œ„ ์˜ค๋ฅ˜(10๋ฐฐ)\n", " qty, cause = q * 10, \"๋‹จ์œ„ ์˜ค๋ฅ˜\"\n", " d = BASE_DATE - datetime.timedelta(days=int(cause == \"๋ฐ˜์˜ ์ง€์—ฐ\"))\n", " if rng.random() < missing: # ๊ฒฐ์ธก\n", " qty, cause = None, \"๊ฒฐ์ธก\"\n", " if cause:\n", " truth_diff[(s, p, u)] = cause\n", " out[s].append({\"part\": FMT[s][\"part\"](p), \"unit\": FMT[s][\"unit\"](u), \"date\": FMT[s][\"date\"](d), \"qty\": qty})\n", " return true, out, truth_diff\n", "\n", "\n", "def parse_part(code):\n", " return int(code[3:]) if code.startswith(\"AP-\") else int(code[:-2]) if code.endswith(\"-X\") else int(code[1:])\n", "\n", "\n", "def parse_unit(code):\n", " return int(code[1:]) if code.startswith(\"U\") else int(code[2:4]) if code.startswith(\"๊ฐ€์ƒ\") else int(code)\n", "\n", "\n", "def parse_date(s):\n", " if \"/\" in s:\n", " d, m, y = s.split(\"/\"); return datetime.date(int(y), int(m), int(d))\n", " if \"-\" in s:\n", " return datetime.date(*map(int, s.split(\"-\")))\n", " return datetime.date(int(s[:4]), int(s[4:6]), int(s[6:]))\n", "\n", "true, raw, tdiff = make_systems()\n", "rng = np.random.default_rng(4)\n", "\n", "# 1) ์ด๋ฒคํŠธ ์ŠคํŠธ๋ฆผ ์ƒ์„ฑ: ๊ฐ ์ฒด๊ณ„ ๋ณด๊ณ ๋ฅผ (์ฒด๊ณ„, ํ‚ค, ๊ฐ’, ์‹œ๊ฐ, ์ด๋ฒคํŠธID) ์ด๋ฒคํŠธ๋กœ ๋ฐ”๊พธ๊ณ  ์ค‘๋ณตยท์ˆœ์„œ ๋’ค๋ฐ”๋€œ์„ ์ฃผ์ž…\n", "events = []\n", "for s in \"ABC\":\n", " for i, r in enumerate(raw[s]):\n", " events.append({\"id\": f\"{s}{i:04d}\", \"sys\": s, \"p\": parse_part(r[\"part\"]), \"u\": parse_unit(r[\"unit\"]), \"qty\": r[\"qty\"],\n", " \"ts\": int(rng.integers(0, 10000)), \"day\": parse_date(r[\"date\"])})\n", "dups = [dict(events[i]) for i in rng.choice(len(events), 120, replace=False)] # ์ค‘๋ณต ์ „์†ก 120๊ฑด\n", "stale = [] # ๊ฐ™์€ ํ‚ค์˜ ์˜ค๋ž˜๋œ ๊ฐ’(๋Šฆ๊ฒŒ ๋„์ฐฉํ•œ ์˜› ์ด๋ฒคํŠธ) 80๊ฑด\n", "for i in rng.choice(len(events), 80, replace=False):\n", " e = dict(events[i]); e[\"id\"] += \"-old\"; e[\"ts\"] = e[\"ts\"] - 5000; e[\"qty\"] = (e[\"qty\"] or 0) + 77\n", " stale.append(e)\n", "stream = events + dups + stale\n", "rng.shuffle(stream) # ๋„์ฐฉ ์ˆœ์„œ ๋’ค์„ž๊ธฐ\n", "print(f\"[์ŠคํŠธ๋ฆผ] ์ด๋ฒคํŠธ {len(stream)}๊ฑด (์ •์ƒ {len(events)}, ์ค‘๋ณต {len(dups)}, ์˜ค๋ž˜๋œ ๊ฐ’ {len(stale)}), ๋„์ฐฉ ์ˆœ์„œ ๋ฌด์ž‘์œ„\")\n", "\n", "# 2) ์ž๋™ ๋™๊ธฐํ™”: ์ด๋ฒคํŠธ ID ์ค‘๋ณต ์ œ๊ฑฐ + ํ‚ค๋ณ„ ์ตœ์‹  ์‹œ๊ฐ ์šฐ์„ (last-write-wins)\n", "store, seen, applied, skipped_dup, skipped_old = {}, set(), 0, 0, 0\n", "queue = collections.deque(stream)\n", "while queue:\n", " e = queue.popleft()\n", " if e[\"id\"] in seen:\n", " skipped_dup += 1; continue\n", " seen.add(e[\"id\"])\n", " k = (e[\"sys\"], e[\"p\"], e[\"u\"])\n", " if k in store and store[k][\"ts\"] >= e[\"ts\"]:\n", " skipped_old += 1; continue\n", " store[k] = {\"qty\": e[\"qty\"], \"ts\": e[\"ts\"], \"day\": e[\"day\"]}; applied += 1\n", "exp = {(e[\"sys\"], e[\"p\"], e[\"u\"]): e[\"qty\"] for e in events}\n", "match = sum(store[k][\"qty\"] == v for k, v in exp.items())\n", "print(f\"๋™๊ธฐํ™”: ์ ์šฉ {applied}๊ฑด, ์ค‘๋ณต ๊ฑด๋„ˆ๋œ€ {skipped_dup}๊ฑด, ์˜ค๋ž˜๋œ ๊ฐ’ ๊ฑด๋„ˆ๋œ€ {skipped_old}๊ฑด\")\n", "print(f\"์ผ๊ด„ ์Šค๋ƒ…์ƒท ๋Œ€๋น„ ๋™๊ธฐํ™” ๊ฒฐ๊ณผ ์ผ์น˜ {match}/{len(exp)}๊ฑด({match / len(exp):.1%}) -> ์ŠคํŠธ๋ฆผ ๋ฐฉ์‹๊ณผ ์ผ๊ด„ ๋ฐฉ์‹์˜ ๊ฒฐ๊ณผ๊ฐ€ ๊ฐ™์•„์•ผ ํ•œ๋‹ค\")\n", "\n", "# 3) ๋ถˆ์ผ์น˜ ๊ฐ์ง€ยท์›์ธ ๋ถ„๋ฅ˜ยท๋งˆ์Šคํ„ฐ ํ›„๋ณด ์ถ”์ฒœ\n", "rows = []\n", "for p in PARTS:\n", " for u in range(1, N_UNIT + 1):\n", " vals = {s: store[(s, p, u)] for s in \"ABC\"}\n", " q = {s: v[\"qty\"] for s, v in vals.items()}\n", " have = {s: x for s, x in q.items() if x is not None}\n", " cnt = collections.Counter(have.values())\n", " top, n_top = cnt.most_common(1)[0] if cnt else (None, 0)\n", " for s in \"ABC\":\n", " if q[s] == top and n_top >= 2:\n", " continue\n", " if q[s] is None:\n", " cause = \"๊ฒฐ์ธก\"\n", " elif n_top >= 2 and q[s] != top:\n", " lag = (BASE_DATE - vals[s][\"day\"]).days\n", " cause = \"๋‹จ์œ„ ์˜ค๋ฅ˜\" if q[s] == top * 10 else (\"๋ฐ˜์˜ ์ง€์—ฐ\" if lag >= 1 else \"์ž…๋ ฅ ์˜ค๋ฅ˜\")\n", " else:\n", " cause = \"ํŒ๋‹จ ๋ถˆ๊ฐ€\" # ์„ธ ๊ฐ’์ด ๋ชจ๋‘ ๋‹ค๋ฅด๋ฉด ์‚ฌ๋žŒ์ด ํŒ๋‹จ\n", " rows.append({\"sys\": s, \"p\": p, \"u\": u, \"cause\": cause, \"recommend\": top if n_top >= 2 else None, \"true\": true[(p, u)]})\n", "d = pd.DataFrame(rows)\n", "d[\"truth_cause\"] = [tdiff.get((r.sys, r.p, r.u)) for r in d.itertuples()]\n", "auto = d[d[\"cause\"] != \"ํŒ๋‹จ ๋ถˆ๊ฐ€\"]\n", "cause_acc = float((auto[\"cause\"] == auto[\"truth_cause\"]).mean())\n", "rec_ok = float((auto[\"recommend\"] == auto[\"true\"]).mean())\n", "true_cells = pd.Series(tdiff)\n", "print(f\"\\n[๋ถˆ์ผ์น˜ ๋ถ„๋ฅ˜] ๋ถˆ์ผ์น˜ ์…€ {len(d)}๊ฑด (์ •๋‹ต์ƒ ๋ถˆ์ผ์น˜ {len(true_cells)}๊ฑด), ์ž๋™ ๋ถ„๋ฅ˜ {len(auto)}๊ฑด, ์‚ฌ๋žŒ ํŒ๋‹จ ํ•„์š” {len(d) - len(auto)}๊ฑด\")\n", "print(pd.crosstab(auto[\"truth_cause\"].fillna(\"์ •์ƒ(์˜คํƒ)\"), auto[\"cause\"]).to_string())\n", "print(f\"์›์ธ ๋ถ„๋ฅ˜ ์ •ํ™•๋„ {cause_acc:.1%}, ๋งˆ์Šคํ„ฐ ๊ฐ’ ํ›„๋ณด(๋‹ค์ˆ˜๊ฒฐ) ์ •ํ™•๋„ {rec_ok:.1%}, ๊ฐ์ง€ ์žฌํ˜„์œจ {len(set(zip(d.sys, d.p, d.u)) & set(true_cells.index)) / len(true_cells):.1%}\")\n", "print(\"์ถ”์ฒœ ์‚ฌ์œ  ์˜ˆ์‹œ:\")\n", "for r in auto.head(3).itertuples():\n", " print(f\" - ์ฒด๊ณ„ {r.sys} ํ’ˆ๋ชฉ {r.p} ๋ถ€๋Œ€ {r.u}: ์›์ธ '{r.cause}', ๋‹ค๋ฅธ ๋‘ ์ฒด๊ณ„ ๋‹ค์ˆ˜๊ฐ’ {r.recommend}์„(๋ฅผ) ๋งˆ์Šคํ„ฐ ํ›„๋ณด๋กœ ์ œ์‹œ\")\n", "print(\"์ถ”์ฒœ์€ ํ›„๋ณด์ด๋ฉฐ ๋งˆ์Šคํ„ฐ ๋ฐ์ดํ„ฐ ํ™•์ •์€ ๋ฐ์ดํ„ฐ๊ด€๋ฆฌ๊ด€ ์Šน์ธ(๋ฏธ์…˜ 7)์œผ๋กœ๋งŒ ์ด๋ฃจ์–ด์ง„๋‹ค.\")\n", "print(f\"\\n๋ชฉํ‘œ(์ถ”์ฒœ/๋ถ„๋ฅ˜ ์ •ํ™•๋„ 85% ์ด์ƒ): ๋ถ„๋ฅ˜ {'๋‹ฌ์„ฑ' if cause_acc >= 0.85 else '๋ฏธ๋‹ฌ'}, ๋งˆ์Šคํ„ฐ ํ›„๋ณด {'๋‹ฌ์„ฑ' if rec_ok >= 0.85 else '๋ฏธ๋‹ฌ'}\")\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "
\n", "๐Ÿ’ก [์ฐธ๊ณ ] ๋ฏธ์…˜ 04 ์˜ˆ์ƒ ์‹คํ–‰ ๊ฒฐ๊ณผ (Output)\n", "\n", "```text\n", "[์ŠคํŠธ๋ฆผ] ์ด๋ฒคํŠธ 2360๊ฑด (์ •์ƒ 2160, ์ค‘๋ณต 120, ์˜ค๋ž˜๋œ ๊ฐ’ 80), ๋„์ฐฉ ์ˆœ์„œ ๋ฌด์ž‘์œ„\n", "๋™๊ธฐํ™”: ์ ์šฉ 2200๊ฑด, ์ค‘๋ณต ๊ฑด๋„ˆ๋œ€ 120๊ฑด, ์˜ค๋ž˜๋œ ๊ฐ’ ๊ฑด๋„ˆ๋œ€ 40๊ฑด\n", "์ผ๊ด„ ์Šค๋ƒ…์ƒท ๋Œ€๋น„ ๋™๊ธฐํ™” ๊ฒฐ๊ณผ ์ผ์น˜ 2160/2160๊ฑด(100.0%) -> ์ŠคํŠธ๋ฆผ ๋ฐฉ์‹๊ณผ ์ผ๊ด„ ๋ฐฉ์‹์˜ ๊ฒฐ๊ณผ๊ฐ€ ๊ฐ™์•„์•ผ ํ•œ๋‹ค\n", "\n", "[๋ถˆ์ผ์น˜ ๋ถ„๋ฅ˜] ๋ถˆ์ผ์น˜ ์…€ 261๊ฑด (์ •๋‹ต์ƒ ๋ถˆ์ผ์น˜ 243๊ฑด), ์ž๋™ ๋ถ„๋ฅ˜ 207๊ฑด, ์‚ฌ๋žŒ ํŒ๋‹จ ํ•„์š” 54๊ฑด\n", "cause ๊ฒฐ์ธก ๋‹จ์œ„ ์˜ค๋ฅ˜ ๋ฐ˜์˜ ์ง€์—ฐ ์ž…๋ ฅ ์˜ค๋ฅ˜\n", "truth_cause \n", "๊ฒฐ์ธก 46 0 0 0\n", "๋‹จ์œ„ ์˜ค๋ฅ˜ 0 57 0 0\n", "๋ฐ˜์˜ ์ง€์—ฐ 0 0 53 0\n", "์ž…๋ ฅ ์˜ค๋ฅ˜ 0 0 0 49\n", "์ •์ƒ(์˜คํƒ) 0 0 0 2\n", "์›์ธ ๋ถ„๋ฅ˜ ์ •ํ™•๋„ 99.0%, ๋งˆ์Šคํ„ฐ ๊ฐ’ ํ›„๋ณด(๋‹ค์ˆ˜๊ฒฐ) ์ •ํ™•๋„ 94.7%, ๊ฐ์ง€ ์žฌํ˜„์œจ 98.4%\n", "์ถ”์ฒœ ์‚ฌ์œ  ์˜ˆ์‹œ:\n", " - ์ฒด๊ณ„ A ํ’ˆ๋ชฉ 1 ๋ถ€๋Œ€ 3: ์›์ธ '์ž…๋ ฅ ์˜ค๋ฅ˜', ๋‹ค๋ฅธ ๋‘ ์ฒด๊ณ„ ๋‹ค์ˆ˜๊ฐ’ 481.0์„(๋ฅผ) ๋งˆ์Šคํ„ฐ ํ›„๋ณด๋กœ ์ œ์‹œ\n", " - ์ฒด๊ณ„ B ํ’ˆ๋ชฉ 1 ๋ถ€๋Œ€ 8: ์›์ธ '๋ฐ˜์˜ ์ง€์—ฐ', ๋‹ค๋ฅธ ๋‘ ์ฒด๊ณ„ ๋‹ค์ˆ˜๊ฐ’ 393.0์„(๋ฅผ) ๋งˆ์Šคํ„ฐ ํ›„๋ณด๋กœ ์ œ์‹œ\n", " - ์ฒด๊ณ„ C ํ’ˆ๋ชฉ 1 ๋ถ€๋Œ€ 10: ์›์ธ '๋ฐ˜์˜ ์ง€์—ฐ', ๋‹ค๋ฅธ ๋‘ ์ฒด๊ณ„ ๋‹ค์ˆ˜๊ฐ’ 363.0์„(๋ฅผ) ๋งˆ์Šคํ„ฐ ํ›„๋ณด๋กœ ์ œ์‹œ\n", "์ถ”์ฒœ์€ ํ›„๋ณด์ด๋ฉฐ ๋งˆ์Šคํ„ฐ ๋ฐ์ดํ„ฐ ํ™•์ •์€ ๋ฐ์ดํ„ฐ๊ด€๋ฆฌ๊ด€ ์Šน์ธ(๋ฏธ์…˜ 7)์œผ๋กœ๋งŒ ์ด๋ฃจ์–ด์ง„๋‹ค.\n", "\n", "๋ชฉํ‘œ(์ถ”์ฒœ/๋ถ„๋ฅ˜ ์ •ํ™•๋„ 85% ์ด์ƒ): ๋ถ„๋ฅ˜ ๋‹ฌ์„ฑ, ๋งˆ์Šคํ„ฐ ํ›„๋ณด ๋‹ฌ์„ฑ\n", "```\n", "
\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "---\n", "\n", "## ๐ŸŽฏ ๋ฏธ์…˜ 05. ์ฒด๊ณ„ ๊ฐ„ ๋ถˆ์ผ์น˜ ๋ฐ์ดํ„ฐ ์ž๋™ ์‹๋ณ„ ๋ฐ ๋ฐ์ดํ„ฐ ์‹ ๋ขฐ๋„ ํ’ˆ์งˆ ์ง€์ˆ˜ ์‚ฐ์ถœ ์—”์ง„ ๊ฐœ๋ฐœ\n", "**์†Œ์š”์‹œ๊ฐ„**: ์•ฝ 3.0์‹œ๊ฐ„\n", "\n", "### ๐Ÿ“‹ ๋ฏธ์…˜ ๋ฐฐ๊ฒฝ ๋ฐ ์ˆ˜ํ–‰ ์ง€์นจ\n", "#### ๋ฏธ์…˜ ๋ฐฐ๊ฒฝ ๋ฐ ๋‹น์œ„์„ฑ\n", "\n", "์ฒด๊ณ„ ๊ฐ„ ๋ถˆ์ผ์น˜ ๋ฐ์ดํ„ฐ ์ž๋™ ์‹๋ณ„ ๋ฐ ํ’ˆ์งˆ ์‹ ๋ขฐ๋„ ์ง€์ˆ˜ ์‚ฐ์ถœ ์—”์ง„์„ ๊ฐœ๋ฐœํ•œ๋‹ค. ์‹ค์ œ ์ „์ˆ  ํ˜„์žฅ์—์„œ๋Š” ๋‹จ์ˆœ ํƒ์ง€์— ๊ทธ์น˜์ง€ ์•Š๊ณ  ์‹ ์†ํ•œ ์œ„ํ˜‘ ํŒ๋‹จ๊ณผ ์‹ ๋ขฐ๋„ ๋†’์€ ์˜์‚ฌ๊ฒฐ์ •์ด ํ•„์ˆ˜์ ์ด๋ฏ€๋กœ, **์ฒด๊ณ„ ๊ฐ„ ๋ถˆ์ผ์น˜ ๋ฐ์ดํ„ฐ๋ฅผ ์ž๋™ ์‹๋ณ„ํ•˜๊ณ  ๋ฐ์ดํ„ฐ ํ’ˆ์งˆ ์ง€์ˆ˜๋ฅผ ์‚ฐ์ถœํ•˜๋Š” ์—”์ง„์„ ์ž…๋ ฅ-๋ถ„์„-๊ฒ€ํ† -๊ฒฐ๊ณผ 4๊ฐœ ํ™”๋ฉด์œผ๋กœ ๊ตฌํ˜„ํ•œ๋‹ค.** ๊ณผ์—…์„ ์™„์ˆ˜ํ•˜์—ฌ ์˜คํŒ์„ ๋ฐฉ์ง€ํ•˜๊ณ  ์ž‘์ „ ๋Œ€์‘๋ ฅ์„ ๊ทน๋Œ€ํ™”ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.\n", "\n", "์ด ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด ๋ณธ ๋ฏธ์…˜์—์„œ๋Š” **ํ‚ค๋ณ„๋กœ ์„ธ ์ฒด๊ณ„์˜ ๊ฐ’์„ ๋น„๊ตํ•ด ๊ฐ’ ๋ถˆ์ผ์น˜์™€ ๊ฒฐ์ธก์„ ์‹๋ณ„ํ•˜๊ณ  ํŽธ์ฐจ๊ฐ€ ํฐ ๊ฑด์„ ์šฐ์„  ๊ฒ€ํ†  ๋Œ€์ƒ์œผ๋กœ ํ‘œ์‹œ ์ˆ˜ํ–‰ํ•˜๊ณ  ๊ฒ€์ฆํ•˜๋Š”** ์‹ค์Šต์„ ์ฒด๊ณ„์ ์œผ๋กœ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค.\n", "\n", "ํ•™์Šต์ž๋Š” ์‹ค์Šต๊ฐ€์ด๋“œ์— ์ œ์‹œ๋œ ์ด 4๊ฐœ ์„ธ๋ถ€ ํ™œ๋™(๊ธฐ๋ณธ ๊ธฐ๋Šฅ ๊ตฌํ˜„ โž” ๊ฒฐ๊ณผ ์ง€ํ‘œ ๊ฒ€์ฆ โž” ํŒŒ๋ผ๋ฏธํ„ฐ ํŠœ๋‹ ๋ฐ ๋„์ „๊ณผ์ œ)์„ ์ˆœ์ฐจ์ ์œผ๋กœ ์ง„ํ–‰ํ•˜๋ฉฐ ํ•ต์‹ฌ ํŒ์ • ๊ธฐ์ค€์„ ๋‹ฌ์„ฑํ•ฉ๋‹ˆ๋‹ค.\n", "\n", "**์ด๋ฒˆ ๊ฒฐ๊ณผ๋ฌผ:** `src/ui/dat09_app.py` ํ˜•ํƒœ์˜ ๊ฒฐ๊ณผ๋ฌผ์„ ์™„์„ฑํ•œ๋‹ค.\n", "\n", "### ๐Ÿ” ํ‰๊ฐ€ ๋ฐ ๊ฒ€์ฆ ๊ธฐ์ค€ (PASS ์กฐ๊ฑด)\n", "#### ๋ฏธ์…˜ ํŒ์ •\n", "\n", "**ํ‰๊ฐ€ ๊ตฌ๋ถ„:** ํ•„์ˆ˜\n", "\n", "**ํ•ต์‹ฌ ํ‰๊ฐ€์˜์—ญ:** ๊ตฐ ์‹ค๋ฌด์ž ์นœํ™”์  ๋””์ž์ธ ๋ฐ ์กฐ์ž‘ ํŽธ์˜์„ฑ\n", "\n", "#### ๋ฏธ์…˜ PASS ์กฐ๊ฑด\n", "\n", "- ์ •๋Ÿ‰ ๊ธฐ์ค€: ํ•ต์‹ฌ ํ™”๋ฉด 4์ข…(์ž…๋ ฅ-๋ถ„์„-๊ฒ€ํ† -๊ฒฐ๊ณผ) ๊ตฌํ˜„ ์™„๋ฃŒ\n", "\n", "\n", "- ์ง„ํ–‰ ๊ธฐ์ค€: UI ํ”„๋กœํ† ํƒ€์ž… ๋กœ์ปฌ ์„œ๋ฒ„ ๊ตฌ๋™ ๋ฐ ์ „์ฒด ํŽ˜์ด์ง€ ๋„ค๋น„๊ฒŒ์ด์…˜ ํ™•์ธ\n", "\n", "\n", "- ๊ฒ€์ฆ ๋ฐฉ๋ฒ•: ์‚ฌ์šฉ์„ฑ ํ‰๊ฐ€(UX Walkthrough) ๋ฐ ์ธํ„ฐ๋ž™์…˜ ํ…Œ์ŠคํŠธ\n", "\n", "\n", "#### ์ฑ„์ ์ž ํŒ์ • ๋ฉ”๋ชจ\n", "\n", "> ๋‹จ์ˆœ ์ฝ”๋“œ ์‹คํ–‰ ์—ฌ๋ถ€๋ฟ๋งŒ ์•„๋‹ˆ๋ผ ์ถœ๋ ฅ๋œ ์ˆ˜์น˜์™€ ๊ทผ๊ฑฐ ์ž๋ฃŒ๊ฐ€ ๊ตฐ ์‹ค๋ฌด ์š”๊ตฌ์‚ฌํ•ญ์— ๋ถ€ํ•ฉํ•˜๋Š”์ง€ ๋ฉด๋ฐ€ํžˆ ํ™•์ธํ•œ๋‹ค.\n", "\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "\"\"\"์„ธ ์ฒด๊ณ„ ๊ฐ„ ๋ถˆ์ผ์น˜ ๋ฐ์ดํ„ฐ๋ฅผ ์ž๋™ ์‹๋ณ„ํ•˜๊ณ  ์™„์ „์„ฑยท์ผ์น˜์„ฑยท์ ์‹œ์„ฑยท์œ ํšจ์„ฑ 4๊ฐœ ์ฐจ์›์˜ ๋ฐ์ดํ„ฐ ํ’ˆ์งˆ ์ง€์ˆ˜๋ฅผ ๋ถ€๋Œ€๋ณ„ยท์ฒด๊ณ„๋ณ„๋กœ ์‚ฐ์ถœํ•˜๋ฉฐ, ์ž…๋ ฅ-๋ถ„์„-๊ฒ€ํ† -๊ฒฐ๊ณผ 4๊ฐœ ํ™”๋ฉด์„ ํ…์ŠคํŠธ ํ™”๋ฉด์œผ๋กœ ๊ตฌํ˜„ํ•œ๋‹ค.\"\"\"\n", "import datetime\n", "import numpy as np\n", "import pandas as pd\n", "\n", "# ---------------------------------------------------------------------------\n", "# [๊ต์œก์šฉ๊ฐ€์ƒ์ž๋ฃŒ] ๊ฐ€์ƒ ์ด๊ธฐ์ข… ๊ตฐ์ˆ˜์ •๋ณด์ฒด๊ณ„ 3์ข…(A ๋ณด๊ธ‰, B ์ •๋น„, C ํƒ„์•ฝ)์˜ ์žฌ๊ณ  ๋ณด๊ณ  ์ƒ์„ฑ๊ธฐ\n", "# ์‹ค์ œ ์ฒด๊ณ„๋ช…ยท๋ถ€๋Œ€ยทํ’ˆ๋ชฉยท์ˆ˜๋Ÿ‰ ์—†์Œ. ๊ฐ™์€ (ํ’ˆ๋ชฉ, ๋ถ€๋Œ€) ์žฌ๊ณ ๋ฅผ ์„ธ ์ฒด๊ณ„๊ฐ€ ์„œ๋กœ ๋‹ค๋ฅธ ์ฝ”๋“œยท๋‚ ์งœ ํ˜•์‹์œผ๋กœ ๋ณด๊ณ ํ•œ๋‹ค.\n", "# ๊ธฐ์ค€ ๋‚ ์งœ: 2026-09-30(๊ฐ€์ƒ). ๋งˆ์Šคํ„ฐ(์ •๋‹ต) ์žฌ๊ณ  TRUE ์™€ ์ฒด๊ณ„๋ณ„ ๋ณด๊ณ ์—๋Š” ์˜๋„์  ๋ถˆ์ผ์น˜ยท๊ฒฐ์ธก์ด ์„ž์—ฌ ์žˆ๋‹ค.\n", "# ---------------------------------------------------------------------------\n", "N_PART, N_UNIT = 60, 12\n", "CATS = [\"๊ธ‰์‹\", \"ํ”ผ๋ณต\", \"์œ ๋ฅ˜\", \"์ •๋น„์†Œ๋ชจ\", \"์ˆ˜๋ฆฌ๋ถ€์†\", \"ํƒ„์•ฝ\"]\n", "PARTS = {p: {\"name\": f\"๊ฐ€์ƒํ’ˆ๋ชฉ{p:02d}\", \"cat\": CATS[(p - 1) % 6]} for p in range(1, N_PART + 1)}\n", "FMT = {\"A\": dict(part=lambda p: f\"AP-{p:05d}\", unit=lambda u: f\"U{u:02d}\", date=lambda d: d.strftime(\"%Y%m%d\")),\n", " \"B\": dict(part=lambda p: f\"{p:05d}-X\", unit=lambda u: f\"๊ฐ€์ƒ{u:02d}๋Œ€๋Œ€\", date=lambda d: d.strftime(\"%Y-%m-%d\")),\n", " \"C\": dict(part=lambda p: f\"P{p}\", unit=lambda u: str(u), date=lambda d: d.strftime(\"%d/%m/%Y\"))}\n", "BASE_DATE = datetime.date(2026, 9, 30)\n", "\n", "\n", "def make_systems(seed=9, mismatch=0.10, missing=0.03):\n", " \"\"\"TRUE(์ •๋‹ต ์žฌ๊ณ )์™€ ์ฒด๊ณ„๋ณ„ ์›๋ณธ ๋ณด๊ณ  ๋ ˆ์ฝ”๋“œ(์ฒด๊ณ„ ๊ณ ์œ  ํ˜•์‹)๋ฅผ ๋งŒ๋“ ๋‹ค.\"\"\"\n", " rng = np.random.default_rng(seed)\n", " true = {(p, u): int(rng.integers(20, 500)) for p in PARTS for u in range(1, N_UNIT + 1)}\n", " out = {s: [] for s in \"ABC\"}\n", " truth_diff = {} # (์ฒด๊ณ„, p, u) -> ๋ถˆ์ผ์น˜ ์›์ธ(์ •๋‹ต)\n", " for (p, u), q in true.items():\n", " for s in \"ABC\":\n", " qty, cause = q, None\n", " r = rng.random()\n", " if r < mismatch / 3: # ์ž…๋ ฅ ์˜ค๋ฅ˜(์ž๋ฆฟ์ˆ˜/์˜คํƒ€)\n", " qty, cause = q + int(rng.choice([-1, 1]) * rng.integers(10, 60)), \"์ž…๋ ฅ ์˜ค๋ฅ˜\"\n", " elif r < 2 * mismatch / 3: # ๋ฐ˜์˜ ์ง€์—ฐ(์ „์ผ ๊ฐ’)\n", " qty, cause = q + int(rng.integers(5, 40)), \"๋ฐ˜์˜ ์ง€์—ฐ\"\n", " elif r < mismatch: # ๋‹จ์œ„ ์˜ค๋ฅ˜(10๋ฐฐ)\n", " qty, cause = q * 10, \"๋‹จ์œ„ ์˜ค๋ฅ˜\"\n", " d = BASE_DATE - datetime.timedelta(days=int(cause == \"๋ฐ˜์˜ ์ง€์—ฐ\"))\n", " if rng.random() < missing: # ๊ฒฐ์ธก\n", " qty, cause = None, \"๊ฒฐ์ธก\"\n", " if cause:\n", " truth_diff[(s, p, u)] = cause\n", " out[s].append({\"part\": FMT[s][\"part\"](p), \"unit\": FMT[s][\"unit\"](u), \"date\": FMT[s][\"date\"](d), \"qty\": qty})\n", " return true, out, truth_diff\n", "\n", "\n", "def parse_part(code):\n", " return int(code[3:]) if code.startswith(\"AP-\") else int(code[:-2]) if code.endswith(\"-X\") else int(code[1:])\n", "\n", "\n", "def parse_unit(code):\n", " return int(code[1:]) if code.startswith(\"U\") else int(code[2:4]) if code.startswith(\"๊ฐ€์ƒ\") else int(code)\n", "\n", "\n", "def parse_date(s):\n", " if \"/\" in s:\n", " d, m, y = s.split(\"/\"); return datetime.date(int(y), int(m), int(d))\n", " if \"-\" in s:\n", " return datetime.date(*map(int, s.split(\"-\")))\n", " return datetime.date(int(s[:4]), int(s[4:6]), int(s[6:]))\n", "\n", "# ---------- ํ™”๋ฉด 1: ์ž…๋ ฅ ----------\n", "true, raw, tdiff = make_systems()\n", "rows = [{\"sys\": s, \"p\": parse_part(r[\"part\"]), \"u\": parse_unit(r[\"unit\"]), \"date\": parse_date(r[\"date\"]), \"qty\": r[\"qty\"]} for s in \"ABC\" for r in raw[s]]\n", "df = pd.DataFrame(rows)\n", "df[\"qty_f\"] = df[\"qty\"].astype(\"float\")\n", "print(f\"[ํ™”๋ฉด 1. ์ž…๋ ฅ] 3๊ฐœ ์ฒด๊ณ„ ๋ณด๊ณ  {len(df)}๊ฑด(์ฒด๊ณ„๋ณ„ {df['sys'].value_counts().sort_index().to_dict()}) ์ ์žฌ, ํ‘œ์ค€ ํ‚ค(ํ’ˆ๋ชฉ, ๋ถ€๋Œ€)๋กœ ํ†ตํ•ฉ [๊ต์œก์šฉ๊ฐ€์ƒ์ž๋ฃŒ]\")\n", "\n", "# ---------- ํ™”๋ฉด 2: ๋ถ„์„(๋ถˆ์ผ์น˜ ์‹๋ณ„) ----------\n", "w = df.pivot_table(index=[\"p\", \"u\"], columns=\"sys\", values=\"qty_f\")\n", "w[\"n\"] = w[[\"A\", \"B\", \"C\"]].notna().sum(axis=1)\n", "w[\"spread\"] = w[[\"A\", \"B\", \"C\"]].max(axis=1) - w[[\"A\", \"B\", \"C\"]].min(axis=1)\n", "w[\"consensus\"] = w[[\"A\", \"B\", \"C\"]].median(axis=1)\n", "w[\"mismatch\"] = (w[\"spread\"] > 0) | (w[\"n\"] < 3) # ๊ฐ’์ด ๋‹ค๋ฅด๊ฑฐ๋‚˜ ์–ด๋А ์ฒด๊ณ„์— ๊ฐ’์ด ์—†์œผ๋ฉด ๋ถˆ์ผ์น˜๋กœ ๋ณธ๋‹ค\n", "print(f\"[ํ™”๋ฉด 2. ๋ถ„์„] ํ‚ค {len(w)}๊ฐœ ์ค‘ ๋ถˆ์ผ์น˜ {int(w['mismatch'].sum())}๊ฐœ({w['mismatch'].mean():.1%}): ๊ฐ’ ๋ถˆ์ผ์น˜ {int((w['spread'] > 0).sum())}๊ฐœ, ๊ฒฐ์ธก ํฌํ•จ {int((w['n'] < 3).sum())}๊ฐœ\")\n", "big = w[w[\"spread\"] > 0.2 * w[\"consensus\"]]\n", "print(f\" ํŽธ์ฐจ 20% ์ดˆ๊ณผ(์šฐ์„  ๊ฒ€ํ†  ๋Œ€์ƒ) {len(big)}๊ฐœ\")\n", "\n", "# ---------- ํ’ˆ์งˆ ์ง€์ˆ˜ ----------\n", "def dims(g):\n", " comp = g[\"qty_f\"].notna().mean() # ์™„์ „์„ฑ: ๊ฐ’์ด ์žˆ๋Š” ๋น„์œจ\n", " key = g.set_index([\"p\", \"u\"]).index\n", " cons = (~w.loc[key, \"mismatch\"]).mean() if len(g) else 0 # ์ผ์น˜์„ฑ: ์ฒด๊ณ„ ๊ฐ„ ๊ฐ™์€ ๊ฐ’์ธ ๋น„์œจ\n", " tim = ((BASE_DATE - g[\"date\"]).map(lambda d: d.days) <= 0).mean() # ์ ์‹œ์„ฑ: ๊ธฐ์ค€์ผ ๋‹น์ผ ๋ณด๊ณ  ๋น„์œจ\n", " val = ((g[\"qty_f\"].isna()) | ((g[\"qty_f\"] >= 0) & (g[\"qty_f\"] <= 2000))).mean() # ์œ ํšจ์„ฑ: ํ—ˆ์šฉ ๋ฒ”์œ„(0~2000) ์•ˆ์˜ ๋น„์œจ\n", " return pd.Series({\"์™„์ „์„ฑ\": comp, \"์ผ์น˜์„ฑ\": cons, \"์ ์‹œ์„ฑ\": tim, \"์œ ํšจ์„ฑ\": val})\n", "\n", "\n", "WT = {\"์™„์ „์„ฑ\": 0.25, \"์ผ์น˜์„ฑ\": 0.4, \"์ ์‹œ์„ฑ\": 0.15, \"์œ ํšจ์„ฑ\": 0.2} # ์ผ์น˜์„ฑ ๋น„์ค‘์„ ๊ฐ€์žฅ ๋†’๊ฒŒ\n", "def index(g):\n", " d = dims(g)\n", " return pd.concat([d, pd.Series({\"ํ’ˆ์งˆ์ง€์ˆ˜\": 100 * sum(d[k] * v for k, v in WT.items())})])\n", "\n", "by_sys = pd.DataFrame({k: index(g) for k, g in df.groupby(\"sys\")}).T\n", "by_unit = pd.DataFrame({k: index(g) for k, g in df.groupby(\"u\")}).T\n", "\n", "# ---------- ํ™”๋ฉด 3: ๊ฒ€ํ†  ----------\n", "print(\"\\n[ํ™”๋ฉด 3. ๊ฒ€ํ† ] ์ฒด๊ณ„๋ณ„ ํ’ˆ์งˆ ์ง€์ˆ˜(0~100)\")\n", "print(by_sys.round(3).to_string())\n", "low = by_unit.sort_values(\"ํ’ˆ์งˆ์ง€์ˆ˜\").head(4)\n", "print(\"ํ’ˆ์งˆ ์ง€์ˆ˜๊ฐ€ ๋‚ฎ์€ ๋ถ€๋Œ€ 4๊ณณ(์šฐ์„  ์ ๊ฒ€):\")\n", "print(low.round(3).to_string())\n", "print(\"๊ฒ€ํ† ์ž๋Š” ๋ถˆ์ผ์น˜ ํ‚ค๋งˆ๋‹ค '๋งˆ์Šคํ„ฐ ํ›„๋ณด ์Šน์ธ ์š”์ฒญ / ์ •์ƒ ํ™•์ธ'์„ ์„ ํƒํ•˜๋ฉฐ, ์ˆ˜์ •์€ ์ฒด๊ณ„ ๋‹ด๋‹น์ž๊ฐ€ ํ•œ๋‹ค.\")\n", "\n", "# ---------- ํ™”๋ฉด 4: ๊ฒฐ๊ณผ ----------\n", "total = float(index(df)[\"ํ’ˆ์งˆ์ง€์ˆ˜\"])\n", "grade = \"์–‘ํ˜ธ\" if total >= 90 else \"๋ณด์™„ ํ•„์š”\" if total >= 75 else \"๋ฏธํก\"\n", "gt_keys = {(p, u) for (_, p, u) in tdiff}\n", "found = {k for k, m in w[\"mismatch\"].items() if m}\n", "recall = len(found & gt_keys) / len(gt_keys)\n", "print(f\"\\n[ํ™”๋ฉด 4. ๊ฒฐ๊ณผ] ํ†ตํ•ฉ ๋ฐ์ดํ„ฐ ํ’ˆ์งˆ ์ง€์ˆ˜ {total:.1f}์ ({grade}), ๋ถˆ์ผ์น˜ ์‹๋ณ„ ์žฌํ˜„์œจ(๊ฐ€์ƒ ์ •๋‹ต ๋Œ€๋น„) {recall:.1%}\")\n", "print(f\" ์ฐจ์›๋ณ„ ๊ฐ€์ค‘์น˜: {WT}\")\n", "ok = recall >= 0.95\n", "print(f\"๋ชฉํ‘œ(ํ•ต์‹ฌ ํ™”๋ฉด 4์ข… ์ž…๋ ฅ-๋ถ„์„-๊ฒ€ํ† -๊ฒฐ๊ณผ ๊ตฌํ˜„, ๋ถˆ์ผ์น˜ ์ž๋™ ์‹๋ณ„ยทํ’ˆ์งˆ ์ง€์ˆ˜ ์‚ฐ์ถœ): {'๋‹ฌ์„ฑ' if ok else '๋ฏธ๋‹ฌ'}\")\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "
\n", "๐Ÿ’ก [์ฐธ๊ณ ] ๋ฏธ์…˜ 05 ์˜ˆ์ƒ ์‹คํ–‰ ๊ฒฐ๊ณผ (Output)\n", "\n", "```text\n", "[ํ™”๋ฉด 1. ์ž…๋ ฅ] 3๊ฐœ ์ฒด๊ณ„ ๋ณด๊ณ  2160๊ฑด(์ฒด๊ณ„๋ณ„ {'A': 720, 'B': 720, 'C': 720}) ์ ์žฌ, ํ‘œ์ค€ ํ‚ค(ํ’ˆ๋ชฉ, ๋ถ€๋Œ€)๋กœ ํ†ตํ•ฉ [๊ต์œก์šฉ๊ฐ€์ƒ์ž๋ฃŒ]\n", "[ํ™”๋ฉด 2. ๋ถ„์„] ํ‚ค 720๊ฐœ ์ค‘ ๋ถˆ์ผ์น˜ 219๊ฐœ(30.4%): ๊ฐ’ ๋ถˆ์ผ์น˜ 179๊ฐœ, ๊ฒฐ์ธก ํฌํ•จ 43๊ฐœ\n", " ํŽธ์ฐจ 20% ์ดˆ๊ณผ(์šฐ์„  ๊ฒ€ํ†  ๋Œ€์ƒ) 95๊ฐœ\n", "\n", "[ํ™”๋ฉด 3. ๊ฒ€ํ† ] ์ฒด๊ณ„๋ณ„ ํ’ˆ์งˆ ์ง€์ˆ˜(0~100)\n", " ์™„์ „์„ฑ ์ผ์น˜์„ฑ ์ ์‹œ์„ฑ ์œ ํšจ์„ฑ ํ’ˆ์งˆ์ง€์ˆ˜\n", "A 0.975 0.696 0.962 0.978 86.201\n", "B 0.981 0.696 0.969 0.979 86.472\n", "C 0.981 0.696 0.967 0.974 86.319\n", "ํ’ˆ์งˆ ์ง€์ˆ˜๊ฐ€ ๋‚ฎ์€ ๋ถ€๋Œ€ 4๊ณณ(์šฐ์„  ์ ๊ฒ€):\n", " ์™„์ „์„ฑ ์ผ์น˜์„ฑ ์ ์‹œ์„ฑ ์œ ํšจ์„ฑ ํ’ˆ์งˆ์ง€์ˆ˜\n", "3 0.972 0.583 0.950 0.983 81.556\n", "5 0.972 0.633 0.983 0.983 84.056\n", "10 0.978 0.650 0.967 0.961 84.167\n", "6 0.978 0.667 0.956 0.972 84.889\n", "๊ฒ€ํ† ์ž๋Š” ๋ถˆ์ผ์น˜ ํ‚ค๋งˆ๋‹ค '๋งˆ์Šคํ„ฐ ํ›„๋ณด ์Šน์ธ ์š”์ฒญ / ์ •์ƒ ํ™•์ธ'์„ ์„ ํƒํ•˜๋ฉฐ, ์ˆ˜์ •์€ ์ฒด๊ณ„ ๋‹ด๋‹น์ž๊ฐ€ ํ•œ๋‹ค.\n", "\n", "[ํ™”๋ฉด 4. ๊ฒฐ๊ณผ] ํ†ตํ•ฉ ๋ฐ์ดํ„ฐ ํ’ˆ์งˆ ์ง€์ˆ˜ 86.3์ (๋ณด์™„ ํ•„์š”), ๋ถˆ์ผ์น˜ ์‹๋ณ„ ์žฌํ˜„์œจ(๊ฐ€์ƒ ์ •๋‹ต ๋Œ€๋น„) 100.0%\n", " ์ฐจ์›๋ณ„ ๊ฐ€์ค‘์น˜: {'์™„์ „์„ฑ': 0.25, '์ผ์น˜์„ฑ': 0.4, '์ ์‹œ์„ฑ': 0.15, '์œ ํšจ์„ฑ': 0.2}\n", "๋ชฉํ‘œ(ํ•ต์‹ฌ ํ™”๋ฉด 4์ข… ์ž…๋ ฅ-๋ถ„์„-๊ฒ€ํ† -๊ฒฐ๊ณผ ๊ตฌํ˜„, ๋ถˆ์ผ์น˜ ์ž๋™ ์‹๋ณ„ยทํ’ˆ์งˆ ์ง€์ˆ˜ ์‚ฐ์ถœ): ๋‹ฌ์„ฑ\n", "```\n", "
\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "---\n", "\n", "## ๐ŸŽฏ ๋ฏธ์…˜ 06. ์ง€ํœ˜๊ด€ ๋ฐ ๊ตฐ์ˆ˜์ฐธ๋ชจ์šฉ ํ†ตํ•ฉ ๊ตฐ์ˆ˜ ์ƒํ™ฉํŒ(Logistics COP) ์‹ค์‹œ๊ฐ„ ๋Œ€์‹œ๋ณด๋“œ ๊ตฌํ˜„\n", "**์†Œ์š”์‹œ๊ฐ„**: ์•ฝ 2.5์‹œ๊ฐ„\n", "\n", "### ๐Ÿ“‹ ๋ฏธ์…˜ ๋ฐฐ๊ฒฝ ๋ฐ ์ˆ˜ํ–‰ ์ง€์นจ\n", "#### ๋ฏธ์…˜ ๋ฐฐ๊ฒฝ ๋ฐ ๋‹น์œ„์„ฑ\n", "\n", "์ง€ํœ˜๊ด€ ๋ฐ ๊ตฐ์ˆ˜์ฐธ๋ชจ์šฉ ํ†ตํ•ฉ ๊ตฐ์ˆ˜ ์ƒํ™ฉํŒ(Logistics COP) ์‹ค์‹œ๊ฐ„ ๋Œ€์‹œ๋ณด๋“œ๋ฅผ ๊ตฌํ˜„ํ•œ๋‹ค. ์‹ค์ œ ์ „์ˆ  ํ˜„์žฅ์—์„œ๋Š” ๋‹จ์ˆœ ํƒ์ง€์— ๊ทธ์น˜์ง€ ์•Š๊ณ  ์‹ ์†ํ•œ ์œ„ํ˜‘ ํŒ๋‹จ๊ณผ ์‹ ๋ขฐ๋„ ๋†’์€ ์˜์‚ฌ๊ฒฐ์ •์ด ํ•„์ˆ˜์ ์ด๋ฏ€๋กœ, **์ง€ํœ˜๊ด€๊ณผ ๊ตฐ์ˆ˜์ฐธ๋ชจ๊ฐ€ ๋ณด๋Š” ํ†ตํ•ฉ ๊ตฐ์ˆ˜ ์ƒํ™ฉํŒ(Logistics COP)์„ ์‹œ๊ฐ„๋ณ„ ๊ฐฑ์‹  ๋ฐฉ์‹์œผ๋กœ ๊ตฌํ˜„ํ•˜๊ณ , ์˜ˆ์™ธ ์ƒํ™ฉ์—์„œ๋„ ์ค‘๋‹จ๋˜์ง€ ์•Š๋Š” ์—๋Ÿฌ ์ฒ˜๋ฆฌ๋ฅผ ๊ฐ–์ถ˜๋‹ค.** ๊ณผ์—…์„ ์™„์ˆ˜ํ•˜์—ฌ ์˜คํŒ์„ ๋ฐฉ์ง€ํ•˜๊ณ  ์ž‘์ „ ๋Œ€์‘๋ ฅ์„ ๊ทน๋Œ€ํ™”ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.\n", "\n", "์ด ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด ๋ณธ ๋ฏธ์…˜์—์„œ๋Š” **๋ณ€๊ฒฝ ์ด๋ฒคํŠธ๋ฅผ 1์‹œ๊ฐ„ ๋‹จ์œ„๋กœ ๋ฐ˜์˜ํ•ด ์ด์žฌ๊ณ , ๋ถ„๋ฅ˜๋ณ„ ์žฌ๊ณ , ๋ถ€์กฑ ๊ฒฝ๋ณด๋ฅผ ๊ฐฑ์‹ ํ•˜๊ณ  ์ง€ํœ˜๊ด€์šฉ ์š”์•ฝ ํ™”๋ฉด๊ณผ ๊ตฐ์ˆ˜์ฐธ๋ชจ์šฉ ์ƒ์„ธ ํ™”๋ฉด์„ ๊ตฌ๋ถ„ํ•ด ๋ณด์—ฌ ์ค€๋‹ค ๊ณผ์ •์„ ์ง„ํ–‰ํ•˜๋Š”** ์‹ค์Šต์„ ์ฒด๊ณ„์ ์œผ๋กœ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค.\n", "\n", "ํ•™์Šต์ž๋Š” ์‹ค์Šต๊ฐ€์ด๋“œ์— ์ œ์‹œ๋œ ์ด 4๊ฐœ ์„ธ๋ถ€ ํ™œ๋™(๊ธฐ๋ณธ ๊ธฐ๋Šฅ ๊ตฌํ˜„ โž” ๊ฒฐ๊ณผ ์ง€ํ‘œ ๊ฒ€์ฆ โž” ํŒŒ๋ผ๋ฏธํ„ฐ ํŠœ๋‹ ๋ฐ ๋„์ „๊ณผ์ œ)์„ ์ˆœ์ฐจ์ ์œผ๋กœ ์ง„ํ–‰ํ•˜๋ฉฐ ํ•ต์‹ฌ ํŒ์ • ๊ธฐ์ค€์„ ๋‹ฌ์„ฑํ•ฉ๋‹ˆ๋‹ค.\n", "\n", "**์ด๋ฒˆ ๊ฒฐ๊ณผ๋ฌผ:** `src/utils/dat09_validator.py ๋ฐ ์—๋Ÿฌ ํ•ธ๋“ค๋Ÿฌ` ํ˜•ํƒœ์˜ ๊ฒฐ๊ณผ๋ฌผ์„ ์™„์„ฑํ•œ๋‹ค.\n", "\n", "### ๐Ÿ” ํ‰๊ฐ€ ๋ฐ ๊ฒ€์ฆ ๊ธฐ์ค€ (PASS ์กฐ๊ฑด)\n", "#### ๋ฏธ์…˜ ํŒ์ •\n", "\n", "**ํ‰๊ฐ€ ๊ตฌ๋ถ„:** ํ•„์ˆ˜\n", "\n", "**ํ•ต์‹ฌ ํ‰๊ฐ€์˜์—ญ:** ์‚ฌ์šฉ์ž์—๊ฒŒ ์•Œ๊ธฐ ์‰ฌ์šด ์—๋Ÿฌ ๋ฉ”์‹œ์ง€ ๋ฐ ์กฐ์น˜ ๊ฐ€์ด๋“œ ์ œ๊ณต\n", "\n", "#### ๋ฏธ์…˜ PASS ์กฐ๊ฑด\n", "\n", "- ์ •๋Ÿ‰ ๊ธฐ์ค€: ์˜ˆ์™ธ ์ƒํ™ฉ ๋ฐœ์ƒ ์‹œ ํฌ๋ž˜์‹œ(Crash) 0๊ฑด, ์—๋Ÿฌ ๋กœ๊ทธ ๊ธฐ๋ก๋ฅ  100%\n", "\n", "\n", "- ์ง„ํ–‰ ๊ธฐ์ค€: ์˜ˆ์™ธ ์ฃผ์ž… ํ…Œ์ŠคํŠธ 10์ข… ํ†ต๊ณผ ๋ฐ ๊ฐ์‚ฌ ๋กœ๊ทธ ํŒŒ์ผ ์ƒ์„ฑ\n", "\n", "\n", "- ๊ฒ€์ฆ ๋ฐฉ๋ฒ•: ๋น„์ •์ƒ ๋ฐ์ดํ„ฐ(๊ฒฐ์ธก, ํŠน์ˆ˜๋ฌธ์ž, ๋Œ€์šฉ๋Ÿ‰) ์ŠคํŠธ๋ ˆ์Šค ํ…Œ์ŠคํŠธ\n", "\n", "\n", "#### ์ฑ„์ ์ž ํŒ์ • ๋ฉ”๋ชจ\n", "\n", "> ๋‹จ์ˆœ ์ฝ”๋“œ ์‹คํ–‰ ์—ฌ๋ถ€๋ฟ๋งŒ ์•„๋‹ˆ๋ผ ์ถœ๋ ฅ๋œ ์ˆ˜์น˜์™€ ๊ทผ๊ฑฐ ์ž๋ฃŒ๊ฐ€ ๊ตฐ ์‹ค๋ฌด ์š”๊ตฌ์‚ฌํ•ญ์— ๋ถ€ํ•ฉํ•˜๋Š”์ง€ ๋ฉด๋ฐ€ํžˆ ํ™•์ธํ•œ๋‹ค.\n", "\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "\"\"\"๋ณ€๊ฒฝ ์ด๋ฒคํŠธ๋ฅผ 1์‹œ๊ฐ„ ๋‹จ์œ„๋กœ ๋ฐ›์•„ ๊ฐฑ์‹ ๋˜๋Š” ํ†ตํ•ฉ ๊ตฐ์ˆ˜ ์ƒํ™ฉํŒ(์žฌ๊ณ ยท๋ถ€์กฑ ๊ฒฝ๋ณดยท๋ฐ์ดํ„ฐ ์‹ ๋ขฐ๋„)์„ ์ง€ํœ˜๊ด€์šฉยท๊ตฐ์ˆ˜์ฐธ๋ชจ์šฉ ์—ญํ• ๋ณ„ ํ™”๋ฉด๊ณผ ๊ทธ๋ž˜ํ”„๋กœ ๊ตฌํ˜„ํ•˜๊ณ , ์ž…๋ ฅ ๊ฒ€์ฆ๊ธฐ์™€ ์—๋Ÿฌ ํ•ธ๋“ค๋Ÿฌ๋กœ ์˜ˆ์™ธ 10์ข…์„ ํฌ๋ž˜์‹œ ์—†์ด ๊ธฐ๋กยท์•ˆ๋‚ดํ•œ๋‹ค.\"\"\"\n", "import datetime\n", "import json\n", "import numpy as np\n", "import pandas as pd\n", "import matplotlib.pyplot as plt\n", "\n", "# ---------------------------------------------------------------------------\n", "# [๊ต์œก์šฉ๊ฐ€์ƒ์ž๋ฃŒ] ๊ฐ€์ƒ ์ด๊ธฐ์ข… ๊ตฐ์ˆ˜์ •๋ณด์ฒด๊ณ„ 3์ข…(A ๋ณด๊ธ‰, B ์ •๋น„, C ํƒ„์•ฝ)์˜ ์žฌ๊ณ  ๋ณด๊ณ  ์ƒ์„ฑ๊ธฐ\n", "# ์‹ค์ œ ์ฒด๊ณ„๋ช…ยท๋ถ€๋Œ€ยทํ’ˆ๋ชฉยท์ˆ˜๋Ÿ‰ ์—†์Œ. ๊ฐ™์€ (ํ’ˆ๋ชฉ, ๋ถ€๋Œ€) ์žฌ๊ณ ๋ฅผ ์„ธ ์ฒด๊ณ„๊ฐ€ ์„œ๋กœ ๋‹ค๋ฅธ ์ฝ”๋“œยท๋‚ ์งœ ํ˜•์‹์œผ๋กœ ๋ณด๊ณ ํ•œ๋‹ค.\n", "# ๊ธฐ์ค€ ๋‚ ์งœ: 2026-09-30(๊ฐ€์ƒ). ๋งˆ์Šคํ„ฐ(์ •๋‹ต) ์žฌ๊ณ  TRUE ์™€ ์ฒด๊ณ„๋ณ„ ๋ณด๊ณ ์—๋Š” ์˜๋„์  ๋ถˆ์ผ์น˜ยท๊ฒฐ์ธก์ด ์„ž์—ฌ ์žˆ๋‹ค.\n", "# ---------------------------------------------------------------------------\n", "N_PART, N_UNIT = 60, 12\n", "CATS = [\"๊ธ‰์‹\", \"ํ”ผ๋ณต\", \"์œ ๋ฅ˜\", \"์ •๋น„์†Œ๋ชจ\", \"์ˆ˜๋ฆฌ๋ถ€์†\", \"ํƒ„์•ฝ\"]\n", "PARTS = {p: {\"name\": f\"๊ฐ€์ƒํ’ˆ๋ชฉ{p:02d}\", \"cat\": CATS[(p - 1) % 6]} for p in range(1, N_PART + 1)}\n", "FMT = {\"A\": dict(part=lambda p: f\"AP-{p:05d}\", unit=lambda u: f\"U{u:02d}\", date=lambda d: d.strftime(\"%Y%m%d\")),\n", " \"B\": dict(part=lambda p: f\"{p:05d}-X\", unit=lambda u: f\"๊ฐ€์ƒ{u:02d}๋Œ€๋Œ€\", date=lambda d: d.strftime(\"%Y-%m-%d\")),\n", " \"C\": dict(part=lambda p: f\"P{p}\", unit=lambda u: str(u), date=lambda d: d.strftime(\"%d/%m/%Y\"))}\n", "BASE_DATE = datetime.date(2026, 9, 30)\n", "\n", "\n", "def make_systems(seed=9, mismatch=0.10, missing=0.03):\n", " \"\"\"TRUE(์ •๋‹ต ์žฌ๊ณ )์™€ ์ฒด๊ณ„๋ณ„ ์›๋ณธ ๋ณด๊ณ  ๋ ˆ์ฝ”๋“œ(์ฒด๊ณ„ ๊ณ ์œ  ํ˜•์‹)๋ฅผ ๋งŒ๋“ ๋‹ค.\"\"\"\n", " rng = np.random.default_rng(seed)\n", " true = {(p, u): int(rng.integers(20, 500)) for p in PARTS for u in range(1, N_UNIT + 1)}\n", " out = {s: [] for s in \"ABC\"}\n", " truth_diff = {} # (์ฒด๊ณ„, p, u) -> ๋ถˆ์ผ์น˜ ์›์ธ(์ •๋‹ต)\n", " for (p, u), q in true.items():\n", " for s in \"ABC\":\n", " qty, cause = q, None\n", " r = rng.random()\n", " if r < mismatch / 3: # ์ž…๋ ฅ ์˜ค๋ฅ˜(์ž๋ฆฟ์ˆ˜/์˜คํƒ€)\n", " qty, cause = q + int(rng.choice([-1, 1]) * rng.integers(10, 60)), \"์ž…๋ ฅ ์˜ค๋ฅ˜\"\n", " elif r < 2 * mismatch / 3: # ๋ฐ˜์˜ ์ง€์—ฐ(์ „์ผ ๊ฐ’)\n", " qty, cause = q + int(rng.integers(5, 40)), \"๋ฐ˜์˜ ์ง€์—ฐ\"\n", " elif r < mismatch: # ๋‹จ์œ„ ์˜ค๋ฅ˜(10๋ฐฐ)\n", " qty, cause = q * 10, \"๋‹จ์œ„ ์˜ค๋ฅ˜\"\n", " d = BASE_DATE - datetime.timedelta(days=int(cause == \"๋ฐ˜์˜ ์ง€์—ฐ\"))\n", " if rng.random() < missing: # ๊ฒฐ์ธก\n", " qty, cause = None, \"๊ฒฐ์ธก\"\n", " if cause:\n", " truth_diff[(s, p, u)] = cause\n", " out[s].append({\"part\": FMT[s][\"part\"](p), \"unit\": FMT[s][\"unit\"](u), \"date\": FMT[s][\"date\"](d), \"qty\": qty})\n", " return true, out, truth_diff\n", "\n", "\n", "def parse_part(code):\n", " return int(code[3:]) if code.startswith(\"AP-\") else int(code[:-2]) if code.endswith(\"-X\") else int(code[1:])\n", "\n", "\n", "def parse_unit(code):\n", " return int(code[1:]) if code.startswith(\"U\") else int(code[2:4]) if code.startswith(\"๊ฐ€์ƒ\") else int(code)\n", "\n", "\n", "def parse_date(s):\n", " if \"/\" in s:\n", " d, m, y = s.split(\"/\"); return datetime.date(int(y), int(m), int(d))\n", " if \"-\" in s:\n", " return datetime.date(*map(int, s.split(\"-\")))\n", " return datetime.date(int(s[:4]), int(s[4:6]), int(s[6:]))\n", "\n", "true, raw, tdiff = make_systems()\n", "rng = np.random.default_rng(6)\n", "STATE = {(p, u): true[(p, u)] for p in PARTS for u in range(1, N_UNIT + 1)} # ์ƒํ™ฉํŒ์˜ ํ˜„์žฌ ์žฌ๊ณ (์ฒด๊ณ„ A ๊ธฐ์ค€ ์‹œ์ž‘)\n", "SEEN, LOG, HIST = set(), [], []\n", "LOW = 40 # ๋ถ€์กฑ ๊ฒฝ๋ณด ๊ธฐ์ค€(๊ฐ€์ƒ)\n", "\n", "\n", "class AppError(Exception):\n", " def __init__(self, code, msg, guide):\n", " super().__init__(msg); self.code, self.msg, self.guide = code, msg, guide\n", "\n", "\n", "def validate(ev):\n", " for k in (\"id\", \"sys\", \"p\", \"u\", \"delta\"):\n", " if k not in ev or ev[k] is None:\n", " raise AppError(\"E01\", f\"ํ•„์ˆ˜ ํ•ญ๋ชฉ '{k}' ๋ˆ„๋ฝ\", f\"์ด๋ฒคํŠธ์— '{k}' ๊ฐ’์„ ๋„ฃ์–ด ๋‹ค์‹œ ์ „์†ก\")\n", " if ev[\"id\"] in SEEN:\n", " raise AppError(\"E02\", f\"์ค‘๋ณต ์ด๋ฒคํŠธ {ev['id']}\", \"์ด๋ฏธ ๋ฐ˜์˜๋œ ์ด๋ฒคํŠธ์ด๋ฏ€๋กœ ๋ฌด์‹œํ•ด๋„ ๋จ\")\n", " if ev[\"sys\"] not in (\"A\", \"B\", \"C\"):\n", " raise AppError(\"E03\", f\"์•Œ ์ˆ˜ ์—†๋Š” ์ฒด๊ณ„ '{ev['sys']}'\", \"์—ฐ๋™ ์ฒด๊ณ„(AยทBยทC)๋งŒ ํ—ˆ์šฉ\")\n", " try:\n", " p, u, dl = int(ev[\"p\"]), int(ev[\"u\"]), float(ev[\"delta\"])\n", " except (TypeError, ValueError):\n", " raise AppError(\"E04\", \"์ˆซ์ž ํ˜•์‹ ์˜ค๋ฅ˜\", \"ํ’ˆ๋ชฉยท๋ถ€๋Œ€ยท์ฆ๊ฐ๋Ÿ‰์€ ์ˆซ์ž๋งŒ ์ž…๋ ฅ\")\n", " if not np.isfinite(dl):\n", " raise AppError(\"E05\", \"์ฆ๊ฐ๋Ÿ‰์ด NaN ๋˜๋Š” ๋ฌดํ•œ๋Œ€\", \"์ •์ƒ ์ˆซ์ž๋กœ ์ˆ˜์ •\")\n", " if (p, u) not in STATE:\n", " raise AppError(\"E06\", f\"์กด์žฌํ•˜์ง€ ์•Š๋Š” ํ‚ค(ํ’ˆ๋ชฉ {p}, ๋ถ€๋Œ€ {u})\", \"์ฝ”๋“œ ๋ณ€ํ™˜ํ‘œ์—์„œ ํ‚ค ํ™•์ธ\")\n", " if abs(dl) > 5000:\n", " raise AppError(\"E09\", f\"์ฆ๊ฐ๋Ÿ‰ {dl:+.0f}์ด ํ—ˆ์šฉ ๋ฒ”์œ„(ยฑ5000) ์ดˆ๊ณผ\", \"์ž๋ฆฟ์ˆ˜ ์˜ค๋ฅ˜ ์—ฌ๋ถ€๋ฅผ ์ถœ์ฒ˜ ์ฒด๊ณ„์— ํ™•์ธ\")\n", " if STATE[(p, u)] + dl < 0:\n", " raise AppError(\"E07\", f\"์žฌ๊ณ ๊ฐ€ ์Œ์ˆ˜๊ฐ€ ๋จ({STATE[(p, u)]} + {dl:+.0f})\", \"์ถœ์ฒ˜ ์ฒด๊ณ„์— ์ฆ๊ฐ๋Ÿ‰ ์žฌํ™•์ธ ์š”์ฒญ\")\n", " return p, u, dl\n", "\n", "\n", "def apply(ev):\n", " p, u, dl = validate(ev)\n", " STATE[(p, u)] += int(dl); SEEN.add(ev[\"id\"])\n", "\n", "\n", "def safe(fn, *a):\n", " \"\"\"์—๋Ÿฌ ํ•ธ๋“ค๋Ÿฌ: ๋ชจ๋“  ์˜ˆ์™ธ๋ฅผ ๋กœ๊ทธ๋กœ ๋‚จ๊ธฐ๊ณ  ์‚ฌ์šฉ์ž์—๊ฒŒ ์›์ธ๊ณผ ์กฐ์น˜๋ฅผ ์•ˆ๋‚ดํ•œ๋‹ค(ํฌ๋ž˜์‹œ ์—†์Œ).\"\"\"\n", " try:\n", " r = fn(*a); LOG.append({\"level\": \"INFO\", \"code\": \"OK\"}); return r\n", " except AppError as e:\n", " LOG.append({\"level\": \"ERROR\", \"code\": e.code, \"msg\": e.msg, \"guide\": e.guide})\n", " except Exception as e:\n", " LOG.append({\"level\": \"ERROR\", \"code\": \"E99\", \"msg\": repr(e)[:50], \"guide\": \"์šด์˜ ๋‹ด๋‹น์ž์—๊ฒŒ ๋กœ๊ทธ ์ „๋‹ฌ\"})\n", "\n", "\n", "def view(role):\n", " \"\"\"์—ญํ• ๋ณ„ ํ™”๋ฉด: ์ง€ํœ˜๊ด€์€ ์š”์•ฝ, ๊ตฐ์ˆ˜์ฐธ๋ชจ๋Š” ๋ถ„๋ฅ˜ยท๋ถ€๋Œ€ ์ƒ์„ธ.\"\"\"\n", " if role not in (\"์ง€ํœ˜๊ด€\", \"๊ตฐ์ˆ˜์ฐธ๋ชจ\"):\n", " raise AppError(\"E08\", f\"๊ถŒํ•œ ์—†๋Š” ์—ญํ•  '{role}'\", \"์ง€ํœ˜๊ด€ ๋˜๋Š” ๊ตฐ์ˆ˜์ฐธ๋ชจ ๊ณ„์ •์œผ๋กœ ์ ‘์†\")\n", " s = pd.Series(STATE)\n", " total, low = int(s.sum()), int((s < LOW).sum())\n", " if role == \"์ง€ํœ˜๊ด€\":\n", " return f\"์ด์žฌ๊ณ  {total}, ๋ถ€์กฑ ๊ฒฝ๋ณด {low}๊ฑด\"\n", " cat = {c: int(sum(v for (p, u), v in STATE.items() if PARTS[p][\"cat\"] == c)) for c in CATS}\n", " worst = s.groupby(level=1).apply(lambda g: int((g < LOW).sum())).sort_values(ascending=False).head(3)\n", " return f\"์ด์žฌ๊ณ  {total}, ๋ถ„๋ฅ˜๋ณ„ {cat}, ๋ถ€์กฑ ๊ฒฝ๋ณด {low}๊ฑด, ๊ฒฝ๋ณด ๋งŽ์€ ๋ถ€๋Œ€ {worst.to_dict()}\"\n", "\n", "\n", "# 1) ์ •์ƒ ์šด์˜: 6์‹œ๊ฐ„ ๋™์•ˆ ์‹œ๊ฐ„๋‹น ์ด๋ฒคํŠธ 80๊ฑด(๋ถˆ์ถœ -, ๋ณด๊ธ‰ +) ๋ฐ˜์˜\n", "print(\"[ํ†ตํ•ฉ ๊ตฐ์ˆ˜ ์ƒํ™ฉํŒ - ์‹œ๊ฐ„๋ณ„ ๊ฐฑ์‹ ] [๊ต์œก์šฉ๊ฐ€์ƒ์ž๋ฃŒ]\")\n", "for hour in range(6):\n", " for i in range(80):\n", " p, u = int(rng.integers(1, N_PART + 1)), int(rng.integers(1, N_UNIT + 1))\n", " d = int(rng.integers(-30, 25))\n", " safe(apply, {\"id\": f\"EV{hour}-{i}\", \"sys\": \"ABC\"[i % 3], \"p\": p, \"u\": u, \"delta\": d})\n", " s = pd.Series(STATE)\n", " cat_tot = {c: sum(v for (p, u), v in STATE.items() if PARTS[p][\"cat\"] == c) for c in CATS}\n", " HIST.append({\"hour\": hour + 8, \"total\": int(s.sum()), \"low\": int((s < LOW).sum()), **cat_tot})\n", " print(f\" {hour + 8:02d}:00 ์ง€ํœ˜๊ด€ -> {view('์ง€ํœ˜๊ด€')}\")\n", "rej = [l for l in LOG if l[\"level\"] == \"ERROR\"]\n", "print(f\"์ •์ƒ ์šด์˜ ์ค‘ ๊ฑฐ๋ถ€๋œ ์ด๋ฒคํŠธ {len(rej)}๊ฑด(์žฌ๊ณ  ์Œ์ˆ˜ ๋ณดํ˜ธ ๋“ฑ):\", sorted({l['code'] for l in rej}))\n", "print(\"๊ตฐ์ˆ˜์ฐธ๋ชจ ํ™”๋ฉด:\", view(\"๊ตฐ์ˆ˜์ฐธ๋ชจ\"))\n", "\n", "# 2) ์˜ˆ์™ธ ์ฃผ์ž… 10์ข…\n", "LOG.clear()\n", "base = {\"id\": \"T-OK\", \"sys\": \"A\", \"p\": 1, \"u\": 1, \"delta\": 1}\n", "safe(apply, base) # ๋จผ์ € ์ •์ƒ ๋ฐ˜์˜ํ•ด ๋‘์–ด์•ผ ๊ฐ™์€ ๋ฒˆํ˜ธ ์žฌ์ „์†ก์ด ์ค‘๋ณต์ด ๋œ๋‹ค(์‹œํ—˜ ๋Œ€์ƒ ์•„๋‹˜)\n", "LOG.clear()\n", "cases = [(\"ํ•„์ˆ˜ ํ•ญ๋ชฉ ๋ˆ„๋ฝ\", {\"id\": \"T1\", \"sys\": \"A\", \"p\": 1, \"u\": 1}), (\"์ค‘๋ณต ์ด๋ฒคํŠธ\", base),\n", " (\"์•Œ ์ˆ˜ ์—†๋Š” ์ฒด๊ณ„\", {**base, \"id\": \"T3\", \"sys\": \"Z\"}), (\"์ˆซ์ž ์•„๋‹˜\", {**base, \"id\": \"T4\", \"delta\": \"10๊ฐœ\"}),\n", " (\"NaN ๊ฐ’\", {**base, \"id\": \"T5\", \"delta\": float(\"nan\")}), (\"์—†๋Š” ํ‚ค\", {**base, \"id\": \"T6\", \"p\": 999}),\n", " (\"์ฆ๊ฐ๋Ÿ‰ ๊ณผ๋Œ€\", {**base, \"id\": \"T7\", \"delta\": -99999}), (\"์žฌ๊ณ  ์Œ์ˆ˜ํ™”\", {**base, \"id\": \"T8\", \"delta\": -4000}),\n", " (\"๊ถŒํ•œ ์—†๋Š” ์—ญํ• \", \"๋ณ‘์‚ฌ\"), (\"๋นˆ ์ด๋ฒคํŠธ\", {})]\n", "crash = 0\n", "for n, c in cases:\n", " try:\n", " safe(view, c) if isinstance(c, str) else safe(apply, c)\n", " except BaseException:\n", " crash += 1\n", "errs = [l for l in LOG if l[\"level\"] == \"ERROR\"]\n", "print(\"\\n[์˜ˆ์™ธ ์ฃผ์ž… ๊ฒฐ๊ณผ - ์‚ฌ์šฉ์ž ์•ˆ๋‚ด ๋ฉ”์‹œ์ง€]\")\n", "for (n, c), l in zip(cases, errs):\n", " print(f\" {l['code']} {n:<14} {l['msg']:<32} -> ์กฐ์น˜: {l['guide']}\")\n", "exp_errors = len(cases)\n", "rate = len(errs) / exp_errors\n", "print(f\"์˜ˆ์™ธ {exp_errors}์ข… ์ค‘ ๋กœ๊ทธ ๊ธฐ๋ก {len(errs)}๊ฑด(๊ธฐ๋ก๋ฅ  {rate:.0%}), ํฌ๋ž˜์‹œ {crash}๊ฑด\")\n", "print(\"๋กœ๊ทธ ์˜ˆ์‹œ:\", json.dumps(errs[0], ensure_ascii=False))\n", "print(f\"๋ชฉํ‘œ(ํฌ๋ž˜์‹œ 0๊ฑด, ์—๋Ÿฌ ๋กœ๊ทธ ๊ธฐ๋ก๋ฅ  100%): {'๋‹ฌ์„ฑ' if crash == 0 and rate == 1 else '๋ฏธ๋‹ฌ'}\")\n", "\n", "h = pd.DataFrame(HIST)\n", "fig, ax = plt.subplots(2, 2, figsize=(11, 7))\n", "for c in CATS:\n", " ax[0, 0].plot(h[\"hour\"], h[c], marker=\"o\", label=f\"cat{CATS.index(c) + 1}\")\n", "ax[0, 0].legend(fontsize=7, ncol=3); ax[0, 0].set_title(\"Stock by category (hourly)\")\n", "ax[0, 1].plot(h[\"hour\"], h[\"low\"], color=\"tab:red\", marker=\"s\"); ax[0, 1].set_title(\"Shortage alerts (stock < 40)\")\n", "s = pd.Series(STATE)\n", "ax[1, 0].bar(range(1, N_UNIT + 1), s.groupby(level=1).sum().values, color=\"tab:blue\"); ax[1, 0].set_title(\"Total stock by unit (latest)\")\n", "ax[1, 1].hist(s.values, bins=20, color=\"tab:gray\"); ax[1, 1].axvline(LOW, color=\"red\", ls=\"--\"); ax[1, 1].set_title(\"Stock distribution (red = alert line)\")\n", "plt.suptitle(\"DAT09 logistics COP (synthetic)\"); plt.tight_layout(); plt.show()\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "
\n", "๐Ÿ’ก [์ฐธ๊ณ ] ๋ฏธ์…˜ 06 ์˜ˆ์ƒ ์‹คํ–‰ ๊ฒฐ๊ณผ (Output)\n", "\n", "```text\n", "[ํ†ตํ•ฉ ๊ตฐ์ˆ˜ ์ƒํ™ฉํŒ - ์‹œ๊ฐ„๋ณ„ ๊ฐฑ์‹ ] [๊ต์œก์šฉ๊ฐ€์ƒ์ž๋ฃŒ]\n", " 08:00 ์ง€ํœ˜๊ด€ -> ์ด์žฌ๊ณ  188532, ๋ถ€์กฑ ๊ฒฝ๋ณด 27๊ฑด\n", " 09:00 ์ง€ํœ˜๊ด€ -> ์ด์žฌ๊ณ  188232, ๋ถ€์กฑ ๊ฒฝ๋ณด 28๊ฑด\n", " 10:00 ์ง€ํœ˜๊ด€ -> ์ด์žฌ๊ณ  188292, ๋ถ€์กฑ ๊ฒฝ๋ณด 26๊ฑด\n", " 11:00 ์ง€ํœ˜๊ด€ -> ์ด์žฌ๊ณ  187942, ๋ถ€์กฑ ๊ฒฝ๋ณด 29๊ฑด\n", " 12:00 ์ง€ํœ˜๊ด€ -> ์ด์žฌ๊ณ  187663, ๋ถ€์กฑ ๊ฒฝ๋ณด 27๊ฑด\n", " 13:00 ์ง€ํœ˜๊ด€ -> ์ด์žฌ๊ณ  187444, ๋ถ€์กฑ ๊ฒฝ๋ณด 28๊ฑด\n", "์ •์ƒ ์šด์˜ ์ค‘ ๊ฑฐ๋ถ€๋œ ์ด๋ฒคํŠธ 0๊ฑด(์žฌ๊ณ  ์Œ์ˆ˜ ๋ณดํ˜ธ ๋“ฑ): []\n", "๊ตฐ์ˆ˜์ฐธ๋ชจ ํ™”๋ฉด: ์ด์žฌ๊ณ  187444, ๋ถ„๋ฅ˜๋ณ„ {'๊ธ‰์‹': 30500, 'ํ”ผ๋ณต': 34149, '์œ ๋ฅ˜': 30600, '์ •๋น„์†Œ๋ชจ': 30650, '์ˆ˜๋ฆฌ๋ถ€์†': 31493, 'ํƒ„์•ฝ': 30052}, ๋ถ€์กฑ ๊ฒฝ๋ณด 28๊ฑด, ๊ฒฝ๋ณด ๋งŽ์€ ๋ถ€๋Œ€ {1: 4, 3: 4, 8: 4}\n", "\n", "[์˜ˆ์™ธ ์ฃผ์ž… ๊ฒฐ๊ณผ - ์‚ฌ์šฉ์ž ์•ˆ๋‚ด ๋ฉ”์‹œ์ง€]\n", " E01 ํ•„์ˆ˜ ํ•ญ๋ชฉ ๋ˆ„๋ฝ ํ•„์ˆ˜ ํ•ญ๋ชฉ 'delta' ๋ˆ„๋ฝ -> ์กฐ์น˜: ์ด๋ฒคํŠธ์— 'delta' ๊ฐ’์„ ๋„ฃ์–ด ๋‹ค์‹œ ์ „์†ก\n", " E02 ์ค‘๋ณต ์ด๋ฒคํŠธ ์ค‘๋ณต ์ด๋ฒคํŠธ T-OK -> ์กฐ์น˜: ์ด๋ฏธ ๋ฐ˜์˜๋œ ์ด๋ฒคํŠธ์ด๋ฏ€๋กœ ๋ฌด์‹œํ•ด๋„ ๋จ\n", " E03 ์•Œ ์ˆ˜ ์—†๋Š” ์ฒด๊ณ„ ์•Œ ์ˆ˜ ์—†๋Š” ์ฒด๊ณ„ 'Z' -> ์กฐ์น˜: ์—ฐ๋™ ์ฒด๊ณ„(AยทBยทC)๋งŒ ํ—ˆ์šฉ\n", " E04 ์ˆซ์ž ์•„๋‹˜ ์ˆซ์ž ํ˜•์‹ ์˜ค๋ฅ˜ -> ์กฐ์น˜: ํ’ˆ๋ชฉยท๋ถ€๋Œ€ยท์ฆ๊ฐ๋Ÿ‰์€ ์ˆซ์ž๋งŒ ์ž…๋ ฅ\n", " E05 NaN ๊ฐ’ ์ฆ๊ฐ๋Ÿ‰์ด NaN ๋˜๋Š” ๋ฌดํ•œ๋Œ€ -> ์กฐ์น˜: ์ •์ƒ ์ˆซ์ž๋กœ ์ˆ˜์ •\n", " E06 ์—†๋Š” ํ‚ค ์กด์žฌํ•˜์ง€ ์•Š๋Š” ํ‚ค(ํ’ˆ๋ชฉ 999, ๋ถ€๋Œ€ 1) -> ์กฐ์น˜: ์ฝ”๋“œ ๋ณ€ํ™˜ํ‘œ์—์„œ ํ‚ค ํ™•์ธ\n", " E09 ์ฆ๊ฐ๋Ÿ‰ ๊ณผ๋Œ€ ์ฆ๊ฐ๋Ÿ‰ -99999์ด ํ—ˆ์šฉ ๋ฒ”์œ„(ยฑ5000) ์ดˆ๊ณผ -> ์กฐ์น˜: ์ž๋ฆฟ์ˆ˜ ์˜ค๋ฅ˜ ์—ฌ๋ถ€๋ฅผ ์ถœ์ฒ˜ ์ฒด๊ณ„์— ํ™•์ธ\n", " E07 ์žฌ๊ณ  ์Œ์ˆ˜ํ™” ์žฌ๊ณ ๊ฐ€ ์Œ์ˆ˜๊ฐ€ ๋จ(216 + -4000) -> ์กฐ์น˜: ์ถœ์ฒ˜ ์ฒด๊ณ„์— ์ฆ๊ฐ๋Ÿ‰ ์žฌํ™•์ธ ์š”์ฒญ\n", " E08 ๊ถŒํ•œ ์—†๋Š” ์—ญํ•  ๊ถŒํ•œ ์—†๋Š” ์—ญํ•  '๋ณ‘์‚ฌ' -> ์กฐ์น˜: ์ง€ํœ˜๊ด€ ๋˜๋Š” ๊ตฐ์ˆ˜์ฐธ๋ชจ ๊ณ„์ •์œผ๋กœ ์ ‘์†\n", " E01 ๋นˆ ์ด๋ฒคํŠธ ํ•„์ˆ˜ ํ•ญ๋ชฉ 'id' ๋ˆ„๋ฝ -> ์กฐ์น˜: ์ด๋ฒคํŠธ์— 'id' ๊ฐ’์„ ๋„ฃ์–ด ๋‹ค์‹œ ์ „์†ก\n", "์˜ˆ์™ธ 10์ข… ์ค‘ ๋กœ๊ทธ ๊ธฐ๋ก 10๊ฑด(๊ธฐ๋ก๋ฅ  100%), ํฌ๋ž˜์‹œ 0๊ฑด\n", "๋กœ๊ทธ ์˜ˆ์‹œ: {\"level\": \"ERROR\", \"code\": \"E01\", \"msg\": \"ํ•„์ˆ˜ ํ•ญ๋ชฉ 'delta' ๋ˆ„๋ฝ\", \"guide\": \"์ด๋ฒคํŠธ์— 'delta' ๊ฐ’์„ ๋„ฃ์–ด ๋‹ค์‹œ ์ „์†ก\"}\n", "๋ชฉํ‘œ(ํฌ๋ž˜์‹œ 0๊ฑด, ์—๋Ÿฌ ๋กœ๊ทธ ๊ธฐ๋ก๋ฅ  100%): ๋‹ฌ์„ฑ\n", "```\n", "
\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "---\n", "\n", "## ๐ŸŽฏ ๋ฏธ์…˜ 07. ์ฒด๊ณ„ ๊ฐ„ ๋ฐ์ดํ„ฐ ์ถฉ๋Œ ์‹œ ๋งˆ์Šคํ„ฐ ๋ฐ์ดํ„ฐ ํ™•์ •์„ ์œ„ํ•œ ๋ฐ์ดํ„ฐ๊ด€๋ฆฌ๊ด€ HITL ์Šน์ธ ์„ค๊ณ„\n", "**์†Œ์š”์‹œ๊ฐ„**: ์•ฝ 3.0์‹œ๊ฐ„\n", "\n", "### ๐Ÿ“‹ ๋ฏธ์…˜ ๋ฐฐ๊ฒฝ ๋ฐ ์ˆ˜ํ–‰ ์ง€์นจ\n", "#### ๋ฏธ์…˜ ๋ฐฐ๊ฒฝ ๋ฐ ๋‹น์œ„์„ฑ\n", "\n", "๋ฐ์ดํ„ฐ ์ถฉ๋Œ ์‹œ ๋งˆ์Šคํ„ฐ ๋ฐ์ดํ„ฐ ํ™•์ •์„ ์œ„ํ•œ ๋ฐ์ดํ„ฐ๊ด€๋ฆฌ๊ด€ HITL ์Šน์ธ์„ ์„ค๊ณ„ํ•œ๋‹ค. ์‹ค์ œ ์ „์ˆ  ํ˜„์žฅ์—์„œ๋Š” ๋‹จ์ˆœ ํƒ์ง€์— ๊ทธ์น˜์ง€ ์•Š๊ณ  ์‹ ์†ํ•œ ์œ„ํ˜‘ ํŒ๋‹จ๊ณผ ์‹ ๋ขฐ๋„ ๋†’์€ ์˜์‚ฌ๊ฒฐ์ •์ด ํ•„์ˆ˜์ ์ด๋ฏ€๋กœ, **์ฒด๊ณ„ ๊ฐ„ ๋ฐ์ดํ„ฐ ์ถฉ๋Œ ์‹œ ์‹œ์Šคํ…œ์€ ํ›„๋ณด๋งŒ ์ œ์•ˆํ•˜๊ณ  ๋ฐ์ดํ„ฐ๊ด€๋ฆฌ๊ด€ ์Šน์ธ์œผ๋กœ๋งŒ ๋งˆ์Šคํ„ฐ ๋ฐ์ดํ„ฐ๋ฅผ ํ™•์ •ํ•˜๋Š” ์‚ฌ๋žŒ ์Šน์ธ ์ฒด๊ณ„๋ฅผ ์„ค๊ณ„ํ•œ๋‹ค.** ๊ณผ์—…์„ ์™„์ˆ˜ํ•˜์—ฌ ์˜คํŒ์„ ๋ฐฉ์ง€ํ•˜๊ณ  ์ž‘์ „ ๋Œ€์‘๋ ฅ์„ ๊ทน๋Œ€ํ™”ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.\n", "\n", "์ด ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด ๋ณธ ๋ฏธ์…˜์—์„œ๋Š” **์ถฉ๋Œ ๊ฑด๋งˆ๋‹ค ๋‹ค์ˆ˜๊ฐ’ ํ›„๋ณด์™€ ๊ทผ๊ฑฐ๋ฅผ ์ œ์•ˆ์œผ๋กœ ๋“ฑ๋กํ•˜๊ณ , ๋ฐ์ดํ„ฐ๊ด€๋ฆฌ๊ด€์ด ๊ทผ๊ฑฐ๋ฅผ ์ ์–ด ์Šน์ธํ•˜๊ฑฐ๋‚˜ ๋ฐ˜๋ คํ•˜๊ฒŒ ์ˆ˜ํ–‰ํ•˜๊ณ  ๊ฒ€์ฆํ•˜๋Š”** ์‹ค์Šต์„ ์ฒด๊ณ„์ ์œผ๋กœ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค.\n", "\n", "ํ•™์Šต์ž๋Š” ์‹ค์Šต๊ฐ€์ด๋“œ์— ์ œ์‹œ๋œ ์ด 4๊ฐœ ์„ธ๋ถ€ ํ™œ๋™(๊ธฐ๋ณธ ๊ธฐ๋Šฅ ๊ตฌํ˜„ โž” ๊ฒฐ๊ณผ ์ง€ํ‘œ ๊ฒ€์ฆ โž” ํŒŒ๋ผ๋ฏธํ„ฐ ํŠœ๋‹ ๋ฐ ๋„์ „๊ณผ์ œ)์„ ์ˆœ์ฐจ์ ์œผ๋กœ ์ง„ํ–‰ํ•˜๋ฉฐ ํ•ต์‹ฌ ํŒ์ • ๊ธฐ์ค€์„ ๋‹ฌ์„ฑํ•ฉ๋‹ˆ๋‹ค.\n", "\n", "**์ด๋ฒˆ ๊ฒฐ๊ณผ๋ฌผ:** `src/workflow/dat09_approval_system.py` ํ˜•ํƒœ์˜ ๊ฒฐ๊ณผ๋ฌผ์„ ์™„์„ฑํ•œ๋‹ค.\n", "\n", "### ๐Ÿ” ํ‰๊ฐ€ ๋ฐ ๊ฒ€์ฆ ๊ธฐ์ค€ (PASS ์กฐ๊ฑด)\n", "#### ๋ฏธ์…˜ ํŒ์ •\n", "\n", "**ํ‰๊ฐ€ ๊ตฌ๋ถ„:** ํ•„์ˆ˜\n", "\n", "**ํ•ต์‹ฌ ํ‰๊ฐ€์˜์—ญ:** ๊ตฐ ๋ณด์•ˆ ๊ทœ์ • ๋ฐ ํ–‰์ • ์ ˆ์ฐจ ๋ถ€ํ•ฉ์„ฑ\n", "\n", "#### ๋ฏธ์…˜ PASS ์กฐ๊ฑด\n", "\n", "- ์ •๋Ÿ‰ ๊ธฐ์ค€: ์ด๋ ฅ ๊ธฐ๋ก ๋ฌด๊ฒฐ์„ฑ 100%, ์Šน์ธ ๊ถŒํ•œ ์šฐํšŒ ์‹œ๋„ ์ฐจ๋‹จ์œจ 100%\n", "\n", "\n", "- ์ง„ํ–‰ ๊ธฐ์ค€: ๊ฒฐ์žฌ์„  ์Šน์ธ/๋ฐ˜๋ ค ์‹œ๋ฎฌ๋ ˆ์ด์…˜ ๋ฐ ์ด๋ ฅ ์กฐํšŒ ํ™”๋ฉด ๊ตฌํ˜„\n", "\n", "\n", "- ๊ฒ€์ฆ ๋ฐฉ๋ฒ•: ๊ถŒํ•œ๋ณ„ ์ ‘๊ทผ ์ œ์–ด(RBAC) ํ…Œ์ŠคํŠธ\n", "\n", "\n", "#### ์ฑ„์ ์ž ํŒ์ • ๋ฉ”๋ชจ\n", "\n", "> ๋‹จ์ˆœ ์ฝ”๋“œ ์‹คํ–‰ ์—ฌ๋ถ€๋ฟ๋งŒ ์•„๋‹ˆ๋ผ ์ถœ๋ ฅ๋œ ์ˆ˜์น˜์™€ ๊ทผ๊ฑฐ ์ž๋ฃŒ๊ฐ€ ๊ตฐ ์‹ค๋ฌด ์š”๊ตฌ์‚ฌํ•ญ์— ๋ถ€ํ•ฉํ•˜๋Š”์ง€ ๋ฉด๋ฐ€ํžˆ ํ™•์ธํ•œ๋‹ค.\n", "\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "\"\"\"์ฒด๊ณ„ ๊ฐ„ ์ˆ˜๋Ÿ‰ ์ถฉ๋Œ์ด ์ƒ๊ธฐ๋ฉด ์‹œ์Šคํ…œ์ด ๋งˆ์Šคํ„ฐ ๊ฐ’ ํ›„๋ณด์™€ ๊ทผ๊ฑฐ๋ฅผ ์ œ์•ˆํ•˜๊ณ  ๋ฐ์ดํ„ฐ๊ด€๋ฆฌ๊ด€์ด ์Šน์ธํ•ด์•ผ๋งŒ ๋งˆ์Šคํ„ฐ๋ฅผ ํ™•์ •ํ•˜๋Š” ์‚ฌ๋žŒ ์Šน์ธ ์ฒด๊ณ„๋ฅผ ์„ค๊ณ„ํ•˜๋ฉฐ(๊ณ ์˜ํ–ฅ ๊ฑด์€ ๊ตฐ์ˆ˜์ฐธ๋ชจ ์ถ”๊ฐ€ ํ™•์ธ), ํ•ด์‹œ ์ฒด์ธ ๊ฐ์‚ฌ ์ด๋ ฅ์˜ ๋ฌด๊ฒฐ์„ฑ๊ณผ ๊ถŒํ•œ ์šฐํšŒ ์ฐจ๋‹จ์„ ๊ฒ€์ฆํ•œ๋‹ค.\"\"\"\n", "import collections\n", "import datetime\n", "import hashlib\n", "import json\n", "import numpy as np\n", "import pandas as pd\n", "\n", "# ---------------------------------------------------------------------------\n", "# [๊ต์œก์šฉ๊ฐ€์ƒ์ž๋ฃŒ] ๊ฐ€์ƒ ์ด๊ธฐ์ข… ๊ตฐ์ˆ˜์ •๋ณด์ฒด๊ณ„ 3์ข…(A ๋ณด๊ธ‰, B ์ •๋น„, C ํƒ„์•ฝ)์˜ ์žฌ๊ณ  ๋ณด๊ณ  ์ƒ์„ฑ๊ธฐ\n", "# ์‹ค์ œ ์ฒด๊ณ„๋ช…ยท๋ถ€๋Œ€ยทํ’ˆ๋ชฉยท์ˆ˜๋Ÿ‰ ์—†์Œ. ๊ฐ™์€ (ํ’ˆ๋ชฉ, ๋ถ€๋Œ€) ์žฌ๊ณ ๋ฅผ ์„ธ ์ฒด๊ณ„๊ฐ€ ์„œ๋กœ ๋‹ค๋ฅธ ์ฝ”๋“œยท๋‚ ์งœ ํ˜•์‹์œผ๋กœ ๋ณด๊ณ ํ•œ๋‹ค.\n", "# ๊ธฐ์ค€ ๋‚ ์งœ: 2026-09-30(๊ฐ€์ƒ). ๋งˆ์Šคํ„ฐ(์ •๋‹ต) ์žฌ๊ณ  TRUE ์™€ ์ฒด๊ณ„๋ณ„ ๋ณด๊ณ ์—๋Š” ์˜๋„์  ๋ถˆ์ผ์น˜ยท๊ฒฐ์ธก์ด ์„ž์—ฌ ์žˆ๋‹ค.\n", "# ---------------------------------------------------------------------------\n", "N_PART, N_UNIT = 60, 12\n", "CATS = [\"๊ธ‰์‹\", \"ํ”ผ๋ณต\", \"์œ ๋ฅ˜\", \"์ •๋น„์†Œ๋ชจ\", \"์ˆ˜๋ฆฌ๋ถ€์†\", \"ํƒ„์•ฝ\"]\n", "PARTS = {p: {\"name\": f\"๊ฐ€์ƒํ’ˆ๋ชฉ{p:02d}\", \"cat\": CATS[(p - 1) % 6]} for p in range(1, N_PART + 1)}\n", "FMT = {\"A\": dict(part=lambda p: f\"AP-{p:05d}\", unit=lambda u: f\"U{u:02d}\", date=lambda d: d.strftime(\"%Y%m%d\")),\n", " \"B\": dict(part=lambda p: f\"{p:05d}-X\", unit=lambda u: f\"๊ฐ€์ƒ{u:02d}๋Œ€๋Œ€\", date=lambda d: d.strftime(\"%Y-%m-%d\")),\n", " \"C\": dict(part=lambda p: f\"P{p}\", unit=lambda u: str(u), date=lambda d: d.strftime(\"%d/%m/%Y\"))}\n", "BASE_DATE = datetime.date(2026, 9, 30)\n", "\n", "\n", "def make_systems(seed=9, mismatch=0.10, missing=0.03):\n", " \"\"\"TRUE(์ •๋‹ต ์žฌ๊ณ )์™€ ์ฒด๊ณ„๋ณ„ ์›๋ณธ ๋ณด๊ณ  ๋ ˆ์ฝ”๋“œ(์ฒด๊ณ„ ๊ณ ์œ  ํ˜•์‹)๋ฅผ ๋งŒ๋“ ๋‹ค.\"\"\"\n", " rng = np.random.default_rng(seed)\n", " true = {(p, u): int(rng.integers(20, 500)) for p in PARTS for u in range(1, N_UNIT + 1)}\n", " out = {s: [] for s in \"ABC\"}\n", " truth_diff = {} # (์ฒด๊ณ„, p, u) -> ๋ถˆ์ผ์น˜ ์›์ธ(์ •๋‹ต)\n", " for (p, u), q in true.items():\n", " for s in \"ABC\":\n", " qty, cause = q, None\n", " r = rng.random()\n", " if r < mismatch / 3: # ์ž…๋ ฅ ์˜ค๋ฅ˜(์ž๋ฆฟ์ˆ˜/์˜คํƒ€)\n", " qty, cause = q + int(rng.choice([-1, 1]) * rng.integers(10, 60)), \"์ž…๋ ฅ ์˜ค๋ฅ˜\"\n", " elif r < 2 * mismatch / 3: # ๋ฐ˜์˜ ์ง€์—ฐ(์ „์ผ ๊ฐ’)\n", " qty, cause = q + int(rng.integers(5, 40)), \"๋ฐ˜์˜ ์ง€์—ฐ\"\n", " elif r < mismatch: # ๋‹จ์œ„ ์˜ค๋ฅ˜(10๋ฐฐ)\n", " qty, cause = q * 10, \"๋‹จ์œ„ ์˜ค๋ฅ˜\"\n", " d = BASE_DATE - datetime.timedelta(days=int(cause == \"๋ฐ˜์˜ ์ง€์—ฐ\"))\n", " if rng.random() < missing: # ๊ฒฐ์ธก\n", " qty, cause = None, \"๊ฒฐ์ธก\"\n", " if cause:\n", " truth_diff[(s, p, u)] = cause\n", " out[s].append({\"part\": FMT[s][\"part\"](p), \"unit\": FMT[s][\"unit\"](u), \"date\": FMT[s][\"date\"](d), \"qty\": qty})\n", " return true, out, truth_diff\n", "\n", "\n", "def parse_part(code):\n", " return int(code[3:]) if code.startswith(\"AP-\") else int(code[:-2]) if code.endswith(\"-X\") else int(code[1:])\n", "\n", "\n", "def parse_unit(code):\n", " return int(code[1:]) if code.startswith(\"U\") else int(code[2:4]) if code.startswith(\"๊ฐ€์ƒ\") else int(code)\n", "\n", "\n", "def parse_date(s):\n", " if \"/\" in s:\n", " d, m, y = s.split(\"/\"); return datetime.date(int(y), int(m), int(d))\n", " if \"-\" in s:\n", " return datetime.date(*map(int, s.split(\"-\")))\n", " return datetime.date(int(s[:4]), int(s[4:6]), int(s[6:]))\n", "\n", "true, raw, tdiff = make_systems()\n", "# [๊ต์œก์šฉ๊ฐ€์ƒ์ž๋ฃŒ] ๊ฐ€์ƒ ์ง์ฑ…ยท์ธ๋ฌผ(๊ฐ€๋ช…)\n", "USERS = {\"sys_bot\": \"์‹œ์Šคํ…œ\", \"kim\": \"์ฒด๊ณ„๋‹ด๋‹น์ž\", \"lee\": \"์ฒด๊ณ„๋‹ด๋‹น์ž\", \"park\": \"๋ฐ์ดํ„ฐ๊ด€๋ฆฌ๊ด€\", \"choi\": \"๊ตฐ์ˆ˜์ฐธ๋ชจ\", \"han\": \"์—ด๋žŒ์ž\"}\n", "MASTER, PROPOSALS, AUDIT = {}, {}, []\n", "\n", "def h(prev, rec):\n", " return hashlib.sha256((prev + json.dumps(rec, ensure_ascii=False, sort_keys=True, default=str)).encode()).hexdigest()[:16]\n", "\n", "def log(key, user, action, result, note=\"\"):\n", " rec = {\"seq\": len(AUDIT) + 1, \"key\": str(key), \"user\": user, \"role\": USERS.get(user, \"๋ฏธ๋“ฑ๋ก\"), \"action\": action, \"result\": result, \"note\": note}\n", " rec[\"hash\"] = h(AUDIT[-1][\"hash\"] if AUDIT else \"GENESIS\", {k: v for k, v in rec.items() if k != \"hash\"})\n", " AUDIT.append(rec)\n", "\n", "def verify():\n", " prev, ok = \"GENESIS\", 0\n", " for r in AUDIT:\n", " ok += r[\"hash\"] == h(prev, {k: v for k, v in r.items() if k != \"hash\"}); prev = r[\"hash\"]\n", " return ok, len(AUDIT)\n", "\n", "def block(key, user, action, why):\n", " log(key, user, action, \"์ฐจ๋‹จ\", why); return False\n", "\n", "def propose(key, user, cand, evidence, old):\n", " if USERS.get(user) not in (\"์‹œ์Šคํ…œ\", \"์ฒด๊ณ„๋‹ด๋‹น์ž\"):\n", " return block(key, user, \"์ œ์•ˆ\", \"์ œ์•ˆ ๊ถŒํ•œ ์—†์Œ\")\n", " high = abs(cand - old) >= 100 or abs(cand - old) / max(old, 1) >= 0.5 # ๊ณ ์˜ํ–ฅ ๊ธฐ์ค€(๊ฐ€์ƒ)\n", " PROPOSALS[key] = {\"cand\": cand, \"old\": old, \"by\": user, \"high\": high, \"staff_ok\": False, \"status\": \"๋Œ€๊ธฐ\"}\n", " log(key, user, \"์ œ์•ˆ\", \"์ ‘์ˆ˜\", f\"ํ›„๋ณด {cand}, ๊ทผ๊ฑฐ {evidence}\"); return True\n", "\n", "def staff_confirm(key, user, note):\n", " p = PROPOSALS.get(key)\n", " if USERS.get(user) != \"๊ตฐ์ˆ˜์ฐธ๋ชจ\" or p is None or not p[\"high\"]:\n", " return block(key, user, \"์ฐธ๋ชจํ™•์ธ\", \"๊ณ ์˜ํ–ฅ ๊ฑด์— ๋Œ€ํ•œ ๊ตฐ์ˆ˜์ฐธ๋ชจ๋งŒ ๊ฐ€๋Šฅ\")\n", " if len(note.strip()) < 5:\n", " return block(key, user, \"์ฐธ๋ชจํ™•์ธ\", \"ํ™•์ธ ์˜๊ฒฌ 5์ž ์ด์ƒ ํ•„์š”\")\n", " p[\"staff_ok\"] = True; log(key, user, \"์ฐธ๋ชจํ™•์ธ\", \"์™„๋ฃŒ\", note); return True\n", "\n", "def approve(key, user, note):\n", " p = PROPOSALS.get(key)\n", " if p is None:\n", " return block(key, user, \"์Šน์ธ\", \"์ œ์•ˆ ์—†๋Š” ํ‚ค\")\n", " if p[\"status\"] != \"๋Œ€๊ธฐ\":\n", " return block(key, user, \"์Šน์ธ\", f\"์ด๋ฏธ ์ฒ˜๋ฆฌ๋จ({p['status']})\")\n", " if USERS.get(user) != \"๋ฐ์ดํ„ฐ๊ด€๋ฆฌ๊ด€\":\n", " return block(key, user, \"์Šน์ธ\", f\"๊ถŒํ•œ ์—†์Œ: ์—ญํ•  '{USERS.get(user, '๋ฏธ๋“ฑ๋ก')}'์€(๋Š”) ๋งˆ์Šคํ„ฐ ํ™•์ • ๋ถˆ๊ฐ€(์ž๋™ ํ™•์ • ๊ธˆ์ง€)\")\n", " if p[\"by\"] == user:\n", " return block(key, user, \"์Šน์ธ\", \"์ œ์•ˆ์ž ๋ณธ์ธ ์Šน์ธ ๊ธˆ์ง€\")\n", " if len(note.strip()) < 5:\n", " return block(key, user, \"์Šน์ธ\", \"์Šน์ธ ๊ทผ๊ฑฐ 5์ž ์ด์ƒ ํ•„์š”\")\n", " if p[\"high\"] and not p[\"staff_ok\"]:\n", " return block(key, user, \"์Šน์ธ\", \"๊ณ ์˜ํ–ฅ ๊ฑด์€ ๊ตฐ์ˆ˜์ฐธ๋ชจ ํ™•์ธ ์„ ํ–‰ ํ•„์š”\")\n", " MASTER[key] = p[\"cand\"]; p[\"status\"] = \"ํ™•์ •\"; log(key, user, \"์Šน์ธ\", \"ํ™•์ •\", note); return True\n", "\n", "def reject(key, user, note):\n", " p = PROPOSALS.get(key)\n", " if USERS.get(user) != \"๋ฐ์ดํ„ฐ๊ด€๋ฆฌ๊ด€\" or p is None or p[\"status\"] != \"๋Œ€๊ธฐ\" or len(note.strip()) < 5:\n", " return block(key, user, \"๋ฐ˜๋ ค\", \"๋ฐ˜๋ ค ์กฐ๊ฑด ๋ถˆ์ถฉ์กฑ\")\n", " p[\"status\"] = \"๋ฐ˜๋ ค\"; log(key, user, \"๋ฐ˜๋ ค\", \"๋ฐ˜๋ ค\", note); return True\n", "\n", "# 1) ์ถฉ๋Œ ํ‚ค ์ฐพ๊ธฐ: ์„ธ ์ฒด๊ณ„ ๊ฐ’์ด ์„œ๋กœ ๋‹ค๋ฅด๋ฉด ๋‹ค์ˆ˜๊ฐ’์„ ํ›„๋ณด๋กœ ์ œ์•ˆ\n", "vals = collections.defaultdict(dict)\n", "for s in \"ABC\":\n", " for r in raw[s]:\n", " vals[(parse_part(r[\"part\"]), parse_unit(r[\"unit\"]))][s] = r[\"qty\"]\n", "confl = []\n", "for k, v in vals.items():\n", " have = [x for x in v.values() if x is not None]\n", " if len(set(have)) > 1:\n", " top, n = collections.Counter(have).most_common(1)[0]\n", " if n >= 2:\n", " confl.append((k, top, v[\"A\"] if v[\"A\"] is not None else top))\n", "confl = sorted(confl)[:24]\n", "print(f\"[์ถฉ๋Œ ๊ฑด] ๋‹ค์ˆ˜๊ฐ’ ํ›„๋ณด๊ฐ€ ์žˆ๋Š” ์ถฉ๋Œ {len(confl)}๊ฑด์„ ์ฒ˜๋ฆฌ ๋Œ€์ƒ์œผ๋กœ ์„ ์ • [๊ต์œก์šฉ๊ฐ€์ƒ์ž๋ฃŒ]\")\n", "\n", "for i, (k, cand, old) in enumerate(confl):\n", " propose(k, \"sys_bot\" if i % 2 else \"kim\", cand, \"๋‹ค์ˆ˜ ์ฒด๊ณ„ ์ผ์น˜\", old)\n", " if PROPOSALS[k][\"high\"]:\n", " staff_confirm(k, \"choi\", \"์ˆ˜๋Ÿ‰ ์ฐจ์ด๊ฐ€ ์ปค์„œ ์ถœ์ฒ˜ ์ฒด๊ณ„ ๋Œ€์กฐ ํ›„ ํ™•์ธ\")\n", " if i == 3:\n", " reject(k, \"park\", \"ํ›„๋ณด๊ฐ€ ์˜ค๋ž˜๋œ ๋ณด๊ณ ์™€ ๊ฐ™์•„ ๋ณด๋ฅ˜ ์š”์ฒญ\"); continue\n", " approve(k, \"park\", \"์ฒด๊ณ„ ๊ฐ„ ๋‹ค์ˆ˜๊ฐ’ ๋ฐ ์›์ธ ๋ถ„๋ฅ˜ ๊ฒ€ํ†  ์™„๋ฃŒ\")\n", "acc = np.mean([MASTER[k] == true[k] for k in MASTER])\n", "print(f\"ํ™•์ • {len(MASTER)}๊ฑด, ๋ฐ˜๋ ค {sum(p['status'] == '๋ฐ˜๋ ค' for p in PROPOSALS.values())}๊ฑด, ๊ณ ์˜ํ–ฅ(๊ตฐ์ˆ˜์ฐธ๋ชจ ํ™•์ธ ํ•„์š”) {sum(p['high'] for p in PROPOSALS.values())}๊ฑด, ํ™•์ • ๊ฐ’์ด ๊ฐ€์ƒ ์ •๋‹ต๊ณผ ์ผ์น˜ {acc:.0%}\")\n", "\n", "# 2) ๊ถŒํ•œ ์šฐํšŒ ์‹œ๋„ 10์ข…\n", "k0 = confl[0][0]\n", "pend = confl[-1][0]\n", "PROPOSALS[pend].update(status=\"๋Œ€๊ธฐ\"); MASTER.pop(pend, None)\n", "hi = next(k for k, p in PROPOSALS.items() if p[\"high\"]) if any(p[\"high\"] for p in PROPOSALS.values()) else pend\n", "PROPOSALS[hi].update(status=\"๋Œ€๊ธฐ\", staff_ok=False); MASTER.pop(hi, None)\n", "total = blocked = 0\n", "print(\"\\n[๊ถŒํ•œ ์šฐํšŒ ์‹œ๋„ ๊ฒฐ๊ณผ]\")\n", "def attempt(name, fn):\n", " global total, blocked\n", " n = len(AUDIT); fn(); r = AUDIT[-1]\n", " ok = len(AUDIT) == n + 1 and r[\"result\"] == \"์ฐจ๋‹จ\"\n", " total += 1; blocked += ok\n", " print(f\" - {name}: {r['result']} ({r['note']})\")\n", "attempt(\"์‹œ์Šคํ…œ์ด ์ž๋™์œผ๋กœ ๋งˆ์Šคํ„ฐ ํ™•์ •\", lambda: approve(pend, \"sys_bot\", \"์ž๋™ ์ฒ˜๋ฆฌ ๊ฒฐ๊ณผ๋ฅผ ํ™•์ •ํ•จ\"))\n", "attempt(\"์ฒด๊ณ„๋‹ด๋‹น์ž๊ฐ€ ์ง์ ‘ ํ™•์ •\", lambda: approve(pend, \"lee\", \"์ œ๊ฐ€ ์ง์ ‘ ํ™•์ •ํ•ฉ๋‹ˆ๋‹ค ํ™•์ธ\"))\n", "attempt(\"์—ด๋žŒ์ž๊ฐ€ ํ™•์ •\", lambda: approve(pend, \"han\", \"์—ด๋žŒ์ž ๊ถŒํ•œ์œผ๋กœ ํ™•์ • ์‹œ๋„\"))\n", "attempt(\"๋ฏธ๋“ฑ๋ก ์‚ฌ์šฉ์ž ํ™•์ •\", lambda: approve(pend, \"hacker\", \"์™ธ๋ถ€ ๊ณ„์ • ํ™•์ • ์‹œ๋„ ํ™•์ธ\"))\n", "attempt(\"์Šน์ธ ๊ทผ๊ฑฐ ์—†์ด ํ™•์ •\", lambda: approve(pend, \"park\", \"\"))\n", "attempt(\"์ œ์•ˆ ์—†๋Š” ํ‚ค ํ™•์ •\", lambda: approve((99, 99), \"park\", \"์กด์žฌํ•˜์ง€ ์•Š๋Š” ์ œ์•ˆ ์Šน์ธ ์‹œ๋„\"))\n", "attempt(\"๊ณ ์˜ํ–ฅ ๊ฑด์„ ๊ตฐ์ˆ˜์ฐธ๋ชจ ํ™•์ธ ์—†์ด ํ™•์ •\", lambda: approve(hi, \"park\", \"์ฐธ๋ชจ ํ™•์ธ ๋‹จ๊ณ„๋ฅผ ๊ฑด๋„ˆ๋›ฐ๊ณ  ์Šน์ธ\"))\n", "attempt(\"์ œ์•ˆ์ž ๋ณธ์ธ(๊ด€๋ฆฌ๊ด€)์ด ์Šน์ธ\", lambda: (PROPOSALS.update({pend: {**PROPOSALS[pend], \"by\": \"park\"}}), approve(pend, \"park\", \"๋ณธ์ธ ์ œ์•ˆ์„ ๋ณธ์ธ์ด ์Šน์ธ\"))[1])\n", "attempt(\"์ฒด๊ณ„๋‹ด๋‹น์ž๊ฐ€ ์ฐธ๋ชจํ™•์ธ ๋Œ€ํ–‰\", lambda: staff_confirm(hi, \"kim\", \"์ฐธ๋ชจ ๋Œ€์‹  ํ™•์ธํ–ˆ์Šต๋‹ˆ๋‹ค ํ™•์ธ\"))\n", "PROPOSALS[pend].update(by=\"kim\"); approve(pend, \"park\", \"๋‹ค์ˆ˜ ์ฒด๊ณ„ ์ผ์น˜ ํ™•์ธ ํ›„ ์ •์ƒ ์Šน์ธ\")\n", "attempt(\"์ด๋ฏธ ํ™•์ •๋œ ๊ฑด ์žฌ์Šน์ธ\", lambda: approve(pend, \"park\", \"์ด๋ฏธ ํ™•์ •๋œ ๊ฑด์„ ๋‹ค์‹œ ์Šน์ธ\"))\n", "print(f\"์šฐํšŒ ์‹œ๋„ {total}๊ฑด ์ค‘ ์ฐจ๋‹จ {blocked}๊ฑด({blocked / total:.0%}), ์ž๋™ ํ™•์ • 0๊ฑด\")\n", "\n", "# 3) ๊ฐ์‚ฌ ์ด๋ ฅ ๋ฌด๊ฒฐ์„ฑ + ์œ„์กฐ ์‹œํ—˜\n", "ok, tot = verify()\n", "print(f\"\\n๊ฐ์‚ฌ ์ด๋ ฅ {tot}๊ฑด ํ•ด์‹œ ์ฒด์ธ ๊ฒ€์ฆ {ok}/{tot}({ok / tot:.0%})\")\n", "j = next(i for i, r in enumerate(AUDIT) if r[\"result\"] == \"์ฐจ๋‹จ\")\n", "keep = AUDIT[j][\"result\"]; AUDIT[j][\"result\"] = \"ํ™•์ •\"\n", "ok2, _ = verify(); AUDIT[j][\"result\"] = keep\n", "print(f\"์œ„์กฐ ์‹œํ—˜: {j + 1}๋ฒˆ ๊ธฐ๋ก ๋ณ€์กฐ -> ๊ฒ€์ฆ {ok2}/{tot} (๋ณ€์กฐ ํƒ์ง€ {'์„ฑ๊ณต' if ok2 < tot else '์‹คํŒจ'}), ์›๋ณต ํ›„ {verify()[0]}/{tot}\")\n", "df = pd.DataFrame(AUDIT)\n", "print(\"[ํ–‰์œ„๋ณ„ ๊ฒฐ๊ณผ ์š”์•ฝ]\")\n", "print(df.groupby([\"action\", \"result\"]).size().to_string())\n", "print(f\"๋ชฉํ‘œ(์ด๋ ฅ ๋ฌด๊ฒฐ์„ฑ 100%, ์šฐํšŒ ์ฐจ๋‹จ์œจ 100%): {'๋‹ฌ์„ฑ' if ok == tot and blocked == total and ok2 < tot else '๋ฏธ๋‹ฌ'}\")\n", "print(\"๋งˆ์Šคํ„ฐ ํ™•์ •์€ ๋ฐ์ดํ„ฐ๊ด€๋ฆฌ๊ด€ ์Šน์ธ์œผ๋กœ๋งŒ ๊ฐ€๋Šฅํ•˜๋‹ค. ์‹ค์ œ ๋ณด์•ˆยทํ–‰์ • ์ ˆ์ฐจ๋Š” ๋ถ€๋Œ€ ๊ทœ์ •๊ณผ ์ง€ํœ˜๊ณ„ํ†ต์— ๋”ฐ๋ฅธ๋‹ค(ํ•™์Šต์šฉ ๋ชจ์˜).\")\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "
\n", "๐Ÿ’ก [์ฐธ๊ณ ] ๋ฏธ์…˜ 07 ์˜ˆ์ƒ ์‹คํ–‰ ๊ฒฐ๊ณผ (Output)\n", "\n", "```text\n", "[์ถฉ๋Œ ๊ฑด] ๋‹ค์ˆ˜๊ฐ’ ํ›„๋ณด๊ฐ€ ์žˆ๋Š” ์ถฉ๋Œ 24๊ฑด์„ ์ฒ˜๋ฆฌ ๋Œ€์ƒ์œผ๋กœ ์„ ์ • [๊ต์œก์šฉ๊ฐ€์ƒ์ž๋ฃŒ]\n", "ํ™•์ • 23๊ฑด, ๋ฐ˜๋ ค 1๊ฑด, ๊ณ ์˜ํ–ฅ(๊ตฐ์ˆ˜์ฐธ๋ชจ ํ™•์ธ ํ•„์š”) 4๊ฑด, ํ™•์ • ๊ฐ’์ด ๊ฐ€์ƒ ์ •๋‹ต๊ณผ ์ผ์น˜ 100%\n", "\n", "[๊ถŒํ•œ ์šฐํšŒ ์‹œ๋„ ๊ฒฐ๊ณผ]\n", " - ์‹œ์Šคํ…œ์ด ์ž๋™์œผ๋กœ ๋งˆ์Šคํ„ฐ ํ™•์ •: ์ฐจ๋‹จ (๊ถŒํ•œ ์—†์Œ: ์—ญํ•  '์‹œ์Šคํ…œ'์€(๋Š”) ๋งˆ์Šคํ„ฐ ํ™•์ • ๋ถˆ๊ฐ€(์ž๋™ ํ™•์ • ๊ธˆ์ง€))\n", " - ์ฒด๊ณ„๋‹ด๋‹น์ž๊ฐ€ ์ง์ ‘ ํ™•์ •: ์ฐจ๋‹จ (๊ถŒํ•œ ์—†์Œ: ์—ญํ•  '์ฒด๊ณ„๋‹ด๋‹น์ž'์€(๋Š”) ๋งˆ์Šคํ„ฐ ํ™•์ • ๋ถˆ๊ฐ€(์ž๋™ ํ™•์ • ๊ธˆ์ง€))\n", " - ์—ด๋žŒ์ž๊ฐ€ ํ™•์ •: ์ฐจ๋‹จ (๊ถŒํ•œ ์—†์Œ: ์—ญํ•  '์—ด๋žŒ์ž'์€(๋Š”) ๋งˆ์Šคํ„ฐ ํ™•์ • ๋ถˆ๊ฐ€(์ž๋™ ํ™•์ • ๊ธˆ์ง€))\n", " - ๋ฏธ๋“ฑ๋ก ์‚ฌ์šฉ์ž ํ™•์ •: ์ฐจ๋‹จ (๊ถŒํ•œ ์—†์Œ: ์—ญํ•  '๋ฏธ๋“ฑ๋ก'์€(๋Š”) ๋งˆ์Šคํ„ฐ ํ™•์ • ๋ถˆ๊ฐ€(์ž๋™ ํ™•์ • ๊ธˆ์ง€))\n", " - ์Šน์ธ ๊ทผ๊ฑฐ ์—†์ด ํ™•์ •: ์ฐจ๋‹จ (์Šน์ธ ๊ทผ๊ฑฐ 5์ž ์ด์ƒ ํ•„์š”)\n", " - ์ œ์•ˆ ์—†๋Š” ํ‚ค ํ™•์ •: ์ฐจ๋‹จ (์ œ์•ˆ ์—†๋Š” ํ‚ค)\n", " - ๊ณ ์˜ํ–ฅ ๊ฑด์„ ๊ตฐ์ˆ˜์ฐธ๋ชจ ํ™•์ธ ์—†์ด ํ™•์ •: ์ฐจ๋‹จ (๊ณ ์˜ํ–ฅ ๊ฑด์€ ๊ตฐ์ˆ˜์ฐธ๋ชจ ํ™•์ธ ์„ ํ–‰ ํ•„์š”)\n", " - ์ œ์•ˆ์ž ๋ณธ์ธ(๊ด€๋ฆฌ๊ด€)์ด ์Šน์ธ: ์ฐจ๋‹จ (์ œ์•ˆ์ž ๋ณธ์ธ ์Šน์ธ ๊ธˆ์ง€)\n", " - ์ฒด๊ณ„๋‹ด๋‹น์ž๊ฐ€ ์ฐธ๋ชจํ™•์ธ ๋Œ€ํ–‰: ์ฐจ๋‹จ (๊ณ ์˜ํ–ฅ ๊ฑด์— ๋Œ€ํ•œ ๊ตฐ์ˆ˜์ฐธ๋ชจ๋งŒ ๊ฐ€๋Šฅ)\n", " - ์ด๋ฏธ ํ™•์ •๋œ ๊ฑด ์žฌ์Šน์ธ: ์ฐจ๋‹จ (์ด๋ฏธ ์ฒ˜๋ฆฌ๋จ(ํ™•์ •))\n", "์šฐํšŒ ์‹œ๋„ 10๊ฑด ์ค‘ ์ฐจ๋‹จ 10๊ฑด(100%), ์ž๋™ ํ™•์ • 0๊ฑด\n", "\n", "๊ฐ์‚ฌ ์ด๋ ฅ 63๊ฑด ํ•ด์‹œ ์ฒด์ธ ๊ฒ€์ฆ 63/63(100%)\n", "์œ„์กฐ ์‹œํ—˜: 53๋ฒˆ ๊ธฐ๋ก ๋ณ€์กฐ -> ๊ฒ€์ฆ 62/63 (๋ณ€์กฐ ํƒ์ง€ ์„ฑ๊ณต), ์›๋ณต ํ›„ 63/63\n", "[ํ–‰์œ„๋ณ„ ๊ฒฐ๊ณผ ์š”์•ฝ]\n", "action result\n", "๋ฐ˜๋ ค ๋ฐ˜๋ ค 1\n", "์Šน์ธ ์ฐจ๋‹จ 9\n", " ํ™•์ • 24\n", "์ œ์•ˆ ์ ‘์ˆ˜ 24\n", "์ฐธ๋ชจํ™•์ธ ์™„๋ฃŒ 4\n", " ์ฐจ๋‹จ 1\n", "๋ชฉํ‘œ(์ด๋ ฅ ๋ฌด๊ฒฐ์„ฑ 100%, ์šฐํšŒ ์ฐจ๋‹จ์œจ 100%): ๋‹ฌ์„ฑ\n", "๋งˆ์Šคํ„ฐ ํ™•์ •์€ ๋ฐ์ดํ„ฐ๊ด€๋ฆฌ๊ด€ ์Šน์ธ์œผ๋กœ๋งŒ ๊ฐ€๋Šฅํ•˜๋‹ค. ์‹ค์ œ ๋ณด์•ˆยทํ–‰์ • ์ ˆ์ฐจ๋Š” ๋ถ€๋Œ€ ๊ทœ์ •๊ณผ ์ง€ํœ˜๊ณ„ํ†ต์— ๋”ฐ๋ฅธ๋‹ค(ํ•™์Šต์šฉ ๋ชจ์˜).\n", "```\n", "
\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "---\n", "\n", "## ๐ŸŽฏ ๋ฏธ์…˜ 08. ์ˆ˜๋งŒ ๊ฑด์˜ ์ด๊ธฐ์ข… ๊ตฐ์ˆ˜ ํŠธ๋žœ์žญ์…˜ ๋™์‹œ ์œ ์ž… ์‹œ๋ฎฌ๋ ˆ์ด์…˜ ๋ถ€ํ•˜ ํ…Œ์ŠคํŠธ\n", "**์†Œ์š”์‹œ๊ฐ„**: ์•ฝ 3.5์‹œ๊ฐ„\n", "\n", "### ๐Ÿ“‹ ๋ฏธ์…˜ ๋ฐฐ๊ฒฝ ๋ฐ ์ˆ˜ํ–‰ ์ง€์นจ\n", "#### ๋ฏธ์…˜ ๋ฐฐ๊ฒฝ ๋ฐ ๋‹น์œ„์„ฑ\n", "\n", "์ˆ˜๋งŒ ๊ฑด์˜ ์ด๊ธฐ์ข… ๊ตฐ์ˆ˜ ํŠธ๋žœ์žญ์…˜ ๋™์‹œ ์œ ์ž… ๋ถ€ํ•˜ ํ…Œ์ŠคํŠธ๋ฅผ ์ˆ˜ํ–‰ํ•œ๋‹ค. ์‹ค์ œ ์ „์ˆ  ํ˜„์žฅ์—์„œ๋Š” ๋‹จ์ˆœ ํƒ์ง€์— ๊ทธ์น˜์ง€ ์•Š๊ณ  ์‹ ์†ํ•œ ์œ„ํ˜‘ ํŒ๋‹จ๊ณผ ์‹ ๋ขฐ๋„ ๋†’์€ ์˜์‚ฌ๊ฒฐ์ •์ด ํ•„์ˆ˜์ ์ด๋ฏ€๋กœ, **์„ธ ์ฒด๊ณ„์—์„œ ์ˆ˜๋งŒ ๊ฑด์˜ ํŠธ๋žœ์žญ์…˜์ด ๋’ค์„ž์—ฌ ์œ ์ž…๋˜๋Š” ์ƒํ™ฉ์„ ๋ชจ์˜ํ•ด ๋ถ€ํ•˜ ํ…Œ์ŠคํŠธ๋ฅผ ์ˆ˜ํ–‰ํ•œ๋‹ค.** ๊ณผ์—…์„ ์™„์ˆ˜ํ•˜์—ฌ ์˜คํŒ์„ ๋ฐฉ์ง€ํ•˜๊ณ  ์ž‘์ „ ๋Œ€์‘๋ ฅ์„ ๊ทน๋Œ€ํ™”ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.\n", "\n", "์ด ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด ๋ณธ ๋ฏธ์…˜์—์„œ๋Š” **์ฒด๊ณ„๋ณ„ ์ฝ”๋“œ ํ˜•์‹์ด ๋‹ค๋ฅธ ํŠธ๋žœ์žญ์…˜ 5์ฒœ~5๋งŒ ๊ฑด์„ ๋งŒ๋“ค๊ณ  ์ค‘๋ณต 1%, ํ˜•์‹ ์˜ค๋ฅ˜ 0.4%, ์ผ์‹œ ์žฅ์•  ์ฃผ์ž…ํ•˜์—ฌ ์ฒ˜๋ฆฌ ์ง€์—ฐ ๋ฐ ๋ณต์›๋ ฅ์„ ์‹œํ—˜ํ•˜๋Š”** ์‹ค์Šต์„ ์ฒด๊ณ„์ ์œผ๋กœ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค.\n", "\n", "ํ•™์Šต์ž๋Š” ์‹ค์Šต๊ฐ€์ด๋“œ์— ์ œ์‹œ๋œ ์ด 4๊ฐœ ์„ธ๋ถ€ ํ™œ๋™(๊ธฐ๋ณธ ๊ธฐ๋Šฅ ๊ตฌํ˜„ โž” ๊ฒฐ๊ณผ ์ง€ํ‘œ ๊ฒ€์ฆ โž” ํŒŒ๋ผ๋ฏธํ„ฐ ํŠœ๋‹ ๋ฐ ๋„์ „๊ณผ์ œ)์„ ์ˆœ์ฐจ์ ์œผ๋กœ ์ง„ํ–‰ํ•˜๋ฉฐ ํ•ต์‹ฌ ํŒ์ • ๊ธฐ์ค€์„ ๋‹ฌ์„ฑํ•ฉ๋‹ˆ๋‹ค.\n", "\n", "**์ด๋ฒˆ ๊ฒฐ๊ณผ๋ฌผ:** `src/pipeline/dat09_batch_runner.py` ํ˜•ํƒœ์˜ ๊ฒฐ๊ณผ๋ฌผ์„ ์™„์„ฑํ•œ๋‹ค.\n", "\n", "### ๐Ÿ” ํ‰๊ฐ€ ๋ฐ ๊ฒ€์ฆ ๊ธฐ์ค€ (PASS ์กฐ๊ฑด)\n", "#### ๋ฏธ์…˜ ํŒ์ •\n", "\n", "**ํ‰๊ฐ€ ๊ตฌ๋ถ„:** ํ•„์ˆ˜\n", "\n", "**ํ•ต์‹ฌ ํ‰๊ฐ€์˜์—ญ:** ์ˆ˜์ž‘์—… ๋Œ€๋น„ ์—…๋ฌด ์†Œ์š” ์‹œ๊ฐ„ 80% ์ด์ƒ ์ ˆ๊ฐ ํšจ๊ณผ\n", "\n", "#### ๋ฏธ์…˜ PASS ์กฐ๊ฑด\n", "\n", "- ์ •๋Ÿ‰ ๊ธฐ์ค€: ์ „์ฒด ๋ฐฐ์น˜ ํŒŒ์ดํ”„๋ผ์ธ ์‹คํ–‰ ์‹œ๊ฐ„ 5๋ถ„ ์ด๋‚ด, ์ž๋™ํ™” ์„ฑ๊ณต๋ฅ  99% ์ด์ƒ\n", "\n", "\n", "- ์ง„ํ–‰ ๊ธฐ์ค€: ์Šค์ผ€์ค„๋Ÿฌ ์—ฐ๋™ ํ…Œ์ŠคํŠธ ๋ฐ ์ž๋™ ์‹คํ–‰ ํ™•์ธ\n", "\n", "\n", "- ๊ฒ€์ฆ ๋ฐฉ๋ฒ•: ์‹ค์ œ ์›”๊ฐ„ ์—…๋ฌด ๋ฐ์ดํ„ฐ 1๋…„์น˜ ์ผ๊ด„ ๋ฐฐ์น˜ ์‹œ๋ฎฌ๋ ˆ์ด์…˜\n", "\n", "\n", "#### ์ฑ„์ ์ž ํŒ์ • ๋ฉ”๋ชจ\n", "\n", "> ๋‹จ์ˆœ ์ฝ”๋“œ ์‹คํ–‰ ์—ฌ๋ถ€๋ฟ๋งŒ ์•„๋‹ˆ๋ผ ์ถœ๋ ฅ๋œ ์ˆ˜์น˜์™€ ๊ทผ๊ฑฐ ์ž๋ฃŒ๊ฐ€ ๊ตฐ ์‹ค๋ฌด ์š”๊ตฌ์‚ฌํ•ญ์— ๋ถ€ํ•ฉํ•˜๋Š”์ง€ ๋ฉด๋ฐ€ํžˆ ํ™•์ธํ•œ๋‹ค.\n", "\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "\"\"\"์„ธ ์ฒด๊ณ„์—์„œ ์ˆ˜๋งŒ ๊ฑด์˜ ์žฌ๊ณ  ๋ณ€๋™ ํŠธ๋žœ์žญ์…˜์ด ๋’ค์„ž์—ฌ ์œ ์ž…๋˜๋Š” ์ƒํ™ฉ์„ ๋ชจ์˜ํ•ด ํ‘œ์ค€ํ™”ยท์ค‘๋ณต ์ œ๊ฑฐยท์˜ค๋ฅ˜ ๊ฒฉ๋ฆฌยท๋งˆ์ดํฌ๋กœ ๋ฐฐ์น˜ ๋ฐ˜์˜์„ ์ˆ˜ํ–‰ํ•˜๊ณ , ๊ทœ๋ชจ๋ณ„ ์ฒ˜๋ฆฌ ์‹œ๊ฐ„๊ณผ ์ž๋™ํ™” ์„ฑ๊ณต๋ฅ , ์ˆ˜์ž‘์—… ๋Œ€๋น„ ์ ˆ๊ฐ๋ฅ ์„ ์ธก์ •ํ•˜๋Š” ๋ถ€ํ•˜ ํ…Œ์ŠคํŠธ๋ฅผ ์‹คํ–‰ํ•œ๋‹ค.\"\"\"\n", "import datetime\n", "import time\n", "import numpy as np\n", "import pandas as pd\n", "\n", "# ---------------------------------------------------------------------------\n", "# [๊ต์œก์šฉ๊ฐ€์ƒ์ž๋ฃŒ] ๊ฐ€์ƒ ์ด๊ธฐ์ข… ๊ตฐ์ˆ˜์ •๋ณด์ฒด๊ณ„ 3์ข…(A ๋ณด๊ธ‰, B ์ •๋น„, C ํƒ„์•ฝ)์˜ ์žฌ๊ณ  ๋ณด๊ณ  ์ƒ์„ฑ๊ธฐ\n", "# ์‹ค์ œ ์ฒด๊ณ„๋ช…ยท๋ถ€๋Œ€ยทํ’ˆ๋ชฉยท์ˆ˜๋Ÿ‰ ์—†์Œ. ๊ฐ™์€ (ํ’ˆ๋ชฉ, ๋ถ€๋Œ€) ์žฌ๊ณ ๋ฅผ ์„ธ ์ฒด๊ณ„๊ฐ€ ์„œ๋กœ ๋‹ค๋ฅธ ์ฝ”๋“œยท๋‚ ์งœ ํ˜•์‹์œผ๋กœ ๋ณด๊ณ ํ•œ๋‹ค.\n", "# ๊ธฐ์ค€ ๋‚ ์งœ: 2026-09-30(๊ฐ€์ƒ). ๋งˆ์Šคํ„ฐ(์ •๋‹ต) ์žฌ๊ณ  TRUE ์™€ ์ฒด๊ณ„๋ณ„ ๋ณด๊ณ ์—๋Š” ์˜๋„์  ๋ถˆ์ผ์น˜ยท๊ฒฐ์ธก์ด ์„ž์—ฌ ์žˆ๋‹ค.\n", "# ---------------------------------------------------------------------------\n", "N_PART, N_UNIT = 60, 12\n", "CATS = [\"๊ธ‰์‹\", \"ํ”ผ๋ณต\", \"์œ ๋ฅ˜\", \"์ •๋น„์†Œ๋ชจ\", \"์ˆ˜๋ฆฌ๋ถ€์†\", \"ํƒ„์•ฝ\"]\n", "PARTS = {p: {\"name\": f\"๊ฐ€์ƒํ’ˆ๋ชฉ{p:02d}\", \"cat\": CATS[(p - 1) % 6]} for p in range(1, N_PART + 1)}\n", "FMT = {\"A\": dict(part=lambda p: f\"AP-{p:05d}\", unit=lambda u: f\"U{u:02d}\", date=lambda d: d.strftime(\"%Y%m%d\")),\n", " \"B\": dict(part=lambda p: f\"{p:05d}-X\", unit=lambda u: f\"๊ฐ€์ƒ{u:02d}๋Œ€๋Œ€\", date=lambda d: d.strftime(\"%Y-%m-%d\")),\n", " \"C\": dict(part=lambda p: f\"P{p}\", unit=lambda u: str(u), date=lambda d: d.strftime(\"%d/%m/%Y\"))}\n", "BASE_DATE = datetime.date(2026, 9, 30)\n", "\n", "\n", "def make_systems(seed=9, mismatch=0.10, missing=0.03):\n", " \"\"\"TRUE(์ •๋‹ต ์žฌ๊ณ )์™€ ์ฒด๊ณ„๋ณ„ ์›๋ณธ ๋ณด๊ณ  ๋ ˆ์ฝ”๋“œ(์ฒด๊ณ„ ๊ณ ์œ  ํ˜•์‹)๋ฅผ ๋งŒ๋“ ๋‹ค.\"\"\"\n", " rng = np.random.default_rng(seed)\n", " true = {(p, u): int(rng.integers(20, 500)) for p in PARTS for u in range(1, N_UNIT + 1)}\n", " out = {s: [] for s in \"ABC\"}\n", " truth_diff = {} # (์ฒด๊ณ„, p, u) -> ๋ถˆ์ผ์น˜ ์›์ธ(์ •๋‹ต)\n", " for (p, u), q in true.items():\n", " for s in \"ABC\":\n", " qty, cause = q, None\n", " r = rng.random()\n", " if r < mismatch / 3: # ์ž…๋ ฅ ์˜ค๋ฅ˜(์ž๋ฆฟ์ˆ˜/์˜คํƒ€)\n", " qty, cause = q + int(rng.choice([-1, 1]) * rng.integers(10, 60)), \"์ž…๋ ฅ ์˜ค๋ฅ˜\"\n", " elif r < 2 * mismatch / 3: # ๋ฐ˜์˜ ์ง€์—ฐ(์ „์ผ ๊ฐ’)\n", " qty, cause = q + int(rng.integers(5, 40)), \"๋ฐ˜์˜ ์ง€์—ฐ\"\n", " elif r < mismatch: # ๋‹จ์œ„ ์˜ค๋ฅ˜(10๋ฐฐ)\n", " qty, cause = q * 10, \"๋‹จ์œ„ ์˜ค๋ฅ˜\"\n", " d = BASE_DATE - datetime.timedelta(days=int(cause == \"๋ฐ˜์˜ ์ง€์—ฐ\"))\n", " if rng.random() < missing: # ๊ฒฐ์ธก\n", " qty, cause = None, \"๊ฒฐ์ธก\"\n", " if cause:\n", " truth_diff[(s, p, u)] = cause\n", " out[s].append({\"part\": FMT[s][\"part\"](p), \"unit\": FMT[s][\"unit\"](u), \"date\": FMT[s][\"date\"](d), \"qty\": qty})\n", " return true, out, truth_diff\n", "\n", "\n", "def parse_part(code):\n", " return int(code[3:]) if code.startswith(\"AP-\") else int(code[:-2]) if code.endswith(\"-X\") else int(code[1:])\n", "\n", "\n", "def parse_unit(code):\n", " return int(code[1:]) if code.startswith(\"U\") else int(code[2:4]) if code.startswith(\"๊ฐ€์ƒ\") else int(code)\n", "\n", "\n", "def parse_date(s):\n", " if \"/\" in s:\n", " d, m, y = s.split(\"/\"); return datetime.date(int(y), int(m), int(d))\n", " if \"-\" in s:\n", " return datetime.date(*map(int, s.split(\"-\")))\n", " return datetime.date(int(s[:4]), int(s[4:6]), int(s[6:]))\n", "\n", "def make_tx(n, seed=8):\n", " \"\"\"n๊ฑด ํŠธ๋žœ์žญ์…˜: ์ฒด๊ณ„๋ณ„ ๊ณ ์œ  ์ฝ”๋“œ ํ˜•์‹, ๋„์ฐฉ ์‹œ๊ฐ ๋’ค์„ž์ž„, ์ค‘๋ณต 1%, ํ˜•์‹ ์˜ค๋ฅ˜ 0.4%.\"\"\"\n", " rng = np.random.default_rng(seed)\n", " sysn = rng.choice(list(\"ABC\"), n, p=[0.45, 0.30, 0.25])\n", " p, u = rng.integers(1, N_PART + 1, n), rng.integers(1, N_UNIT + 1, n)\n", " delta = rng.integers(-20, 21, n)\n", " part = np.where(sysn == \"A\", [f\"AP-{x:05d}\" for x in p], np.where(sysn == \"B\", [f\"{x:05d}-X\" for x in p], [f\"P{x}\" for x in p]))\n", " unit = np.where(sysn == \"A\", [f\"U{x:02d}\" for x in u], np.where(sysn == \"B\", [f\"๊ฐ€์ƒ{x:02d}๋Œ€๋Œ€\" for x in u], [str(x) for x in u]))\n", " tx = pd.DataFrame({\"id\": [f\"TX{i:06d}\" for i in range(n)], \"sys\": sysn, \"part\": part, \"unit\": unit, \"delta\": delta,\n", " \"ts\": np.sort(rng.integers(0, 3600 * 1000, n))})\n", " dup = tx.sample(int(n * 0.01), random_state=seed)\n", " tx = pd.concat([tx, dup]).sample(frac=1, random_state=seed).reset_index(drop=True) # ์ค‘๋ณต ํฌํ•จ, ๋„์ฐฉ ์ˆœ์„œ ๋’ค์„ž๊ธฐ\n", " bad = rng.choice(len(tx), int(len(tx) * 0.004), replace=False)\n", " tx.loc[bad, \"part\"] = \"??-\" + tx.loc[bad, \"part\"] # ํ˜•์‹ ์˜ค๋ฅ˜ ์ฃผ์ž…\n", " return tx\n", "\n", "\n", "def process(tx, batch=2000, flaky=()):\n", " \"\"\"๋งˆ์ดํฌ๋กœ ๋ฐฐ์น˜ ์ฒ˜๋ฆฌ. flaky์— ๋“  ๋ฐฐ์น˜ ๋ฒˆํ˜ธ๋Š” ์ฒซ ์‹œ๋„์—์„œ ์ผ์‹œ ์žฅ์• ๊ฐ€ ๋‚˜๋ฉฐ ์žฌ์‹œ๋„๋กœ ๋ณต๊ตฌํ•œ๋‹ค.\"\"\"\n", " stock = {(p, u): 0 for p in PARTS for u in range(1, N_UNIT + 1)}\n", " seen, stat = set(), {\"ok\": 0, \"dup\": 0, \"quarantine\": 0, \"retry\": 0}\n", " for b, i in enumerate(range(0, len(tx), batch)):\n", " chunk = tx.iloc[i:i + batch]\n", " for attempt in (1, 2, 3):\n", " if b in flaky and attempt == 1:\n", " stat[\"retry\"] += 1; continue # ์ผ์‹œ ์žฅ์• : ๊ฐ™์€ ๋ฐฐ์น˜๋ฅผ ๋‹ค์‹œ ์‹œ๋„\n", " e1 = chunk[\"part\"].str.extract(r\"^(?:AP-(\\d+)|(\\d+)-X|P(\\d+))$\")\n", " e2 = chunk[\"unit\"].str.extract(r\"^(?:U(\\d+)|๊ฐ€์ƒ(\\d+)๋Œ€๋Œ€|(\\d+))$\")\n", " p, u = e1[0].fillna(e1[1]).fillna(e1[2]), e2[0].fillna(e2[1]).fillna(e2[2])\n", " valid = p.notna() & u.notna()\n", " isdup = chunk[\"id\"].isin(seen) | chunk[\"id\"].duplicated()\n", " stat[\"quarantine\"] += int((~valid).sum()); stat[\"dup\"] += int((valid & isdup).sum())\n", " good = chunk[valid & ~isdup]\n", " seen.update(good[\"id\"])\n", " pp, uu = p[good.index].astype(int), u[good.index].astype(int)\n", " for k, d in pd.DataFrame({\"p\": pp, \"u\": uu, \"d\": good[\"delta\"]}).groupby([\"p\", \"u\"])[\"d\"].sum().items():\n", " stock[k] += int(d)\n", " stat[\"ok\"] += len(good)\n", " break\n", " return stock, stat\n", "\n", "\n", "print(\"[๊ทœ๋ชจ๋ณ„ ๋ถ€ํ•˜ ํ…Œ์ŠคํŠธ] ์ฒด๊ณ„ 3์ข… ํŠธ๋žœ์žญ์…˜ ๋™์‹œ ์œ ์ž… ๋ชจ์˜ [๊ต์œก์šฉ๊ฐ€์ƒ์ž๋ฃŒ]\")\n", "res = []\n", "for n in (5000, 20000, 50000):\n", " tx = make_tx(n)\n", " t0 = time.perf_counter(); stock, st = process(tx, flaky=(1, 3)); dt = time.perf_counter() - t0\n", " res.append({\"์œ ์ž… ๊ฑด์ˆ˜\": len(tx), \"์ฒ˜๋ฆฌ ์‹œ๊ฐ„(์ดˆ)\": round(dt, 2), \"์ดˆ๋‹น ์ฒ˜๋ฆฌ\": int(len(tx) / dt), **st})\n", " succ = (st[\"ok\"] + st[\"dup\"]) / len(tx)\n", " res[-1][\"์ž๋™ ์ฒ˜๋ฆฌ์œจ\"] = f\"{succ:.2%}\"\n", "print(pd.DataFrame(res).to_string(index=False))\n", "\n", "# ์ •ํ•ฉ์„ฑ: ๋งˆ์ง€๋ง‰ ๊ทœ๋ชจ์—์„œ ์ผ๊ด„(๋ฒกํ„ฐ) ๊ณ„์‚ฐ ๊ฒฐ๊ณผ์™€ ๋งˆ์ดํฌ๋กœ ๋ฐฐ์น˜ ๊ฒฐ๊ณผ ๋น„๊ต\n", "tx2 = tx[~tx[\"part\"].str.startswith(\"??\")].drop_duplicates(\"id\")\n", "p = tx2[\"part\"].str.extract(r\"(\\d+)\")[0].astype(int)\n", "u = tx2[\"unit\"].str.extract(r\"(\\d+)\")[0].astype(int)\n", "ref = tx2.assign(p=p, u=u).groupby([\"p\", \"u\"])[\"delta\"].sum()\n", "stock, st = process(tx, flaky=(1, 3))\n", "mism = sum(stock[(a, b)] != int(v) for (a, b), v in ref.items())\n", "print(f\"\\n์ •ํ•ฉ์„ฑ: ์ผ๊ด„ ๊ณ„์‚ฐ๊ณผ ๋ฐฐ์น˜ ๊ฒฐ๊ณผ ๋น„๊ต {len(ref)}๊ฐœ ํ‚ค ์ค‘ ๋ถˆ์ผ์น˜ {mism}๊ฑด\")\n", "n = len(tx); success = (st[\"ok\"] + st[\"dup\"]) / n\n", "print(f\"์ „์ฒด {n}๊ฑด: ์ •์ƒ ๋ฐ˜์˜ {st['ok']}, ์ค‘๋ณต ์ œ๊ฑฐ {st['dup']}, ํ˜•์‹ ์˜ค๋ฅ˜ ๊ฒฉ๋ฆฌ {st['quarantine']}(๋‹ด๋‹น์ž ํ™•์ธ), ์ผ์‹œ ์žฅ์•  ์žฌ์‹œ๋„ {st['retry']}ํšŒ\")\n", "total_s = res[-1][\"์ฒ˜๋ฆฌ ์‹œ๊ฐ„(์ดˆ)\"]\n", "print(f\"์ž๋™ํ™” ์„ฑ๊ณต๋ฅ  {success:.2%} (๊ฒฉ๋ฆฌ ๊ฑด์€ ์‚ฌ๋žŒ์ด ํ™•์ธ), ์ตœ๋Œ€ ๊ทœ๋ชจ ์ฒ˜๋ฆฌ ์‹œ๊ฐ„ {total_s}์ดˆ(๋ชฉํ‘œ 5๋ถ„ ์ด๋‚ด: {'๋‹ฌ์„ฑ' if total_s < 300 else '๋ฏธ๋‹ฌ'})\")\n", "manual = n * 0.4 # ๊ฐ€์ •: ์ˆ˜์ž‘์—… ํ™•์ธยท์ž…๋ ฅ 0.4๋ถ„/๊ฑด\n", "auto = n * 0.01 + st[\"quarantine\"] * 4 + total_s / 60 # ๊ฐ€์ •: ํ‘œ๋ณธ ๊ฒ€ํ†  0.01๋ถ„/๊ฑด, ๊ฒฉ๋ฆฌ ๊ฑด ์ˆ˜๋™ 4๋ถ„\n", "print(f\"์—…๋ฌด ์†Œ์š”(๊ฐ€์ •๊ฐ’): ์ˆ˜์ž‘์—… {manual:.0f}๋ถ„ vs ์ž๋™ํ™” {auto:.0f}๋ถ„ -> ์ ˆ๊ฐ {1 - auto / manual:.1%}\")\n", "print(f\"\\n๋ชฉํ‘œ(5๋ถ„ ์ด๋‚ด, ์ž๋™ํ™” ์„ฑ๊ณต๋ฅ  99% ์ด์ƒ, ์ˆ˜์ž‘์—… ๋Œ€๋น„ 80% ์ด์ƒ ์ ˆ๊ฐ): \"\n", " f\"{'๋‹ฌ์„ฑ' if total_s < 300 and success >= 0.99 and 1 - auto / manual >= 0.8 and mism == 0 else '๋ฏธ๋‹ฌ'}\")\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "
\n", "๐Ÿ’ก [์ฐธ๊ณ ] ๋ฏธ์…˜ 08 ์˜ˆ์ƒ ์‹คํ–‰ ๊ฒฐ๊ณผ (Output)\n", "\n", "```text\n", "[๊ทœ๋ชจ๋ณ„ ๋ถ€ํ•˜ ํ…Œ์ŠคํŠธ] ์ฒด๊ณ„ 3์ข… ํŠธ๋žœ์žญ์…˜ ๋™์‹œ ์œ ์ž… ๋ชจ์˜ [๊ต์œก์šฉ๊ฐ€์ƒ์ž๋ฃŒ]\n", " ์œ ์ž… ๊ฑด์ˆ˜ ์ฒ˜๋ฆฌ ์‹œ๊ฐ„(์ดˆ) ์ดˆ๋‹น ์ฒ˜๋ฆฌ ok dup quarantine retry ์ž๋™ ์ฒ˜๋ฆฌ์œจ\n", " 5050 0.04 114205 4982 48 20 1 99.60%\n", " 20200 0.20 100425 19921 199 80 2 99.60%\n", " 50500 0.69 73706 49801 497 202 2 99.60%\n", "\n", "์ •ํ•ฉ์„ฑ: ์ผ๊ด„ ๊ณ„์‚ฐ๊ณผ ๋ฐฐ์น˜ ๊ฒฐ๊ณผ ๋น„๊ต 720๊ฐœ ํ‚ค ์ค‘ ๋ถˆ์ผ์น˜ 0๊ฑด\n", "์ „์ฒด 50500๊ฑด: ์ •์ƒ ๋ฐ˜์˜ 49801, ์ค‘๋ณต ์ œ๊ฑฐ 497, ํ˜•์‹ ์˜ค๋ฅ˜ ๊ฒฉ๋ฆฌ 202(๋‹ด๋‹น์ž ํ™•์ธ), ์ผ์‹œ ์žฅ์•  ์žฌ์‹œ๋„ 2ํšŒ\n", "์ž๋™ํ™” ์„ฑ๊ณต๋ฅ  99.60% (๊ฒฉ๋ฆฌ ๊ฑด์€ ์‚ฌ๋žŒ์ด ํ™•์ธ), ์ตœ๋Œ€ ๊ทœ๋ชจ ์ฒ˜๋ฆฌ ์‹œ๊ฐ„ 0.69์ดˆ(๋ชฉํ‘œ 5๋ถ„ ์ด๋‚ด: ๋‹ฌ์„ฑ)\n", "์—…๋ฌด ์†Œ์š”(๊ฐ€์ •๊ฐ’): ์ˆ˜์ž‘์—… 20200๋ถ„ vs ์ž๋™ํ™” 1313๋ถ„ -> ์ ˆ๊ฐ 93.5%\n", "\n", "๋ชฉํ‘œ(5๋ถ„ ์ด๋‚ด, ์ž๋™ํ™” ์„ฑ๊ณต๋ฅ  99% ์ด์ƒ, ์ˆ˜์ž‘์—… ๋Œ€๋น„ 80% ์ด์ƒ ์ ˆ๊ฐ): ๋‹ฌ์„ฑ\n", "```\n", "
\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "---\n", "\n", "## ๐ŸŽฏ ๋ฏธ์…˜ 09. AAR ๊ธฐ๋ฐ˜ ๋ฐ์ดํ„ฐ ์—ฐ๋™ ๋ˆ„๋ฝ ๋ถ„์„ ๋ฐ ๋งคํ•‘ ํ…Œ์ด๋ธ” ํŒŒ๋ผ๋ฏธํ„ฐ ์žฌํŠœ๋‹\n", "**์†Œ์š”์‹œ๊ฐ„**: ์•ฝ 3.0์‹œ๊ฐ„\n", "\n", "### ๐Ÿ“‹ ๋ฏธ์…˜ ๋ฐฐ๊ฒฝ ๋ฐ ์ˆ˜ํ–‰ ์ง€์นจ\n", "#### ๋ฏธ์…˜ ๋ฐฐ๊ฒฝ ๋ฐ ๋‹น์œ„์„ฑ\n", "\n", "AAR ๊ธฐ๋ฐ˜ ๋ฐ์ดํ„ฐ ์—ฐ๋™ ๋ˆ„๋ฝ์„ ๋ถ„์„ํ•˜๊ณ  ๋งคํ•‘ ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ์žฌํŠœ๋‹ํ•œ๋‹ค. ์‹ค์ œ ์ „์ˆ  ํ˜„์žฅ์—์„œ๋Š” ๋‹จ์ˆœ ํƒ์ง€์— ๊ทธ์น˜์ง€ ์•Š๊ณ  ์‹ ์†ํ•œ ์œ„ํ˜‘ ํŒ๋‹จ๊ณผ ์‹ ๋ขฐ๋„ ๋†’์€ ์˜์‚ฌ๊ฒฐ์ •์ด ํ•„์ˆ˜์ ์ด๋ฏ€๋กœ, **AAR์—์„œ ๋“œ๋Ÿฌ๋‚œ ๋ฐ์ดํ„ฐ ์—ฐ๋™ ๋ˆ„๋ฝ 5์œ ํ˜•์„ ์žฌํ˜„ยท๋ถ„์„ํ•˜๊ณ  ๋งคํ•‘ ๊ทœ์น™๊ณผ ํŒ์ • ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ์žฌํŠœ๋‹ํ•œ๋‹ค.** ๊ณผ์—…์„ ์™„์ˆ˜ํ•˜์—ฌ ์˜คํŒ์„ ๋ฐฉ์ง€ํ•˜๊ณ  ์ž‘์ „ ๋Œ€์‘๋ ฅ์„ ๊ทน๋Œ€ํ™”ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.\n", "\n", "์ด ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด ๋ณธ ๋ฏธ์…˜์—์„œ๋Š” **์ฝ”๋“œ ์ž๋ฆฟ์ˆ˜, ๋ถ€๋Œ€๋ช…, ๋‚ ์งœ ํ˜•์‹, ๊ฒฐ์ธก ํ‘œ๊ธฐ, ์‹ ๊ทœ ํ’ˆ๋ชฉ์˜ 5์œ ํ˜• ๋ณ€ํ˜•์„ ๊ฐ€์ƒ ๋ณด๊ณ ์— ์‹ฌ์–ด ๋งคํ•‘ v1์˜ ๋ˆ„๋ฝ์„ ์žฌํ˜„ ์ˆ˜ํ–‰ํ•˜๊ณ  ๊ฒ€์ฆํ•˜๋Š”** ์‹ค์Šต์„ ์ฒด๊ณ„์ ์œผ๋กœ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค.\n", "\n", "ํ•™์Šต์ž๋Š” ์‹ค์Šต๊ฐ€์ด๋“œ์— ์ œ์‹œ๋œ ์ด 4๊ฐœ ์„ธ๋ถ€ ํ™œ๋™(๊ธฐ๋ณธ ๊ธฐ๋Šฅ ๊ตฌํ˜„ โž” ๊ฒฐ๊ณผ ์ง€ํ‘œ ๊ฒ€์ฆ โž” ํŒŒ๋ผ๋ฏธํ„ฐ ํŠœ๋‹ ๋ฐ ๋„์ „๊ณผ์ œ)์„ ์ˆœ์ฐจ์ ์œผ๋กœ ์ง„ํ–‰ํ•˜๋ฉฐ ํ•ต์‹ฌ ํŒ์ • ๊ธฐ์ค€์„ ๋‹ฌ์„ฑํ•ฉ๋‹ˆ๋‹ค.\n", "\n", "**์ด๋ฒˆ ๊ฒฐ๊ณผ๋ฌผ:** `docs/dat09_uat_feedback_report.md ๋ฐ ํŒจ์น˜ ๋‚ด์—ญ` ํ˜•ํƒœ์˜ ๊ฒฐ๊ณผ๋ฌผ์„ ์™„์„ฑํ•œ๋‹ค.\n", "\n", "### ๐Ÿ” ํ‰๊ฐ€ ๋ฐ ๊ฒ€์ฆ ๊ธฐ์ค€ (PASS ์กฐ๊ฑด)\n", "#### ๋ฏธ์…˜ ํŒ์ •\n", "\n", "**ํ‰๊ฐ€ ๊ตฌ๋ถ„:** ํ•„์ˆ˜\n", "\n", "**ํ•ต์‹ฌ ํ‰๊ฐ€์˜์—ญ:** ์‹ค๋ฌด ์—…๋ฌด ์ƒ์‚ฐ์„ฑ ๊ธฐ์—ฌ๋„ ๋ฐ ํ˜„์žฅ ์ ์‘์„ฑ\n", "\n", "#### ๋ฏธ์…˜ PASS ์กฐ๊ฑด\n", "\n", "- ์ •๋Ÿ‰ ๊ธฐ์ค€: UAT ์ฒดํฌ๋ฆฌ์ŠคํŠธ ๋งŒ์กฑ๋„ 90์  ์ด์ƒ, ๊ฐœ์„  ์š”๊ตฌ 5๊ฑด ๋ฐ˜์˜ ์™„๋ฃŒ\n", "\n", "\n", "- ์ง„ํ–‰ ๊ธฐ์ค€: UAT ๊ฒฐ๊ณผ ๋ณด๊ณ ์„œ ์ž‘์„ฑ ๋ฐ ์ตœ์ข… ๋ฒ„๊ทธ ํ”ฝ์Šค ์™„๋ฃŒ\n", "\n", "\n", "- ๊ฒ€์ฆ ๋ฐฉ๋ฒ•: ์‹ค์ œ ์—…๋ฌด ์‹œ๋‚˜๋ฆฌ์˜ค ๊ธฐ๋ฐ˜ ์ตœ์ข… ๊ฒ€์ˆ˜ ํ…Œ์ŠคํŠธ\n", "\n", "\n", "#### ์ฑ„์ ์ž ํŒ์ • ๋ฉ”๋ชจ\n", "\n", "> ๋‹จ์ˆœ ์ฝ”๋“œ ์‹คํ–‰ ์—ฌ๋ถ€๋ฟ๋งŒ ์•„๋‹ˆ๋ผ ์ถœ๋ ฅ๋œ ์ˆ˜์น˜์™€ ๊ทผ๊ฑฐ ์ž๋ฃŒ๊ฐ€ ๊ตฐ ์‹ค๋ฌด ์š”๊ตฌ์‚ฌํ•ญ์— ๋ถ€ํ•ฉํ•˜๋Š”์ง€ ๋ฉด๋ฐ€ํžˆ ํ™•์ธํ•œ๋‹ค.\n", "\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "\"\"\"AAR์—์„œ ๋“œ๋Ÿฌ๋‚œ ์ฒด๊ณ„ ์—ฐ๋™ ๋ˆ„๋ฝ 5์œ ํ˜•(์ฝ”๋“œ ์ž๋ฆฟ์ˆ˜ ๋ณ€ํ˜•, ๋ถ€๋Œ€๋ช… ๋ณ€ํ˜•, ๋‚ ์งœ ํ˜•์‹ ๋ณ€ํ˜•, ๊ฒฐ์ธก ํ‘œ๊ธฐ ๋ณ€ํ˜•, ์‹ ๊ทœ ํ’ˆ๋ชฉ)์„ ์žฌํ˜„ํ•ด ๋งคํ•‘ ๊ทœ์น™ v1์˜ ๋ˆ„๋ฝ์„ ๋ถ„์„ํ•˜๊ณ , ํ—ˆ์šฉ ์˜ค์ฐจยท์ง€์—ฐ ์ผ์ˆ˜ ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ๊ฒฉ์ž ํƒ์ƒ‰์œผ๋กœ ์žฌํŠœ๋‹ํ•œ v2์™€ UAT ์ฒดํฌ๋ฆฌ์ŠคํŠธ ์ ์ˆ˜๋กœ ๊ฐœ์„  ํšจ๊ณผ๋ฅผ ๊ฒ€์ฆํ•œ๋‹ค.\"\"\"\n", "import datetime\n", "import itertools\n", "import re\n", "import numpy as np\n", "import pandas as pd\n", "\n", "# ---------------------------------------------------------------------------\n", "# [๊ต์œก์šฉ๊ฐ€์ƒ์ž๋ฃŒ] ๊ฐ€์ƒ ์ด๊ธฐ์ข… ๊ตฐ์ˆ˜์ •๋ณด์ฒด๊ณ„ 3์ข…(A ๋ณด๊ธ‰, B ์ •๋น„, C ํƒ„์•ฝ)์˜ ์žฌ๊ณ  ๋ณด๊ณ  ์ƒ์„ฑ๊ธฐ\n", "# ์‹ค์ œ ์ฒด๊ณ„๋ช…ยท๋ถ€๋Œ€ยทํ’ˆ๋ชฉยท์ˆ˜๋Ÿ‰ ์—†์Œ. ๊ฐ™์€ (ํ’ˆ๋ชฉ, ๋ถ€๋Œ€) ์žฌ๊ณ ๋ฅผ ์„ธ ์ฒด๊ณ„๊ฐ€ ์„œ๋กœ ๋‹ค๋ฅธ ์ฝ”๋“œยท๋‚ ์งœ ํ˜•์‹์œผ๋กœ ๋ณด๊ณ ํ•œ๋‹ค.\n", "# ๊ธฐ์ค€ ๋‚ ์งœ: 2026-09-30(๊ฐ€์ƒ). ๋งˆ์Šคํ„ฐ(์ •๋‹ต) ์žฌ๊ณ  TRUE ์™€ ์ฒด๊ณ„๋ณ„ ๋ณด๊ณ ์—๋Š” ์˜๋„์  ๋ถˆ์ผ์น˜ยท๊ฒฐ์ธก์ด ์„ž์—ฌ ์žˆ๋‹ค.\n", "# ---------------------------------------------------------------------------\n", "N_PART, N_UNIT = 60, 12\n", "CATS = [\"๊ธ‰์‹\", \"ํ”ผ๋ณต\", \"์œ ๋ฅ˜\", \"์ •๋น„์†Œ๋ชจ\", \"์ˆ˜๋ฆฌ๋ถ€์†\", \"ํƒ„์•ฝ\"]\n", "PARTS = {p: {\"name\": f\"๊ฐ€์ƒํ’ˆ๋ชฉ{p:02d}\", \"cat\": CATS[(p - 1) % 6]} for p in range(1, N_PART + 1)}\n", "FMT = {\"A\": dict(part=lambda p: f\"AP-{p:05d}\", unit=lambda u: f\"U{u:02d}\", date=lambda d: d.strftime(\"%Y%m%d\")),\n", " \"B\": dict(part=lambda p: f\"{p:05d}-X\", unit=lambda u: f\"๊ฐ€์ƒ{u:02d}๋Œ€๋Œ€\", date=lambda d: d.strftime(\"%Y-%m-%d\")),\n", " \"C\": dict(part=lambda p: f\"P{p}\", unit=lambda u: str(u), date=lambda d: d.strftime(\"%d/%m/%Y\"))}\n", "BASE_DATE = datetime.date(2026, 9, 30)\n", "\n", "\n", "def make_systems(seed=9, mismatch=0.10, missing=0.03):\n", " \"\"\"TRUE(์ •๋‹ต ์žฌ๊ณ )์™€ ์ฒด๊ณ„๋ณ„ ์›๋ณธ ๋ณด๊ณ  ๋ ˆ์ฝ”๋“œ(์ฒด๊ณ„ ๊ณ ์œ  ํ˜•์‹)๋ฅผ ๋งŒ๋“ ๋‹ค.\"\"\"\n", " rng = np.random.default_rng(seed)\n", " true = {(p, u): int(rng.integers(20, 500)) for p in PARTS for u in range(1, N_UNIT + 1)}\n", " out = {s: [] for s in \"ABC\"}\n", " truth_diff = {} # (์ฒด๊ณ„, p, u) -> ๋ถˆ์ผ์น˜ ์›์ธ(์ •๋‹ต)\n", " for (p, u), q in true.items():\n", " for s in \"ABC\":\n", " qty, cause = q, None\n", " r = rng.random()\n", " if r < mismatch / 3: # ์ž…๋ ฅ ์˜ค๋ฅ˜(์ž๋ฆฟ์ˆ˜/์˜คํƒ€)\n", " qty, cause = q + int(rng.choice([-1, 1]) * rng.integers(10, 60)), \"์ž…๋ ฅ ์˜ค๋ฅ˜\"\n", " elif r < 2 * mismatch / 3: # ๋ฐ˜์˜ ์ง€์—ฐ(์ „์ผ ๊ฐ’)\n", " qty, cause = q + int(rng.integers(5, 40)), \"๋ฐ˜์˜ ์ง€์—ฐ\"\n", " elif r < mismatch: # ๋‹จ์œ„ ์˜ค๋ฅ˜(10๋ฐฐ)\n", " qty, cause = q * 10, \"๋‹จ์œ„ ์˜ค๋ฅ˜\"\n", " d = BASE_DATE - datetime.timedelta(days=int(cause == \"๋ฐ˜์˜ ์ง€์—ฐ\"))\n", " if rng.random() < missing: # ๊ฒฐ์ธก\n", " qty, cause = None, \"๊ฒฐ์ธก\"\n", " if cause:\n", " truth_diff[(s, p, u)] = cause\n", " out[s].append({\"part\": FMT[s][\"part\"](p), \"unit\": FMT[s][\"unit\"](u), \"date\": FMT[s][\"date\"](d), \"qty\": qty})\n", " return true, out, truth_diff\n", "\n", "\n", "def parse_part(code):\n", " return int(code[3:]) if code.startswith(\"AP-\") else int(code[:-2]) if code.endswith(\"-X\") else int(code[1:])\n", "\n", "\n", "def parse_unit(code):\n", " return int(code[1:]) if code.startswith(\"U\") else int(code[2:4]) if code.startswith(\"๊ฐ€์ƒ\") else int(code)\n", "\n", "\n", "def parse_date(s):\n", " if \"/\" in s:\n", " d, m, y = s.split(\"/\"); return datetime.date(int(y), int(m), int(d))\n", " if \"-\" in s:\n", " return datetime.date(*map(int, s.split(\"-\")))\n", " return datetime.date(int(s[:4]), int(s[4:6]), int(s[6:]))\n", "\n", "true, raw, tdiff = make_systems()\n", "rng = np.random.default_rng(9)\n", "\n", "# 1) AAR ์‚ฌ๋ก€ ์žฌํ˜„: ์ผ๋ถ€ ๋ณด๊ณ ๋ฅผ ์ฒด๊ณ„ ๋‹ด๋‹น์ž๊ฐ€ ๋‹ค๋ฅด๊ฒŒ ์“ด ํ˜•ํƒœ๋กœ ๋ฐ”๊พผ๋‹ค(๋ณ€ํ˜• 5์œ ํ˜•)\n", "VAR = {\"์ฝ”๋“œ ์ž๋ฆฟ์ˆ˜ ๋ณ€ํ˜•\": [], \"๋ถ€๋Œ€๋ช… ๋ณ€ํ˜•\": [], \"๋‚ ์งœ ํ˜•์‹ ๋ณ€ํ˜•\": [], \"๊ฒฐ์ธก ํ‘œ๊ธฐ ๋ณ€ํ˜•\": [], \"์‹ ๊ทœ ํ’ˆ๋ชฉ\": []}\n", "def pick(s, n, f=lambda r: True):\n", " idx = [i for i, r in enumerate(raw[s]) if f(r) and not any(i == j for v in VAR.values() for (s2, j) in v if s2 == s)]\n", " return [(s, int(i)) for i in rng.choice(idx, n, replace=False)]\n", "for s, i in pick(\"A\", 25):\n", " raw[s][i][\"part\"] = \"AP-\" + raw[s][i][\"part\"][-4:]; VAR[\"์ฝ”๋“œ ์ž๋ฆฟ์ˆ˜ ๋ณ€ํ˜•\"].append((s, i))\n", "for s, i in pick(\"B\", 30):\n", " raw[s][i][\"unit\"] = \"๊ฐ€์ƒ\" + str(int(raw[s][i][\"unit\"][2:4])) + \"๋Œ€๋Œ€\"; VAR[\"๋ถ€๋Œ€๋ช… ๋ณ€ํ˜•\"].append((s, i))\n", "for s, i in pick(\"C\", 30):\n", " d, m, y = raw[s][i][\"date\"].split(\"/\"); raw[s][i][\"date\"] = f\"{d}-{m}-{y}\"; VAR[\"๋‚ ์งœ ํ˜•์‹ ๋ณ€ํ˜•\"].append((s, i))\n", "for s, i in pick(\"A\", 20, lambda r: r[\"qty\"] is not None):\n", " raw[s][i][\"qty\"] = \"N/A\"; VAR[\"๊ฒฐ์ธก ํ‘œ๊ธฐ ๋ณ€ํ˜•\"].append((s, i))\n", "for s, i in pick(\"B\", 15):\n", " raw[s][i][\"part\"] = \"00061-X\"; VAR[\"์‹ ๊ทœ ํ’ˆ๋ชฉ\"].append((s, i))\n", "for s in \"ABC\": # ๋ฐ˜์˜ฌ๋ฆผ ์ฐจ์ด ๋“ฑ ์ •์ƒ ๋ณ€๋™(+-1)\n", " for i in rng.choice(len(raw[s]), 60, replace=False):\n", " if isinstance(raw[s][i][\"qty\"], int):\n", " raw[s][i][\"qty\"] += int(rng.choice([-1, 1]))\n", "\n", "# 2) ๋งคํ•‘ ๊ทœ์น™ v1(์—„๊ฒฉ) / v2(๋ณด์ •)\n", "PAT1 = {\"part\": r\"^(?:AP-(\\d{5})|(\\d{5})-X|P(\\d+))$\", \"unit\": r\"^(?:U(\\d{2})|๊ฐ€์ƒ(\\d{2})๋Œ€๋Œ€|(\\d+))$\"}\n", "PAT2 = {\"part\": r\"^(?:AP-(\\d{4,5})|(\\d{5})-X|P(\\d+))$\", \"unit\": r\"^(?:U(\\d{2})|๊ฐ€์ƒ(\\d{1,2})๋Œ€๋Œ€|(\\d+))$\"}\n", "def g(pat, s):\n", " m = re.match(pat, s)\n", " return int(next(x for x in m.groups() if x)) if m else None\n", "def pdate(s, v2):\n", " for f in (\"%Y%m%d\", \"%Y-%m-%d\", \"%d/%m/%Y\") + ((\"%d-%m-%Y\",) if v2 else ()):\n", " try:\n", " return datetime.datetime.strptime(s, f).date()\n", " except ValueError:\n", " pass\n", " return None\n", "def link(v2):\n", " pat, out, unlinked, held = (PAT2 if v2 else PAT1), [], 0, 0\n", " for s in \"ABC\":\n", " for i, r in enumerate(raw[s]):\n", " p, u, d = g(pat[\"part\"], r[\"part\"]), g(pat[\"unit\"], r[\"unit\"]), pdate(r[\"date\"], v2)\n", " q = r[\"qty\"]\n", " if q == \"N/A\":\n", " q = None if v2 else \"ERR\"\n", " if p is None or u is None or d is None or q == \"ERR\":\n", " unlinked += 1; continue\n", " if p > N_PART and v2:\n", " held += 1; continue # v2: ์‹ ๊ทœ ํ’ˆ๋ชฉ์€ ์กฐ์šฉํžˆ ๋ฒ„๋ฆฌ์ง€ ์•Š๊ณ  '์‹ ๊ทœ ํ’ˆ๋ชฉ ์‹ ์ฒญ' ๋Œ€์ƒ์œผ๋กœ ๋ณด๋ฅ˜\n", " if p > N_PART:\n", " unlinked += 1; continue\n", " out.append({\"sys\": s, \"p\": p, \"u\": u, \"date\": d, \"qty\": q})\n", " return pd.DataFrame(out), unlinked, held\n", "d1, un1, h1 = link(False); d2, un2, h2 = link(True)\n", "total = sum(len(v) for v in raw.values())\n", "print(f\"[AAR ์—ฐ๋™ ๋ˆ„๋ฝ ๋ถ„์„] ์ „์ฒด ๋ณด๊ณ  {total}๊ฑด [๊ต์œก์šฉ๊ฐ€์ƒ์ž๋ฃŒ]\")\n", "print(f\"๋งคํ•‘ v1: ์—ฐ๋™ {len(d1)}๊ฑด, ๋ˆ„๋ฝ {un1}๊ฑด({un1 / total:.1%}) | ๋งคํ•‘ v2: ์—ฐ๋™ {len(d2)}๊ฑด, ๋ˆ„๋ฝ {un2}๊ฑด, ์‹ ๊ทœ ํ’ˆ๋ชฉ ๋ณด๋ฅ˜ {h2}๊ฑด\")\n", "# ์‚ฌ๋ก€๋ณ„ v1 ๋ˆ„๋ฝ ๊ฑด์ˆ˜๋Š” ์ง์ ‘ ์žฌํŒ์ •\n", "def lost(s, i, v2):\n", " r = raw[s][i]; pat = PAT2 if v2 else PAT1\n", " p, u, d = g(pat[\"part\"], r[\"part\"]), g(pat[\"unit\"], r[\"unit\"]), pdate(r[\"date\"], v2)\n", " return p is None or u is None or d is None or (not v2 and (r[\"qty\"] == \"N/A\" or p > N_PART))\n", "tab = pd.DataFrame([{\"์‚ฌ๋ก€\": k, \"๋ฐœ์ƒ\": len(v), \"v1 ๋ˆ„๋ฝ\": sum(lost(s, i, False) for s, i in v), \"v2 ๋ˆ„๋ฝ\": sum(lost(s, i, True) for s, i in v)} for k, v in VAR.items()])\n", "print(tab.to_string(index=False))\n", "\n", "# 3) ๋ถˆ์ผ์น˜ ํŒ์ • ํŒŒ๋ผ๋ฏธํ„ฐ ์žฌํŠœ๋‹: ์ˆ˜๋Ÿ‰ ํ—ˆ์šฉ ์˜ค์ฐจ(%)์™€ ํ—ˆ์šฉ ์ง€์—ฐ ์ผ์ˆ˜์˜ ๊ฒฉ์ž ํƒ์ƒ‰(๊ฐ€์ƒ ์ •๋‹ต ๋Œ€๋น„ F1)\n", "gt = {(p, u) for (_, p, u), c in tdiff.items() if c != \"๊ฒฐ์ธก\"}\n", "def detect(df, tol, lag):\n", " df = df.copy(); df[\"q\"] = pd.to_numeric(df[\"qty\"], errors=\"coerce\")\n", " w = df.pivot_table(index=[\"p\", \"u\"], columns=\"sys\", values=\"q\", aggfunc=\"first\")\n", " med = w.median(axis=1)\n", " dev = w.sub(med, axis=0).abs().div(med.clip(lower=1), axis=0)\n", " late = df.assign(l=(BASE_DATE - df[\"date\"]).map(lambda x: x.days)).pivot_table(index=[\"p\", \"u\"], columns=\"sys\", values=\"l\", aggfunc=\"first\")\n", " flag = ((dev > tol) | (late.reindex(w.index) > lag).fillna(False) & (dev > 0)).any(axis=1)\n", " return set(flag[flag].index)\n", "def f1(found):\n", " tp = len(found & gt); pr = tp / max(len(found), 1); rc = tp / len(gt)\n", " return 2 * pr * rc / max(pr + rc, 1e-9), pr, rc\n", "grid = []\n", "for tol, lag in itertools.product((0.0, 0.005, 0.02, 0.05), (0, 1, 2)):\n", " grid.append({\"ํ—ˆ์šฉ์˜ค์ฐจ\": tol, \"ํ—ˆ์šฉ์ง€์—ฐ(์ผ)\": lag, **dict(zip((\"F1\", \"์ •๋ฐ€๋„\", \"์žฌํ˜„์œจ\"), f1(detect(d2, tol, lag))))})\n", "G = pd.DataFrame(grid).sort_values(\"F1\", ascending=False)\n", "best = G.iloc[0]\n", "v1p = f1(detect(d2, 0.0, 0))\n", "print(\"\\n[ํŒŒ๋ผ๋ฏธํ„ฐ ์žฌํŠœ๋‹ - ์ƒ์œ„ 3๊ฐœ ์กฐํ•ฉ]\")\n", "print(G.head(3).round(3).to_string(index=False))\n", "print(f\"ํŠœ๋‹ ์ „(์˜ค์ฐจ 0, ์ง€์—ฐ 0) F1 {v1p[0]:.3f} -> ํŠœ๋‹ ํ›„ F1 {best['F1']:.3f} (ํ—ˆ์šฉ์˜ค์ฐจ {best['ํ—ˆ์šฉ์˜ค์ฐจ']}, ํ—ˆ์šฉ์ง€์—ฐ {int(best['ํ—ˆ์šฉ์ง€์—ฐ(์ผ)'])}์ผ)\")\n", "\n", "# 4) UAT ์ฒดํฌ๋ฆฌ์ŠคํŠธ(์ž๋™ ์ ๊ฒ€ ํ•ญ๋ชฉ) + ๊ฐœ์„  ์š”๊ตฌ 5๊ฑด\n", "tests = [(f\"{k} ์‚ฌ๋ก€ v2์—์„œ ํ•ด์†Œ\", int(r[\"v2 ๋ˆ„๋ฝ\"]) == 0) for k, r in tab.set_index(\"์‚ฌ๋ก€\").iterrows()]\n", "tests += [(\"v2 ์—ฐ๋™๋ฅ ์ด v1๋ณด๋‹ค ๋†’์Œ\", len(d2) > len(d1)), (\"์‹ ๊ทœ ํ’ˆ๋ชฉ์€ ๋ˆ„๋ฝ์ด ์•„๋‹ˆ๋ผ ๋ณด๋ฅ˜๋กœ ๋ถ„๋ฅ˜\", h2 == 15), (\"v1 ์—ฐ๋™ ํ‚ค๊ฐ€ v2์— ๋ชจ๋‘ ํฌํ•จ\", set(zip(d1[\"sys\"], d1[\"p\"], d1[\"u\"])) <= set(zip(d2[\"sys\"], d2[\"p\"], d2[\"u\"]))),\n", " (\"ํŠœ๋‹ ํ›„ F1์ด ํŠœ๋‹ ์ „๋ณด๋‹ค ๋†’์Œ\", best[\"F1\"] > v1p[0]), (\"์žฌํ˜„์œจ 95% ์ด์ƒ\", best[\"์žฌํ˜„์œจ\"] >= 0.95), (\"์ •๋ฐ€๋„ 90% ์ด์ƒ\", best[\"์ •๋ฐ€๋„\"] >= 0.9),\n", " (\"์—ฐ๋™ ํ‚ค๊ฐ€ (์ฒด๊ณ„, ํ’ˆ๋ชฉ, ๋ถ€๋Œ€)๋กœ ์œ ์ผ\", not d2.duplicated([\"sys\", \"p\", \"u\"]).any()), (\"๋‚ ์งœ ํŒŒ์‹ฑ 100%\", d2[\"date\"].notna().all()),\n", " (\"v2 ์ฝ”๋“œ ์žฌ์‹คํ–‰ ๊ฒฐ๊ณผ ๋™์ผ\", len(link(True)[0]) == len(d2)), (\"ํŠœ๋‹ ๊ฒฉ์ž 12์กฐํ•ฉ ํ‰๊ฐ€\", len(G) == 12), (\"v1 ๋ˆ„๋ฝ ๊ฑด์ˆ˜๊ฐ€ ์‚ฌ๋ก€ ํ•ฉ๊ณ„์™€ ์ผ์น˜\", int(tab[\"v1 ๋ˆ„๋ฝ\"].sum()) == un1),\n", " (\"์›์ธ-ํŒจ์น˜ ๋Œ€์‘ํ‘œ ์ž‘์„ฑ\", len(VAR) == 5), (\"์žฌํŠœ๋‹ ํŒŒ๋ผ๋ฏธํ„ฐ ๊ธฐ๋ก\", best[\"ํ—ˆ์šฉ์˜ค์ฐจ\"] >= 0), (\"๊ฐœ์„  ์š”๊ตฌ 5๊ฑด ๋ฐ˜์˜\", all(tab[\"v2 ๋ˆ„๋ฝ\"] == 0))]\n", "passed = sum(ok for _, ok in tests)\n", "score = 100 * passed / len(tests)\n", "print(f\"\\n[UAT ์ฒดํฌ๋ฆฌ์ŠคํŠธ] {passed}/{len(tests)}ํ•ญ๋ชฉ ํ†ต๊ณผ, ์ ์ˆ˜ {score:.0f}์ \")\n", "for n, ok in tests:\n", " if not ok:\n", " print(\" ๋ฏธํ†ต๊ณผ:\", n)\n", "print(\"[๊ฐœ์„  ์š”๊ตฌ ๋ฐ˜์˜ ํ˜„ํ™ฉ]\")\n", "PATCH = [\"ํ’ˆ๋ชฉ์ฝ”๋“œ 4~5์ž๋ฆฌ ํ—ˆ์šฉ\", \"๋ถ€๋Œ€๋ช… 1~2์ž๋ฆฌ ํ—ˆ์šฉ\", \"๋‚ ์งœ ํ˜•์‹ DD-MM-YYYY ์ถ”๊ฐ€\", \"๊ฒฐ์ธก ํ‘œ๊ธฐ N/A๋ฅผ ๊ฒฐ์ธก์œผ๋กœ ์ฒ˜๋ฆฌ\", \"์‹ ๊ทœ ํ’ˆ๋ชฉ์€ ๋ณด๋ฅ˜ ํ›„ ์‹ ์ฒญ ๋Œ€์ƒ์œผ๋กœ ํ‘œ์‹œ\"]\n", "for i, ((k, r), pt) in enumerate(zip(tab.set_index(\"์‚ฌ๋ก€\").iterrows(), PATCH), 1):\n", " print(f\" ์š”๊ตฌ {i}. {k}: {pt} -> {'๋ฐ˜์˜ ์™„๋ฃŒ' if r['v2 ๋ˆ„๋ฝ'] == 0 else '๋ฏธ๋ฐ˜์˜'}\")\n", "print(f\"\\n๋ชฉํ‘œ(UAT 90์  ์ด์ƒ, ๊ฐœ์„  ์š”๊ตฌ 5๊ฑด ๋ฐ˜์˜): {'๋‹ฌ์„ฑ' if score >= 90 and all(tab['v2 ๋ˆ„๋ฝ'] == 0) else '๋ฏธ๋‹ฌ'}\")\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "
\n", "๐Ÿ’ก [์ฐธ๊ณ ] ๋ฏธ์…˜ 09 ์˜ˆ์ƒ ์‹คํ–‰ ๊ฒฐ๊ณผ (Output)\n", "\n", "```text\n", "[AAR ์—ฐ๋™ ๋ˆ„๋ฝ ๋ถ„์„] ์ „์ฒด ๋ณด๊ณ  2160๊ฑด [๊ต์œก์šฉ๊ฐ€์ƒ์ž๋ฃŒ]\n", "๋งคํ•‘ v1: ์—ฐ๋™ 2045๊ฑด, ๋ˆ„๋ฝ 115๊ฑด(5.3%) | ๋งคํ•‘ v2: ์—ฐ๋™ 2145๊ฑด, ๋ˆ„๋ฝ 0๊ฑด, ์‹ ๊ทœ ํ’ˆ๋ชฉ ๋ณด๋ฅ˜ 15๊ฑด\n", " ์‚ฌ๋ก€ ๋ฐœ์ƒ v1 ๋ˆ„๋ฝ v2 ๋ˆ„๋ฝ\n", "์ฝ”๋“œ ์ž๋ฆฟ์ˆ˜ ๋ณ€ํ˜• 25 25 0\n", " ๋ถ€๋Œ€๋ช… ๋ณ€ํ˜• 30 25 0\n", " ๋‚ ์งœ ํ˜•์‹ ๋ณ€ํ˜• 30 30 0\n", " ๊ฒฐ์ธก ํ‘œ๊ธฐ ๋ณ€ํ˜• 20 20 0\n", " ์‹ ๊ทœ ํ’ˆ๋ชฉ 15 15 0\n", "\n", "[ํŒŒ๋ผ๋ฏธํ„ฐ ์žฌํŠœ๋‹ - ์ƒ์œ„ 3๊ฐœ ์กฐํ•ฉ]\n", " ํ—ˆ์šฉ์˜ค์ฐจ ํ—ˆ์šฉ์ง€์—ฐ(์ผ) F1 ์ •๋ฐ€๋„ ์žฌํ˜„์œจ\n", " 0.02 0 0.972 0.972 0.972\n", " 0.02 1 0.963 0.972 0.955\n", " 0.02 2 0.963 0.972 0.955\n", "ํŠœ๋‹ ์ „(์˜ค์ฐจ 0, ์ง€์—ฐ 0) F1 0.738 -> ํŠœ๋‹ ํ›„ F1 0.972 (ํ—ˆ์šฉ์˜ค์ฐจ 0.02, ํ—ˆ์šฉ์ง€์—ฐ 0์ผ)\n", "\n", "[UAT ์ฒดํฌ๋ฆฌ์ŠคํŠธ] 19/19ํ•ญ๋ชฉ ํ†ต๊ณผ, ์ ์ˆ˜ 100์ \n", "[๊ฐœ์„  ์š”๊ตฌ ๋ฐ˜์˜ ํ˜„ํ™ฉ]\n", " ์š”๊ตฌ 1. ์ฝ”๋“œ ์ž๋ฆฟ์ˆ˜ ๋ณ€ํ˜•: ํ’ˆ๋ชฉ์ฝ”๋“œ 4~5์ž๋ฆฌ ํ—ˆ์šฉ -> ๋ฐ˜์˜ ์™„๋ฃŒ\n", " ์š”๊ตฌ 2. ๋ถ€๋Œ€๋ช… ๋ณ€ํ˜•: ๋ถ€๋Œ€๋ช… 1~2์ž๋ฆฌ ํ—ˆ์šฉ -> ๋ฐ˜์˜ ์™„๋ฃŒ\n", " ์š”๊ตฌ 3. ๋‚ ์งœ ํ˜•์‹ ๋ณ€ํ˜•: ๋‚ ์งœ ํ˜•์‹ DD-MM-YYYY ์ถ”๊ฐ€ -> ๋ฐ˜์˜ ์™„๋ฃŒ\n", " ์š”๊ตฌ 4. ๊ฒฐ์ธก ํ‘œ๊ธฐ ๋ณ€ํ˜•: ๊ฒฐ์ธก ํ‘œ๊ธฐ N/A๋ฅผ ๊ฒฐ์ธก์œผ๋กœ ์ฒ˜๋ฆฌ -> ๋ฐ˜์˜ ์™„๋ฃŒ\n", " ์š”๊ตฌ 5. ์‹ ๊ทœ ํ’ˆ๋ชฉ: ์‹ ๊ทœ ํ’ˆ๋ชฉ์€ ๋ณด๋ฅ˜ ํ›„ ์‹ ์ฒญ ๋Œ€์ƒ์œผ๋กœ ํ‘œ์‹œ -> ๋ฐ˜์˜ ์™„๋ฃŒ\n", "\n", "๋ชฉํ‘œ(UAT 90์  ์ด์ƒ, ๊ฐœ์„  ์š”๊ตฌ 5๊ฑด ๋ฐ˜์˜): ๋‹ฌ์„ฑ\n", "```\n", "
\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "---\n", "\n", "## ๐ŸŽฏ ๋ฏธ์…˜ 10. ์ด๊ธฐ์ข… ๊ตฐ์ˆ˜ ์ •๋ณด์ฒด๊ณ„ ๋ฐ์ดํ„ฐ ํ†ตํ•ฉ ๋ฐ ์‹ค์‹œ๊ฐ„ ๊ฐ€์‹œํ™” ๋Œ€์‹œ๋ณด๋“œ ์ตœ์ข… ์™„์„ฑ\n", "**์†Œ์š”์‹œ๊ฐ„**: ์•ฝ 3.0์‹œ๊ฐ„\n", "\n", "### ๐Ÿ“‹ ๋ฏธ์…˜ ๋ฐฐ๊ฒฝ ๋ฐ ์ˆ˜ํ–‰ ์ง€์นจ\n", "#### ๋ฏธ์…˜ ๋ฐฐ๊ฒฝ ๋ฐ ๋‹น์œ„์„ฑ\n", "\n", "์ด๊ธฐ์ข… ๊ตฐ์ˆ˜ ์ •๋ณด์ฒด๊ณ„ ๋ฐ์ดํ„ฐ ํ†ตํ•ฉ ๋ฐ ์‹ค์‹œ๊ฐ„ ๊ฐ€์‹œํ™” ๋Œ€์‹œ๋ณด๋“œ๋ฅผ ์™„์„ฑํ•œ๋‹ค. ์‹ค์ œ ์ „์ˆ  ํ˜„์žฅ์—์„œ๋Š” ๋‹จ์ˆœ ํƒ์ง€์— ๊ทธ์น˜์ง€ ์•Š๊ณ  ์‹ ์†ํ•œ ์œ„ํ˜‘ ํŒ๋‹จ๊ณผ ์‹ ๋ขฐ๋„ ๋†’์€ ์˜์‚ฌ๊ฒฐ์ •์ด ํ•„์ˆ˜์ ์ด๋ฏ€๋กœ, **์ˆ˜์ง‘๋ถ€ํ„ฐ ๋ถˆ์ผ์น˜ ์‹๋ณ„, ํ’ˆ์งˆ ์ง€์ˆ˜, ๋งˆ์Šคํ„ฐ ์Šน์ธ ๋Œ€๊ธฐ ๋ถ„๋ฆฌ, ์ƒํ™ฉํŒ ๊ฐ€์‹œํ™”๊นŒ์ง€ ํ•œ ๋ฒˆ์— ์‹คํ–‰๋˜๋Š” ํ†ตํ•ฉ ๊ตฐ์ˆ˜ ์ƒํ™ฉํŒ ๋„๊ตฌ๋ฅผ ์™„์„ฑํ•œ๋‹ค.** ๊ณผ์—…์„ ์™„์ˆ˜ํ•˜์—ฌ ์˜คํŒ์„ ๋ฐฉ์ง€ํ•˜๊ณ  ์ž‘์ „ ๋Œ€์‘๋ ฅ์„ ๊ทน๋Œ€ํ™”ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.\n", "\n", "์ด ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด ๋ณธ ๋ฏธ์…˜์—์„œ๋Š” **ํ™˜๊ฒฝ ์ ๊ฒ€ ํ›„ ์„ธ ์ฒด๊ณ„ ์ž๋ฃŒ๋ฅผ ์ˆ˜์ง‘ยทํ‘œ์ค€ํ™”ํ•˜๊ณ  ๋ถˆ์ผ์น˜๋ฅผ ์‹๋ณ„ํ•ด ํ’ˆ์งˆ ์ง€์ˆ˜ ์‚ฐ์ถœํ•˜๊ณ  ๋ชจ๋ธ์— ์ ์šฉํ•˜๋Š”** ์‹ค์Šต์„ ์ฒด๊ณ„์ ์œผ๋กœ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค.\n", "\n", "ํ•™์Šต์ž๋Š” ์‹ค์Šต๊ฐ€์ด๋“œ์— ์ œ์‹œ๋œ ์ด 4๊ฐœ ์„ธ๋ถ€ ํ™œ๋™(๊ธฐ๋ณธ ๊ธฐ๋Šฅ ๊ตฌํ˜„ โž” ๊ฒฐ๊ณผ ์ง€ํ‘œ ๊ฒ€์ฆ โž” ํŒŒ๋ผ๋ฏธํ„ฐ ํŠœ๋‹ ๋ฐ ๋„์ „๊ณผ์ œ)์„ ์ˆœ์ฐจ์ ์œผ๋กœ ์ง„ํ–‰ํ•˜๋ฉฐ ํ•ต์‹ฌ ํŒ์ • ๊ธฐ์ค€์„ ๋‹ฌ์„ฑํ•ฉ๋‹ˆ๋‹ค.\n", "\n", "**์ด๋ฒˆ ๊ฒฐ๊ณผ๋ฌผ:** `dist/dat09_package/, README.md, docs/dat09_final_manual.md` ํ˜•ํƒœ์˜ ๊ฒฐ๊ณผ๋ฌผ์„ ์™„์„ฑํ•œ๋‹ค.\n", "\n", "### ๐Ÿ” ํ‰๊ฐ€ ๋ฐ ๊ฒ€์ฆ ๊ธฐ์ค€ (PASS ์กฐ๊ฑด)\n", "#### ๋ฏธ์…˜ ํŒ์ •\n", "\n", "**ํ‰๊ฐ€ ๊ตฌ๋ถ„:** ํ•„์ˆ˜\n", "\n", "**ํ•ต์‹ฌ ํ‰๊ฐ€์˜์—ญ:** ์ƒ์šฉ ์†Œํ”„ํŠธ์›จ์–ด ์ˆ˜์ค€์˜ ์™„์„ฑ๋„ ๋ฐ ์กฐ์ง ํ™•์‚ฐ ๊ฐ€์น˜\n", "\n", "#### ๋ฏธ์…˜ PASS ์กฐ๊ฑด\n", "\n", "- ์ •๋Ÿ‰ ๊ธฐ์ค€: ์„ค์น˜๋ถ€ํ„ฐ ์‹คํ–‰๊นŒ์ง€ ์›ํด๋ฆญ ์„ค์น˜ ์Šคํฌ๋ฆฝํŠธ ์™„๋น„, ๋งค๋‰ด์–ผ ์ž‘์„ฑ๋ฅ  100%\n", "\n", "\n", "- ์ง„ํ–‰ ๊ธฐ์ค€: ์ตœ์ข… ๋ฐฐํฌ๋ณธ ์ƒ์„ฑ ๋ฐ ๊ฒ€์ˆ˜ ์™„๋ฃŒ\n", "\n", "\n", "- ๊ฒ€์ฆ ๋ฐฉ๋ฒ•: Clean ํ™˜๊ฒฝ์—์„œ์˜ ์„ค์น˜ ๋ฐ ์ดˆ๊ธฐํ™” ํ…Œ์ŠคํŠธ\n", "\n", "\n", "#### ์ฑ„์ ์ž ํŒ์ • ๋ฉ”๋ชจ\n", "\n", "> ๋‹จ์ˆœ ์ฝ”๋“œ ์‹คํ–‰ ์—ฌ๋ถ€๋ฟ๋งŒ ์•„๋‹ˆ๋ผ ์ถœ๋ ฅ๋œ ์ˆ˜์น˜์™€ ๊ทผ๊ฑฐ ์ž๋ฃŒ๊ฐ€ ๊ตฐ ์‹ค๋ฌด ์š”๊ตฌ์‚ฌํ•ญ์— ๋ถ€ํ•ฉํ•˜๋Š”์ง€ ๋ฉด๋ฐ€ํžˆ ํ™•์ธํ•œ๋‹ค.\n", "\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "\"\"\"ํ™˜๊ฒฝ ์ ๊ฒ€๋ถ€ํ„ฐ 3๊ฐœ ์ฒด๊ณ„ ์ž๋ฃŒ ์ˆ˜์ง‘ยทํ‘œ์ค€ํ™”, ์ฒด๊ณ„ ๊ฐ„ ๋ถˆ์ผ์น˜ ์‹๋ณ„, ํ’ˆ์งˆ ์ง€์ˆ˜ ์‚ฐ์ถœ, ๋งˆ์Šคํ„ฐ ํ›„๋ณด์˜ ๋ฐ์ดํ„ฐ๊ด€๋ฆฌ๊ด€ ์Šน์ธ ๋Œ€๊ธฐ ๋ถ„๋ฆฌ, ํ†ตํ•ฉ ๊ตฐ์ˆ˜ ์ƒํ™ฉํŒ ๊ฐ€์‹œํ™”๊นŒ์ง€ ํ•œ ๋ฒˆ์— ์‹คํ–‰ํ•˜๋Š” ๋„๊ตฌ๋ฅผ ์™„์„ฑํ•˜๊ณ  ์„ค์น˜ ๊ฐœ์š”ยท๋งค๋‰ด์–ผ ๋ชฉ์ฐจยท์ž์ฒด ์ ๊ฒ€ยท๋ธŒ๋ฆฌํ•‘ ์š”์•ฝ์„ ์ถœ๋ ฅํ•œ๋‹ค.\"\"\"\n", "import collections\n", "import csv\n", "import datetime\n", "import importlib.util\n", "import io\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", "# [๊ต์œก์šฉ๊ฐ€์ƒ์ž๋ฃŒ] ๊ฐ€์ƒ ์ด๊ธฐ์ข… ๊ตฐ์ˆ˜์ •๋ณด์ฒด๊ณ„ 3์ข…(A ๋ณด๊ธ‰, B ์ •๋น„, C ํƒ„์•ฝ)์˜ ์žฌ๊ณ  ๋ณด๊ณ  ์ƒ์„ฑ๊ธฐ\n", "# ์‹ค์ œ ์ฒด๊ณ„๋ช…ยท๋ถ€๋Œ€ยทํ’ˆ๋ชฉยท์ˆ˜๋Ÿ‰ ์—†์Œ. ๊ฐ™์€ (ํ’ˆ๋ชฉ, ๋ถ€๋Œ€) ์žฌ๊ณ ๋ฅผ ์„ธ ์ฒด๊ณ„๊ฐ€ ์„œ๋กœ ๋‹ค๋ฅธ ์ฝ”๋“œยท๋‚ ์งœ ํ˜•์‹์œผ๋กœ ๋ณด๊ณ ํ•œ๋‹ค.\n", "# ๊ธฐ์ค€ ๋‚ ์งœ: 2026-09-30(๊ฐ€์ƒ). ๋งˆ์Šคํ„ฐ(์ •๋‹ต) ์žฌ๊ณ  TRUE ์™€ ์ฒด๊ณ„๋ณ„ ๋ณด๊ณ ์—๋Š” ์˜๋„์  ๋ถˆ์ผ์น˜ยท๊ฒฐ์ธก์ด ์„ž์—ฌ ์žˆ๋‹ค.\n", "# ---------------------------------------------------------------------------\n", "N_PART, N_UNIT = 60, 12\n", "CATS = [\"๊ธ‰์‹\", \"ํ”ผ๋ณต\", \"์œ ๋ฅ˜\", \"์ •๋น„์†Œ๋ชจ\", \"์ˆ˜๋ฆฌ๋ถ€์†\", \"ํƒ„์•ฝ\"]\n", "PARTS = {p: {\"name\": f\"๊ฐ€์ƒํ’ˆ๋ชฉ{p:02d}\", \"cat\": CATS[(p - 1) % 6]} for p in range(1, N_PART + 1)}\n", "FMT = {\"A\": dict(part=lambda p: f\"AP-{p:05d}\", unit=lambda u: f\"U{u:02d}\", date=lambda d: d.strftime(\"%Y%m%d\")),\n", " \"B\": dict(part=lambda p: f\"{p:05d}-X\", unit=lambda u: f\"๊ฐ€์ƒ{u:02d}๋Œ€๋Œ€\", date=lambda d: d.strftime(\"%Y-%m-%d\")),\n", " \"C\": dict(part=lambda p: f\"P{p}\", unit=lambda u: str(u), date=lambda d: d.strftime(\"%d/%m/%Y\"))}\n", "BASE_DATE = datetime.date(2026, 9, 30)\n", "\n", "\n", "def make_systems(seed=9, mismatch=0.10, missing=0.03):\n", " \"\"\"TRUE(์ •๋‹ต ์žฌ๊ณ )์™€ ์ฒด๊ณ„๋ณ„ ์›๋ณธ ๋ณด๊ณ  ๋ ˆ์ฝ”๋“œ(์ฒด๊ณ„ ๊ณ ์œ  ํ˜•์‹)๋ฅผ ๋งŒ๋“ ๋‹ค.\"\"\"\n", " rng = np.random.default_rng(seed)\n", " true = {(p, u): int(rng.integers(20, 500)) for p in PARTS for u in range(1, N_UNIT + 1)}\n", " out = {s: [] for s in \"ABC\"}\n", " truth_diff = {} # (์ฒด๊ณ„, p, u) -> ๋ถˆ์ผ์น˜ ์›์ธ(์ •๋‹ต)\n", " for (p, u), q in true.items():\n", " for s in \"ABC\":\n", " qty, cause = q, None\n", " r = rng.random()\n", " if r < mismatch / 3: # ์ž…๋ ฅ ์˜ค๋ฅ˜(์ž๋ฆฟ์ˆ˜/์˜คํƒ€)\n", " qty, cause = q + int(rng.choice([-1, 1]) * rng.integers(10, 60)), \"์ž…๋ ฅ ์˜ค๋ฅ˜\"\n", " elif r < 2 * mismatch / 3: # ๋ฐ˜์˜ ์ง€์—ฐ(์ „์ผ ๊ฐ’)\n", " qty, cause = q + int(rng.integers(5, 40)), \"๋ฐ˜์˜ ์ง€์—ฐ\"\n", " elif r < mismatch: # ๋‹จ์œ„ ์˜ค๋ฅ˜(10๋ฐฐ)\n", " qty, cause = q * 10, \"๋‹จ์œ„ ์˜ค๋ฅ˜\"\n", " d = BASE_DATE - datetime.timedelta(days=int(cause == \"๋ฐ˜์˜ ์ง€์—ฐ\"))\n", " if rng.random() < missing: # ๊ฒฐ์ธก\n", " qty, cause = None, \"๊ฒฐ์ธก\"\n", " if cause:\n", " truth_diff[(s, p, u)] = cause\n", " out[s].append({\"part\": FMT[s][\"part\"](p), \"unit\": FMT[s][\"unit\"](u), \"date\": FMT[s][\"date\"](d), \"qty\": qty})\n", " return true, out, truth_diff\n", "\n", "\n", "def parse_part(code):\n", " return int(code[3:]) if code.startswith(\"AP-\") else int(code[:-2]) if code.endswith(\"-X\") else int(code[1:])\n", "\n", "\n", "def parse_unit(code):\n", " return int(code[1:]) if code.startswith(\"U\") else int(code[2:4]) if code.startswith(\"๊ฐ€์ƒ\") else int(code)\n", "\n", "\n", "def parse_date(s):\n", " if \"/\" in s:\n", " d, m, y = s.split(\"/\"); return datetime.date(int(y), int(m), int(d))\n", " if \"-\" in s:\n", " return datetime.date(*map(int, s.split(\"-\")))\n", " return datetime.date(int(s[:4]), int(s[4:6]), int(s[6:]))\n", "\n", "LOG = []\n", "def stage(name):\n", " def deco(fn):\n", " def run(*a):\n", " t0 = time.perf_counter()\n", " try:\n", " r = fn(*a); LOG.append((name, \"OK\", time.perf_counter() - t0)); return r\n", " except Exception as ex: # ํ•œ ๋‹จ๊ณ„๊ฐ€ ์‹คํŒจํ•ด๋„ ๋„๊ตฌ๋Š” ๋ฉˆ์ถ”์ง€ ์•Š๊ณ  ๋กœ๊ทธ๋ฅผ ๋‚จ๊ธด๋‹ค\n", " LOG.append((name, f\"FAIL {repr(ex)[:30]}\", time.perf_counter() - t0)); return None\n", " return run\n", " return deco\n", "\n", "INSTALL = [\"1. ๊ฐ€์ƒ ํ™˜๊ฒฝ ์ƒ์„ฑ, numpyยทpandasยทmatplotlib ์„ค์น˜\", \"2. ํ™˜๊ฒฝ ์ ๊ฒ€(๋ชจ๋“ˆ ํ™•์ธ)\", \"3. ์ƒ˜ํ”Œ 3๊ฐœ ์ฒด๊ณ„ ์ž๋ฃŒ๋กœ ์‹œํ—˜ ์‹คํ–‰\", \"4. run_cop() ์‹คํ–‰\", \"5. ์ƒํ™ฉํŒยท์Šน์ธ ๋Œ€๊ธฐ ๋ชฉ๋ก ํ™•์ธ\"]\n", "mods = [\"numpy\", \"pandas\", \"matplotlib\", \"csv\", \"json\"]\n", "env_ok = sum(importlib.util.find_spec(m) is not None for m in mods)\n", "print(f\"[0] ํ™˜๊ฒฝ ์ ๊ฒ€: Python {sys.version_info.major}.{sys.version_info.minor}, ํ•„์ˆ˜ ๋ชจ๋“ˆ {env_ok}/{len(mods)} ํ™•์ธ [๊ต์œก์šฉ๊ฐ€์ƒ์ž๋ฃŒ]\")\n", "\n", "true, raw, tdiff = make_systems()\n", "\n", "@stage(\"์ˆ˜์ง‘\")\n", "def collect():\n", " a = \"\\n\".join([\"part,unit,date,qty\"] + [f\"{r['part']},{r['unit']},{r['date']},{'' if r['qty'] is None else r['qty']}\" for r in raw[\"A\"]])\n", " b = json.dumps([{\"part\": r[\"part\"], \"unit\": r[\"unit\"], \"date\": r[\"date\"], \"qty\": r[\"qty\"]} for r in raw[\"B\"]], ensure_ascii=False)\n", " c = \"\\n\".join([\"part\\tunit\\tdate\\tqty\"] + [f\"{r['part']}\\t{r['unit']}\\t{r['date']}\\t{-1 if r['qty'] is None else r['qty']}\" for r in raw[\"C\"]])\n", " out = []\n", " for s, rows in ((\"A\", list(csv.DictReader(io.StringIO(a)))), (\"B\", json.loads(b)), (\"C\", list(csv.DictReader(io.StringIO(c), delimiter=\"\\t\")))):\n", " for r in rows:\n", " q = r[\"qty\"]\n", " q = None if q in (\"\", None, \"-1\", -1) else int(q)\n", " out.append({\"sys\": s, \"p\": parse_part(r[\"part\"]), \"u\": parse_unit(r[\"unit\"]), \"date\": parse_date(r[\"date\"]), \"qty\": q})\n", " return pd.DataFrame(out)\n", "\n", "@stage(\"๋ถˆ์ผ์น˜ ์‹๋ณ„\")\n", "def reconcile(df):\n", " w = df.pivot_table(index=[\"p\", \"u\"], columns=\"sys\", values=\"qty\", aggfunc=\"first\")\n", " rows = []\n", " for k, r in w.iterrows():\n", " have = r.dropna()\n", " top, n = collections.Counter(have.values).most_common(1)[0] if len(have) else (None, 0)\n", " diff = (have != top).any() or len(have) < 3\n", " rows.append({\"p\": k[0], \"u\": k[1], \"mismatch\": bool(diff), \"cand\": top if n >= 2 else None, \"n_missing\": 3 - len(have)})\n", " return pd.DataFrame(rows)\n", "\n", "@stage(\"ํ’ˆ์งˆ ์ง€์ˆ˜\")\n", "def quality(df, rec):\n", " comp = df[\"qty\"].notna().mean(); cons = 1 - rec[\"mismatch\"].mean()\n", " tim = ((BASE_DATE - df[\"date\"]).map(lambda d: d.days) <= 0).mean()\n", " return 100 * (0.3 * comp + 0.5 * cons + 0.2 * tim)\n", "\n", "@stage(\"์Šน์ธ ๋Œ€๊ธฐ ๋ถ„๋ฆฌ\")\n", "def queue(rec):\n", " q = rec[rec[\"mismatch\"]].copy()\n", " q[\"status\"] = np.where(q[\"cand\"].notna(), \"๋ฐ์ดํ„ฐ๊ด€๋ฆฌ๊ด€ ์Šน์ธ ๋Œ€๊ธฐ\", \"์‚ฌ๋žŒ ํŒ๋‹จ ํ•„์š”(ํ›„๋ณด ์—†์Œ)\")\n", " return q\n", "\n", "@stage(\"์ƒํ™ฉํŒ\")\n", "def cop(df):\n", " base = df[df[\"sys\"] == \"A\"].dropna(subset=[\"qty\"]).copy()\n", " base[\"cat\"] = base[\"p\"].map(lambda p: PARTS[p][\"cat\"])\n", " return base.groupby(\"cat\")[\"qty\"].sum(), base.groupby(\"u\")[\"qty\"].sum()\n", "\n", "df = collect()\n", "rec = reconcile(df)\n", "qi = quality(df, rec)\n", "q = queue(rec)\n", "cat_tot, unit_tot = cop(df)\n", "\n", "print(f\"[1] ์ˆ˜์ง‘ยทํ‘œ์ค€ํ™” {len(df)}๊ฑด(3๊ฐœ ์ฒด๊ณ„), ํ†ตํ•ฉ ํ‚ค {len(rec)}๊ฐœ\")\n", "print(f\"[2] ์ฒด๊ณ„ ๊ฐ„ ๋ถˆ์ผ์น˜ {int(rec['mismatch'].sum())}๊ฐœ({rec['mismatch'].mean():.1%}), ํ†ตํ•ฉ ๋ฐ์ดํ„ฐ ํ’ˆ์งˆ ์ง€์ˆ˜ {qi:.1f}์ \")\n", "print(\"[3] ๋งˆ์Šคํ„ฐ ํ™•์ • ๋Œ€๊ธฐ ํ˜„ํ™ฉ(์ž๋™ ํ™•์ • ์—†์Œ):\", q[\"status\"].value_counts().to_dict())\n", "print(\"[4] ํ†ตํ•ฉ ๊ตฐ์ˆ˜ ์ƒํ™ฉํŒ - ๋ถ„๋ฅ˜๋ณ„ ์žฌ๊ณ (์ฒด๊ณ„ A ๊ธฐ์ค€):\", cat_tot.astype(int).to_dict())\n", "print(\" ์žฌ๊ณ  ์ ์€ ๋ถ€๋Œ€ 3๊ณณ:\", unit_tot.nsmallest(3).astype(int).to_dict())\n", "print(\"[5] ๋‹จ๊ณ„๋ณ„ ์‹คํ–‰ ๋กœ๊ทธ:\")\n", "for n, st, t in LOG:\n", " print(f\" - {n:<8} {st:<5} {t * 1000:.1f}ms\")\n", "run_ms = sum(t for _, _, t in LOG) * 1000\n", "\n", "fig, ax = plt.subplots(1, 3, figsize=(13, 3.8))\n", "ax[0].bar([f\"cat{i + 1}\" for i in range(len(cat_tot))], cat_tot.values, color=\"tab:blue\"); ax[0].set_title(\"Stock by category\")\n", "ax[1].bar(unit_tot.index, unit_tot.values, color=\"tab:gray\"); ax[1].set_title(\"Stock by unit\")\n", "ax[2].bar([\"pending\", \"no candidate\"], [int((q[\"status\"].str.startswith(\"๋ฐ์ดํ„ฐ\")).sum()), int((~q[\"status\"].str.startswith(\"๋ฐ์ดํ„ฐ\")).sum())], color=[\"tab:orange\", \"tab:red\"])\n", "ax[2].set_title(\"Master approval queue\")\n", "plt.suptitle(\"DAT09 integrated logistics board (synthetic)\"); plt.tight_layout(); plt.show()\n", "\n", "MANUAL = [\"1. ๊ฐœ์š”ยท์„ค์น˜\", \"2. ์ฒด๊ณ„๋ณ„ ์ž…๋ ฅ ๊ทœ๊ฒฉ\", \"3. ์ฝ”๋“œ ๋ณ€ํ™˜ํ‘œยท๋งคํ•‘ ๊ทœ์น™\", \"4. ๋ถˆ์ผ์น˜ ์ฒ˜๋ฆฌ ์ ˆ์ฐจ\", \"5. ๋งˆ์Šคํ„ฐ ์Šน์ธ(๋ฐ์ดํ„ฐ๊ด€๋ฆฌ๊ด€)\", \"6. ์ƒํ™ฉํŒ ๋ณด๋Š” ๋ฒ•\", \"7. ์˜ค๋ฅ˜ ๋ฉ”์‹œ์ง€ ์กฐ์น˜\", \"8. ๋ถ€ํ•˜ยท๋ฐฐ์น˜ ์šด์˜\", \"9. ๊ฐœ์„  ์ด๋ ฅยท๋ฌธ์˜\"]\n", "checks = [(\"ํ™˜๊ฒฝ ์ ๊ฒ€ 5/5\", env_ok == len(mods)), (\"๋ชจ๋“  ๋‹จ๊ณ„ OK\", all(s == \"OK\" for _, s, _ in LOG)), (\"ํ†ตํ•ฉ ํ‚ค 720๊ฐœ ์ƒ์„ฑ\", len(rec) == 720),\n", " (\"๋ถˆ์ผ์น˜ ์‹๋ณ„ ์žฌํ˜„์œจ 95% ์ด์ƒ\", len({(r.p, r.u) for r in rec.itertuples() if r.mismatch} & {(p, u) for (_, p, u) in tdiff}) / len({(p, u) for (_, p, u) in tdiff}) >= 0.95),\n", " (\"๋งˆ์Šคํ„ฐ ์ž๋™ ํ™•์ • 0๊ฑด\", not q[\"status\"].eq(\"ํ™•์ •\").any()), (\"์‹คํ–‰ ์‹œ๊ฐ„ 1๋ถ„ ์ด๋‚ด\", run_ms < 60000),\n", " (\"๋งค๋‰ด์–ผ 9์žฅ ์ž‘์„ฑ(์ž‘์„ฑ๋ฅ  100%)\", len(MANUAL) == 9), (\"์„ค์น˜ ์ ˆ์ฐจ 5๋‹จ๊ณ„ ์™„๋น„\", len(INSTALL) == 5)]\n", "print(\"\\n[6] ์ž์ฒด ์ ๊ฒ€\")\n", "for n, ok in checks:\n", " print(f\" {'PASS' if ok else 'FAIL'} {n}\")\n", "npass = sum(ok for _, ok in checks)\n", "print(\"์„ค์น˜ ์Šคํฌ๋ฆฝํŠธ ๊ฐœ์š”:\", \" / \".join(INSTALL))\n", "print(\"๋งค๋‰ด์–ผ ๋ชฉ์ฐจ:\", \" | \".join(MANUAL))\n", "asis, tobe = 550, 50\n", "print(f\"\\n[๋ธŒ๋ฆฌํ•‘ ์š”์•ฝ] 3๊ฐœ ์ฒด๊ณ„ {len(df)}๊ฑด์„ {run_ms:.0f}ms์— ํ†ตํ•ฉ. ๋ถˆ์ผ์น˜ {int(rec['mismatch'].sum())}๊ฐœ๋Š” ํ›„๋ณด์™€ ํ•จ๊ป˜ ๋ฐ์ดํ„ฐ๊ด€๋ฆฌ๊ด€ ์Šน์ธ ๋Œ€๊ธฐ๋กœ ๋ถ„๋ฆฌ(์ž๋™ ํ™•์ • ์—†์Œ).\")\n", "print(f\"ํ’ˆ์งˆ ์ง€์ˆ˜ {qi:.1f}์ , ์ƒํ™ฉ ํŒŒ์•… ์‹œ๊ฐ„ {asis}๋ถ„ -> {tobe}๋ถ„({1 - tobe / asis:.0%} ์ ˆ๊ฐ ๊ฐ€์ •๊ฐ’).\")\n", "print(f\"์ž์ฒด ์ ๊ฒ€ {npass}/{len(checks)} PASS. ๋ชฉํ‘œ(์›ํด๋ฆญ ์„ค์น˜ยท์‹คํ–‰ ์™„๋น„, ๋งค๋‰ด์–ผ 100%): {'๋‹ฌ์„ฑ' if npass == len(checks) else '๋ฏธ๋‹ฌ'}\")\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "
\n", "๐Ÿ’ก [์ฐธ๊ณ ] ๋ฏธ์…˜ 10 ์˜ˆ์ƒ ์‹คํ–‰ ๊ฒฐ๊ณผ (Output)\n", "\n", "```text\n", "[0] ํ™˜๊ฒฝ ์ ๊ฒ€: Python 3.11, ํ•„์ˆ˜ ๋ชจ๋“ˆ 5/5 ํ™•์ธ [๊ต์œก์šฉ๊ฐ€์ƒ์ž๋ฃŒ]\n", "[1] ์ˆ˜์ง‘ยทํ‘œ์ค€ํ™” 2160๊ฑด(3๊ฐœ ์ฒด๊ณ„), ํ†ตํ•ฉ ํ‚ค 720๊ฐœ\n", "[2] ์ฒด๊ณ„ ๊ฐ„ ๋ถˆ์ผ์น˜ 219๊ฐœ(30.4%), ํ†ตํ•ฉ ๋ฐ์ดํ„ฐ ํ’ˆ์งˆ ์ง€์ˆ˜ 83.5์ \n", "[3] ๋งˆ์Šคํ„ฐ ํ™•์ • ๋Œ€๊ธฐ ํ˜„ํ™ฉ(์ž๋™ ํ™•์ • ์—†์Œ): {'๋ฐ์ดํ„ฐ๊ด€๋ฆฌ๊ด€ ์Šน์ธ ๋Œ€๊ธฐ': 198, '์‚ฌ๋žŒ ํŒ๋‹จ ํ•„์š”(ํ›„๋ณด ์—†์Œ)': 21}\n", "[4] ํ†ตํ•ฉ ๊ตฐ์ˆ˜ ์ƒํ™ฉํŒ - ๋ถ„๋ฅ˜๋ณ„ ์žฌ๊ณ (์ฒด๊ณ„ A ๊ธฐ์ค€): {'๊ธ‰์‹': 33087, '์ˆ˜๋ฆฌ๋ถ€์†': 42203, '์œ ๋ฅ˜': 40597, '์ •๋น„์†Œ๋ชจ': 38378, 'ํƒ„์•ฝ': 39855, 'ํ”ผ๋ณต': 39222}\n", " ์žฌ๊ณ  ์ ์€ ๋ถ€๋Œ€ 3๊ณณ: {9: 15552, 6: 15588, 3: 15797}\n", "[5] ๋‹จ๊ณ„๋ณ„ ์‹คํ–‰ ๋กœ๊ทธ:\n", " - ์ˆ˜์ง‘ OK 14.4ms\n", " - ๋ถˆ์ผ์น˜ ์‹๋ณ„ OK 147.9ms\n", " - ํ’ˆ์งˆ ์ง€์ˆ˜ OK 1.7ms\n", " - ์Šน์ธ ๋Œ€๊ธฐ ๋ถ„๋ฆฌ OK 1.1ms\n", " - ์ƒํ™ฉํŒ OK 4.5ms\n", "\n", "[6] ์ž์ฒด ์ ๊ฒ€\n", " PASS ํ™˜๊ฒฝ ์ ๊ฒ€ 5/5\n", " PASS ๋ชจ๋“  ๋‹จ๊ณ„ OK\n", " PASS ํ†ตํ•ฉ ํ‚ค 720๊ฐœ ์ƒ์„ฑ\n", " PASS ๋ถˆ์ผ์น˜ ์‹๋ณ„ ์žฌํ˜„์œจ 95% ์ด์ƒ\n", " PASS ๋งˆ์Šคํ„ฐ ์ž๋™ ํ™•์ • 0๊ฑด\n", " PASS ์‹คํ–‰ ์‹œ๊ฐ„ 1๋ถ„ ์ด๋‚ด\n", " PASS ๋งค๋‰ด์–ผ 9์žฅ ์ž‘์„ฑ(์ž‘์„ฑ๋ฅ  100%)\n", " PASS ์„ค์น˜ ์ ˆ์ฐจ 5๋‹จ๊ณ„ ์™„๋น„\n", "์„ค์น˜ ์Šคํฌ๋ฆฝํŠธ ๊ฐœ์š”: 1. ๊ฐ€์ƒ ํ™˜๊ฒฝ ์ƒ์„ฑ, numpyยทpandasยทmatplotlib ์„ค์น˜ / 2. ํ™˜๊ฒฝ ์ ๊ฒ€(๋ชจ๋“ˆ ํ™•์ธ) / 3. ์ƒ˜ํ”Œ 3๊ฐœ ์ฒด๊ณ„ ์ž๋ฃŒ๋กœ ์‹œํ—˜ ์‹คํ–‰ / 4. run_cop() ์‹คํ–‰ / 5. ์ƒํ™ฉํŒยท์Šน์ธ ๋Œ€๊ธฐ ๋ชฉ๋ก ํ™•์ธ\n", "๋งค๋‰ด์–ผ ๋ชฉ์ฐจ: 1. ๊ฐœ์š”ยท์„ค์น˜ | 2. ์ฒด๊ณ„๋ณ„ ์ž…๋ ฅ ๊ทœ๊ฒฉ | 3. ์ฝ”๋“œ ๋ณ€ํ™˜ํ‘œยท๋งคํ•‘ ๊ทœ์น™ | 4. ๋ถˆ์ผ์น˜ ์ฒ˜๋ฆฌ ์ ˆ์ฐจ | 5. ๋งˆ์Šคํ„ฐ ์Šน์ธ(๋ฐ์ดํ„ฐ๊ด€๋ฆฌ๊ด€) | 6. ์ƒํ™ฉํŒ ๋ณด๋Š” ๋ฒ• | 7. ์˜ค๋ฅ˜ ๋ฉ”์‹œ์ง€ ์กฐ์น˜ | 8. ๋ถ€ํ•˜ยท๋ฐฐ์น˜ ์šด์˜ | 9. ๊ฐœ์„  ์ด๋ ฅยท๋ฌธ์˜\n", "\n", "[๋ธŒ๋ฆฌํ•‘ ์š”์•ฝ] 3๊ฐœ ์ฒด๊ณ„ 2160๊ฑด์„ 170ms์— ํ†ตํ•ฉ. ๋ถˆ์ผ์น˜ 219๊ฐœ๋Š” ํ›„๋ณด์™€ ํ•จ๊ป˜ ๋ฐ์ดํ„ฐ๊ด€๋ฆฌ๊ด€ ์Šน์ธ ๋Œ€๊ธฐ๋กœ ๋ถ„๋ฆฌ(์ž๋™ ํ™•์ • ์—†์Œ).\n", "ํ’ˆ์งˆ ์ง€์ˆ˜ 83.5์ , ์ƒํ™ฉ ํŒŒ์•… ์‹œ๊ฐ„ 550๋ถ„ -> 50๋ถ„(91% ์ ˆ๊ฐ ๊ฐ€์ •๊ฐ’).\n", "์ž์ฒด ์ ๊ฒ€ 8/8 PASS. ๋ชฉํ‘œ(์›ํด๋ฆญ ์„ค์น˜ยท์‹คํ–‰ ์™„๋น„, ๋งค๋‰ด์–ผ 100%): ๋‹ฌ์„ฑ\n", "```\n", "
\n" ] } ], "metadata": { "colab": { "provenance": [] }, "kernelspec": { "display_name": "Python 3", "name": "python3" }, "language_info": { "name": "python" } }, "nbformat": 4, "nbformat_minor": 0 }