{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# ๐ฏ [DAT09] ๊ตฐ์๋ฐ์ดํฐ ์ ๋ต๊ธฐํยทํ๊ฒฝํ๊ฐ ๋์๋ณด๋\n",
"### Google Colab ์ค์ต ํ๊ฒฝ (๋ฐ์ดํฐยท์๋ํยท์
๋ฌดํ๋ซํผ / Level 2)\n",
"\n",
"[](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",
" # [TODO : ํ์ ์ค์ต 1] ์ด๊ธฐ์ข
๊ตฐ์ ์ ๋ณด์ฒด๊ณ(DELIIS, ์ฅ๋น์ ๋น์ ๋ณด์ฒด๊ณ, ํ์ฝ์ฒด๊ณ) ๋ฐ์ดํฐ ์ฐ๋ ๊ท๊ฒฉ ๋ถ์ ๊ด๋ จ ํต์ฌ ๋ก์ง์ ๊ตฌํํ์ธ์.\n",
" # ํํธ: ์๋จ ์ฃผ์ ๋ฐ ๋ฐ์ดํฐ ์คํค๋ง/์ง์นจ์ ์ฐธ๊ณ ํ์ฌ ์์ฑํฉ๋๋ค.\n",
" # <<< ์ฌ๊ธฐ์ ์ฝ๋๋ฅผ ์์ฑํ์ธ์ >>>\n",
" pass # ์์ฑ ํ pass๋ฅผ ์ ๊ฑฐํ์ธ์.\n",
"def parse_unit(code):\n",
" # [TODO : ํ์ ์ค์ต 1] ์ด๊ธฐ์ข
๊ตฐ์ ์ ๋ณด์ฒด๊ณ(DELIIS, ์ฅ๋น์ ๋น์ ๋ณด์ฒด๊ณ, ํ์ฝ์ฒด๊ณ) ๋ฐ์ดํฐ ์ฐ๋ ๊ท๊ฒฉ ๋ถ์ ๊ด๋ จ ํต์ฌ ๋ก์ง์ ๊ตฌํํ์ธ์.\n",
" # ํํธ: ์๋จ ์ฃผ์ ๋ฐ ๋ฐ์ดํฐ ์คํค๋ง/์ง์นจ์ ์ฐธ๊ณ ํ์ฌ ์์ฑํฉ๋๋ค.\n",
" # <<< ์ฌ๊ธฐ์ ์ฝ๋๋ฅผ ์์ฑํ์ธ์ >>>\n",
" pass # ์์ฑ ํ pass๋ฅผ ์ ๊ฑฐํ์ธ์.\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",
" # [TODO : ํ์ ์ค์ต 2] ์๋ก ๋ค๋ฅธ ๋ถํ์ฝ๋, ๋ถ๋์ฝ๋, ๋ ์ง ํฌ๋งท ํตํฉ ๋ฐ ๊ฒฐ์ธก ๋ฐ์ดํฐ ์ ์ฒ๋ฆฌ ํ์ดํ๋ผ์ธ ๊ตฌ์ถ ๊ด๋ จ ํต์ฌ ๋ก์ง์ ๊ตฌํํ์ธ์.\n",
" # ํํธ: ์๋จ ์ฃผ์ ๋ฐ ๋ฐ์ดํฐ ์คํค๋ง/์ง์นจ์ ์ฐธ๊ณ ํ์ฌ ์์ฑํฉ๋๋ค.\n",
" # <<< ์ฌ๊ธฐ์ ์ฝ๋๋ฅผ ์์ฑํ์ธ์ >>>\n",
" pass # ์์ฑ ํ pass๋ฅผ ์ ๊ฑฐํ์ธ์.\n",
"def parse_unit(code):\n",
" # [TODO : ํ์ ์ค์ต 2] ์๋ก ๋ค๋ฅธ ๋ถํ์ฝ๋, ๋ถ๋์ฝ๋, ๋ ์ง ํฌ๋งท ํตํฉ ๋ฐ ๊ฒฐ์ธก ๋ฐ์ดํฐ ์ ์ฒ๋ฆฌ ํ์ดํ๋ผ์ธ ๊ตฌ์ถ ๊ด๋ จ ํต์ฌ ๋ก์ง์ ๊ตฌํํ์ธ์.\n",
" # ํํธ: ์๋จ ์ฃผ์ ๋ฐ ๋ฐ์ดํฐ ์คํค๋ง/์ง์นจ์ ์ฐธ๊ณ ํ์ฌ ์์ฑํฉ๋๋ค.\n",
" # <<< ์ฌ๊ธฐ์ ์ฝ๋๋ฅผ ์์ฑํ์ธ์ >>>\n",
" pass # ์์ฑ ํ pass๋ฅผ ์ ๊ฑฐํ์ธ์.\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",
" # [TODO : ํ์ ์ค์ต 3] ํตํฉ ๋ฐ์ดํฐ๋ฒ ์ด์ค 1์ฐจ ๊ตฌ์ถ ๋ฐ ํต์ฌ ๊ตฐ์ ์งํ ์ง๊ณ Baseline ์ฟผ๋ฆฌ ๋ชจ๋ ๊ตฌํ ๊ด๋ จ ํต์ฌ ๋ก์ง์ ๊ตฌํํ์ธ์.\n",
" # ํํธ: ์๋จ ์ฃผ์ ๋ฐ ๋ฐ์ดํฐ ์คํค๋ง/์ง์นจ์ ์ฐธ๊ณ ํ์ฌ ์์ฑํฉ๋๋ค.\n",
" # <<< ์ฌ๊ธฐ์ ์ฝ๋๋ฅผ ์์ฑํ์ธ์ >>>\n",
" pass # ์์ฑ ํ pass๋ฅผ ์ ๊ฑฐํ์ธ์.\n",
"def parse_unit(code):\n",
" # [TODO : ํ์ ์ค์ต 3] ํตํฉ ๋ฐ์ดํฐ๋ฒ ์ด์ค 1์ฐจ ๊ตฌ์ถ ๋ฐ ํต์ฌ ๊ตฐ์ ์งํ ์ง๊ณ Baseline ์ฟผ๋ฆฌ ๋ชจ๋ ๊ตฌํ ๊ด๋ จ ํต์ฌ ๋ก์ง์ ๊ตฌํํ์ธ์.\n",
" # ํํธ: ์๋จ ์ฃผ์ ๋ฐ ๋ฐ์ดํฐ ์คํค๋ง/์ง์นจ์ ์ฐธ๊ณ ํ์ฌ ์์ฑํฉ๋๋ค.\n",
" # <<< ์ฌ๊ธฐ์ ์ฝ๋๋ฅผ ์์ฑํ์ธ์ >>>\n",
" pass # ์์ฑ ํ pass๋ฅผ ์ ๊ฑฐํ์ธ์.\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",
" # [TODO : ํ์ ์ค์ต 4] ์ค์๊ฐ ๋ฐ์ดํฐ ์คํธ๋ฆฌ๋ฐ(Kafka/CDC) ๊ธฐ๋ฐ ์ด๊ธฐ์ข
๋ฐ์ดํฐ ์ ํฉ์ฑ ์ ์ง ๋ฐ ์๋ ๋๊ธฐํ ๊ณ ๋ํ ๊ด๋ จ ํต์ฌ ๋ก์ง์ ๊ตฌํํ์ธ์.\n",
" # ํํธ: ์๋จ ์ฃผ์ ๋ฐ ๋ฐ์ดํฐ ์คํค๋ง/์ง์นจ์ ์ฐธ๊ณ ํ์ฌ ์์ฑํฉ๋๋ค.\n",
" # <<< ์ฌ๊ธฐ์ ์ฝ๋๋ฅผ ์์ฑํ์ธ์ >>>\n",
" pass # ์์ฑ ํ pass๋ฅผ ์ ๊ฑฐํ์ธ์.\n",
"def parse_unit(code):\n",
" # [TODO : ํ์ ์ค์ต 4] ์ค์๊ฐ ๋ฐ์ดํฐ ์คํธ๋ฆฌ๋ฐ(Kafka/CDC) ๊ธฐ๋ฐ ์ด๊ธฐ์ข
๋ฐ์ดํฐ ์ ํฉ์ฑ ์ ์ง ๋ฐ ์๋ ๋๊ธฐํ ๊ณ ๋ํ ๊ด๋ จ ํต์ฌ ๋ก์ง์ ๊ตฌํํ์ธ์.\n",
" # ํํธ: ์๋จ ์ฃผ์ ๋ฐ ๋ฐ์ดํฐ ์คํค๋ง/์ง์นจ์ ์ฐธ๊ณ ํ์ฌ ์์ฑํฉ๋๋ค.\n",
" # <<< ์ฌ๊ธฐ์ ์ฝ๋๋ฅผ ์์ฑํ์ธ์ >>>\n",
" pass # ์์ฑ ํ pass๋ฅผ ์ ๊ฑฐํ์ธ์.\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",
" # [TODO : ํ์ ์ค์ต 5] ์ฒด๊ณ ๊ฐ ๋ถ์ผ์น ๋ฐ์ดํฐ ์๋ ์๋ณ ๋ฐ ๋ฐ์ดํฐ ์ ๋ขฐ๋ ํ์ง ์ง์ ์ฐ์ถ ์์ง ๊ฐ๋ฐ ๊ด๋ จ ํต์ฌ ๋ก์ง์ ๊ตฌํํ์ธ์.\n",
" # ํํธ: ์๋จ ์ฃผ์ ๋ฐ ๋ฐ์ดํฐ ์คํค๋ง/์ง์นจ์ ์ฐธ๊ณ ํ์ฌ ์์ฑํฉ๋๋ค.\n",
" # <<< ์ฌ๊ธฐ์ ์ฝ๋๋ฅผ ์์ฑํ์ธ์ >>>\n",
" pass # ์์ฑ ํ pass๋ฅผ ์ ๊ฑฐํ์ธ์.\n",
"def parse_unit(code):\n",
" # [TODO : ํ์ ์ค์ต 5] ์ฒด๊ณ ๊ฐ ๋ถ์ผ์น ๋ฐ์ดํฐ ์๋ ์๋ณ ๋ฐ ๋ฐ์ดํฐ ์ ๋ขฐ๋ ํ์ง ์ง์ ์ฐ์ถ ์์ง ๊ฐ๋ฐ ๊ด๋ จ ํต์ฌ ๋ก์ง์ ๊ตฌํํ์ธ์.\n",
" # ํํธ: ์๋จ ์ฃผ์ ๋ฐ ๋ฐ์ดํฐ ์คํค๋ง/์ง์นจ์ ์ฐธ๊ณ ํ์ฌ ์์ฑํฉ๋๋ค.\n",
" # <<< ์ฌ๊ธฐ์ ์ฝ๋๋ฅผ ์์ฑํ์ธ์ >>>\n",
" pass # ์์ฑ ํ pass๋ฅผ ์ ๊ฑฐํ์ธ์.\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",
" # [TODO : ํ์ ์ค์ต 6] ์งํ๊ด ๋ฐ ๊ตฐ์์ฐธ๋ชจ์ฉ ํตํฉ ๊ตฐ์ ์ํฉํ(Logistics COP) ์ค์๊ฐ ๋์๋ณด๋ ๊ตฌํ ๊ด๋ จ ํต์ฌ ๋ก์ง์ ๊ตฌํํ์ธ์.\n",
" # ํํธ: ์๋จ ์ฃผ์ ๋ฐ ๋ฐ์ดํฐ ์คํค๋ง/์ง์นจ์ ์ฐธ๊ณ ํ์ฌ ์์ฑํฉ๋๋ค.\n",
" # <<< ์ฌ๊ธฐ์ ์ฝ๋๋ฅผ ์์ฑํ์ธ์ >>>\n",
" pass # ์์ฑ ํ pass๋ฅผ ์ ๊ฑฐํ์ธ์.\n",
"def parse_unit(code):\n",
" # [TODO : ํ์ ์ค์ต 6] ์งํ๊ด ๋ฐ ๊ตฐ์์ฐธ๋ชจ์ฉ ํตํฉ ๊ตฐ์ ์ํฉํ(Logistics COP) ์ค์๊ฐ ๋์๋ณด๋ ๊ตฌํ ๊ด๋ จ ํต์ฌ ๋ก์ง์ ๊ตฌํํ์ธ์.\n",
" # ํํธ: ์๋จ ์ฃผ์ ๋ฐ ๋ฐ์ดํฐ ์คํค๋ง/์ง์นจ์ ์ฐธ๊ณ ํ์ฌ ์์ฑํฉ๋๋ค.\n",
" # <<< ์ฌ๊ธฐ์ ์ฝ๋๋ฅผ ์์ฑํ์ธ์ >>>\n",
" pass # ์์ฑ ํ pass๋ฅผ ์ ๊ฑฐํ์ธ์.\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",
" # [TODO : ํ์ ์ค์ต 7] ์ฒด๊ณ ๊ฐ ๋ฐ์ดํฐ ์ถฉ๋ ์ ๋ง์คํฐ ๋ฐ์ดํฐ ํ์ ์ ์ํ ๋ฐ์ดํฐ๊ด๋ฆฌ๊ด HITL ์น์ธ ์ค๊ณ ๊ด๋ จ ํต์ฌ ๋ก์ง์ ๊ตฌํํ์ธ์.\n",
" # ํํธ: ์๋จ ์ฃผ์ ๋ฐ ๋ฐ์ดํฐ ์คํค๋ง/์ง์นจ์ ์ฐธ๊ณ ํ์ฌ ์์ฑํฉ๋๋ค.\n",
" # <<< ์ฌ๊ธฐ์ ์ฝ๋๋ฅผ ์์ฑํ์ธ์ >>>\n",
" pass # ์์ฑ ํ pass๋ฅผ ์ ๊ฑฐํ์ธ์.\n",
"def parse_unit(code):\n",
" # [TODO : ํ์ ์ค์ต 7] ์ฒด๊ณ ๊ฐ ๋ฐ์ดํฐ ์ถฉ๋ ์ ๋ง์คํฐ ๋ฐ์ดํฐ ํ์ ์ ์ํ ๋ฐ์ดํฐ๊ด๋ฆฌ๊ด HITL ์น์ธ ์ค๊ณ ๊ด๋ จ ํต์ฌ ๋ก์ง์ ๊ตฌํํ์ธ์.\n",
" # ํํธ: ์๋จ ์ฃผ์ ๋ฐ ๋ฐ์ดํฐ ์คํค๋ง/์ง์นจ์ ์ฐธ๊ณ ํ์ฌ ์์ฑํฉ๋๋ค.\n",
" # <<< ์ฌ๊ธฐ์ ์ฝ๋๋ฅผ ์์ฑํ์ธ์ >>>\n",
" pass # ์์ฑ ํ pass๋ฅผ ์ ๊ฑฐํ์ธ์.\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",
" # [TODO : ํ์ ์ค์ต 8] ์๋ง ๊ฑด์ ์ด๊ธฐ์ข
๊ตฐ์ ํธ๋์ญ์
๋์ ์ ์
์๋ฎฌ๋ ์ด์
๋ถํ ํ
์คํธ ๊ด๋ จ ํต์ฌ ๋ก์ง์ ๊ตฌํํ์ธ์.\n",
" # ํํธ: ์๋จ ์ฃผ์ ๋ฐ ๋ฐ์ดํฐ ์คํค๋ง/์ง์นจ์ ์ฐธ๊ณ ํ์ฌ ์์ฑํฉ๋๋ค.\n",
" # <<< ์ฌ๊ธฐ์ ์ฝ๋๋ฅผ ์์ฑํ์ธ์ >>>\n",
" pass # ์์ฑ ํ pass๋ฅผ ์ ๊ฑฐํ์ธ์.\n",
"def parse_unit(code):\n",
" # [TODO : ํ์ ์ค์ต 8] ์๋ง ๊ฑด์ ์ด๊ธฐ์ข
๊ตฐ์ ํธ๋์ญ์
๋์ ์ ์
์๋ฎฌ๋ ์ด์
๋ถํ ํ
์คํธ ๊ด๋ จ ํต์ฌ ๋ก์ง์ ๊ตฌํํ์ธ์.\n",
" # ํํธ: ์๋จ ์ฃผ์ ๋ฐ ๋ฐ์ดํฐ ์คํค๋ง/์ง์นจ์ ์ฐธ๊ณ ํ์ฌ ์์ฑํฉ๋๋ค.\n",
" # <<< ์ฌ๊ธฐ์ ์ฝ๋๋ฅผ ์์ฑํ์ธ์ >>>\n",
" pass # ์์ฑ ํ pass๋ฅผ ์ ๊ฑฐํ์ธ์.\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",
" # [TODO : ํ์ ์ค์ต 9] AAR ๊ธฐ๋ฐ ๋ฐ์ดํฐ ์ฐ๋ ๋๋ฝ ๋ถ์ ๋ฐ ๋งคํ ํ
์ด๋ธ ํ๋ผ๋ฏธํฐ ์ฌํ๋ ๊ด๋ จ ํต์ฌ ๋ก์ง์ ๊ตฌํํ์ธ์.\n",
" # ํํธ: ์๋จ ์ฃผ์ ๋ฐ ๋ฐ์ดํฐ ์คํค๋ง/์ง์นจ์ ์ฐธ๊ณ ํ์ฌ ์์ฑํฉ๋๋ค.\n",
" # <<< ์ฌ๊ธฐ์ ์ฝ๋๋ฅผ ์์ฑํ์ธ์ >>>\n",
" pass # ์์ฑ ํ pass๋ฅผ ์ ๊ฑฐํ์ธ์.\n",
"def parse_unit(code):\n",
" # [TODO : ํ์ ์ค์ต 9] AAR ๊ธฐ๋ฐ ๋ฐ์ดํฐ ์ฐ๋ ๋๋ฝ ๋ถ์ ๋ฐ ๋งคํ ํ
์ด๋ธ ํ๋ผ๋ฏธํฐ ์ฌํ๋ ๊ด๋ จ ํต์ฌ ๋ก์ง์ ๊ตฌํํ์ธ์.\n",
" # ํํธ: ์๋จ ์ฃผ์ ๋ฐ ๋ฐ์ดํฐ ์คํค๋ง/์ง์นจ์ ์ฐธ๊ณ ํ์ฌ ์์ฑํฉ๋๋ค.\n",
" # <<< ์ฌ๊ธฐ์ ์ฝ๋๋ฅผ ์์ฑํ์ธ์ >>>\n",
" pass # ์์ฑ ํ pass๋ฅผ ์ ๊ฑฐํ์ธ์.\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",
" # [TODO : ํ์ ์ค์ต 10] ์ด๊ธฐ์ข
๊ตฐ์ ์ ๋ณด์ฒด๊ณ ๋ฐ์ดํฐ ํตํฉ ๋ฐ ์ค์๊ฐ ๊ฐ์ํ ๋์๋ณด๋ ์ต์ข
์์ฑ ๊ด๋ จ ํต์ฌ ๋ก์ง์ ๊ตฌํํ์ธ์.\n",
" # ํํธ: ์๋จ ์ฃผ์ ๋ฐ ๋ฐ์ดํฐ ์คํค๋ง/์ง์นจ์ ์ฐธ๊ณ ํ์ฌ ์์ฑํฉ๋๋ค.\n",
" # <<< ์ฌ๊ธฐ์ ์ฝ๋๋ฅผ ์์ฑํ์ธ์ >>>\n",
" pass # ์์ฑ ํ pass๋ฅผ ์ ๊ฑฐํ์ธ์.\n",
"def parse_unit(code):\n",
" # [TODO : ํ์ ์ค์ต 10] ์ด๊ธฐ์ข
๊ตฐ์ ์ ๋ณด์ฒด๊ณ ๋ฐ์ดํฐ ํตํฉ ๋ฐ ์ค์๊ฐ ๊ฐ์ํ ๋์๋ณด๋ ์ต์ข
์์ฑ ๊ด๋ จ ํต์ฌ ๋ก์ง์ ๊ตฌํํ์ธ์.\n",
" # ํํธ: ์๋จ ์ฃผ์ ๋ฐ ๋ฐ์ดํฐ ์คํค๋ง/์ง์นจ์ ์ฐธ๊ณ ํ์ฌ ์์ฑํฉ๋๋ค.\n",
" # <<< ์ฌ๊ธฐ์ ์ฝ๋๋ฅผ ์์ฑํ์ธ์ >>>\n",
" pass # ์์ฑ ํ pass๋ฅผ ์ ๊ฑฐํ์ธ์.\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
}