{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# ๐ฏ [PRC07] ๋ฐ์ดํฐ ๊ธฐ๋ฐ ๋ถํ ๊ตญ์ฐํ ํ๋ณด ์๋์ถ์ฒ Tool\n",
"### Google Colab ์ค์ต ํ๊ฒฝ (์กฐ๋ฌยท๊ณ์ฝยท๊ตญ์ฐํ / Level 3)\n",
"\n",
"[](https://colab.research.google.com/github/edunlab/milicontent2/blob/main/uploads/PRC07/PRC07_solution.ipynb)\n",
"\n",
"---\n",
"\n",
"## ๐ 1. ํ๋ก์ ํธ ๊ฐ์ ๋ฐ ๋ฐฐ๊ฒฝ\n",
"\n",
"| ํญ๋ชฉ | ๋ด์ฉ |\n",
"| :--- | :--- |\n",
"| **๊ณผ์ ๋ถ๋ฅ** | ์กฐ๋ฌยท๊ณ์ฝยท๊ตญ์ฐํ (Level 3) |\n",
"| **ํ์ต ์์ ์๊ฐ** | 10๊ฐ ๋ฏธ์
(5์ผ / ์ด 27.0์๊ฐ) |\n",
"| **ํ๊ฒฝ ์ฌ์** | Python 3.10+, numpy, pandas, matplotlib, scipy, scikit-learn |\n",
"\n",
"### ๐ ํ๋ก์ ํธ ๋ชฉํ\n",
"๋ฌด๊ธฐ์ฒด๊ณ ๋ถํ์ ํด์ธ ์์กด๋๊ฐ ๋๊ณ ๊ณต๊ธ์ ์ด ํ๋๋ฟ์ด๊ฑฐ๋ ๋จ์ข
(DMSMS) ๊ณต๊ณ ๊ฐ ๋์ค๋ฉด ์กฐ๋ฌ์ด ๋งํ๊ธฐ ์ฌ์ด๋ฐ, ์ํ ์ ํธ๋ฅผ ์ฌ๋์ด ๋ฌธ์์ ์ ํ๋ก ๋ชจ์ผ๋ฉด ๊ตญ์ฐํ ํ๋ณด๋ฅผ ์ฐพ๊ธฐ๊น์ง ์๊ฐ์์ด ๊ฑธ๋ฆฐ๋ค. ๋ณธ ํ๋ก์ ํธ๋ ๋ถํ ๊ณต๊ธ๋ง ์๋ฃ์์ ์กฐ๋ฌ ๋ณ๋ชฉ๊ณผ ์ํ ์ ํธ๋ฅผ ์ฐพ๊ณ ๊ตญ์ฐํ ๋์ฒด ํ๋ณด๋ฅผ ์๋์ผ๋ก ์ถ์ฒํ๋ ๋๊ตฌ๋ฅผ ๋ง๋๋ ๊ณผ์ ๋ก, ์
๋ฌด ๋จ๊ณ๋ณ ๋ณ๋ชฉ๊ณผ ์ํ ์ ํธ ๊ธฐ์ค, ์ถ์ฒ ์์ ์, ๊ณ์ฝ ์๋ขฐ ๊ทผ๊ฑฐ์ ๋ณ๊ฒฝ ์ด๋ ฅ, ์
๋ฌด ํจ๊ณผ ์ธก์ ๊น์ง ํ ํ๋ฆ์ผ๋ก ๊ตฌํํ๋ค. ๊ต์ก์ฉ ๊ฐ์ ๋ถํ 100์ข
๊ณผ ๊ฐ์ ๊ตญ๊ฐยท๊ฐ์ ์
์ฒด ์๋ฃ๋ก ๊ณต๊ธ๋ง ๋ฐ์ดํฐ ์คํค๋ง ์ ์, ๊ณต๊ธ์
์ฒด ์๋ฃ ์ ์ ์ ๋จ์ข
๊ณต๊ณ ํ์ฑ, ๋ฃฐ ๊ธฐ๋ฐ ์กฐ๋ฌ ์ํ ์คํฌ๋ฆฌ๋, ํ
์คํธ ์ ์ฌ๋์ ์ง์ ๊ทธ๋ํ๋ฅผ ์ด ๋์ฒด ๋ถํ ์ถ์ฒ, ๋ถ๊ดด ์ํ๋์ ์กฐ๊ธฐ๊ฒฝ๋ณด, ์ํ ์ง๋ ๋์๋ณด๋์ ์ค๋ฅ ์ฒ๋ฆฌ, ๊ท๊ฒฉ ๊ฒํ ์ ์น์ธ HITL ์ํฌํ๋ก์ฐ, ๊ณต๊ธ๋ง ์ฐจ๋จ ์คํธ๋ ์ค ์ํ, 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. ๋ฌด๊ธฐ์ฒด๊ณ ํต์ฌ ๋ถํ ๊ณต๊ธ๋ง(๋จ์ข
๋ถํ, ํด์ธ ์์กด๋, ๋ฆฌ๋ํ์) ๋ฐ์ดํฐ์
์คํค๋ง ์ ์\n",
"**์์์๊ฐ**: ์ฝ 1.5์๊ฐ\n",
"\n",
"### ๐ ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ์ํ ์ง์นจ\n",
"#### ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ๋น์์ฑ\n",
"\n",
"๋ฌด๊ธฐ์ฒด๊ณ ๋ถํ ๊ณต๊ธ๋ง(๋จ์ข
๋ถํ, ํด์ธ ์์กด๋, ๋ฆฌ๋ํ์) ๋ฐ์ดํฐ์
์คํค๋ง๋ฅผ ์ ์ํ๋ค. ์ค์ ์ ์ ํ์ฅ์์๋ ๋จ์ ํ์ง์ ๊ทธ์น์ง ์๊ณ ์ ์ํ ์ํ ํ๋จ๊ณผ ์ ๋ขฐ๋ ๋์ ์์ฌ๊ฒฐ์ ์ด ํ์์ ์ด๋ฏ๋ก, **๋ฌด๊ธฐ์ฒด๊ณ ๋ถํ ๊ณต๊ธ๋ง(๋จ์ข
๋ถํ, ํด์ธ ์์กด๋, ๋ฆฌ๋ํ์) ์๋ฃ์ ์คํค๋ง์ ์
์ถ๋ ฅ ๋ช
์ธ๋ฅผ ์ ์ํ๊ณ , ํํ(AS-IS) ๊ตญ์ฐํ ํ๋ณด ์ ์ ์
๋ฌด๋ฅผ ๋จ๊ณ๋ก ๋๋์ด ๋ณ๋ชฉ์ ์ฐพ๋๋ค.** ๊ณผ์
์ ์์ํ์ฌ ์คํ์ ๋ฐฉ์งํ๊ณ ์์ ๋์๋ ฅ์ ๊ทน๋ํํด์ผ ํฉ๋๋ค.\n",
"\n",
"์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ณธ ๋ฏธ์
์์๋ **๋ถํ ์๋ณ, ๊ณต๊ธ๋ง(๊ณต๊ธ์ ์, ํด์ธ ์์กด, ๋ฆฌ๋ํ์, ๋ฉ๊ธฐ ์ง์ฐ), ๋จ์ข
์ํ(์ต์ข
๊ตฌ๋งค ์์ฌ ๊ฐ์), ์กฐ๋ฌ ์์ฑ(์ฌ๊ณ , ์์, ๋ถ๋๋ฅ , ์์ถ ํ๊ฐ)์ ๋ฌถ์ด ํ๋ 16๊ฐ์ ์คํค๋ง์ ๊ฒ์ฆ ๊ท์น ์์ฑํ์ฌ ์์ธ์ ๋ถ์ํ๋** ์ค์ต์ ์ฒด๊ณ์ ์ผ๋ก ์ํํฉ๋๋ค.\n",
"\n",
"ํ์ต์๋ ์ค์ต๊ฐ์ด๋์ ์ ์๋ ์ด 4๊ฐ ์ธ๋ถ ํ๋(๊ธฐ๋ณธ ๊ธฐ๋ฅ ๊ตฌํ โ ๊ฒฐ๊ณผ ์งํ ๊ฒ์ฆ โ ํ๋ผ๋ฏธํฐ ํ๋ ๋ฐ ๋์ ๊ณผ์ )์ ์์ฐจ์ ์ผ๋ก ์งํํ๋ฉฐ ํต์ฌ ํ์ ๊ธฐ์ค์ ๋ฌ์ฑํฉ๋๋ค.\n",
"\n",
"**์ด๋ฒ ๊ฒฐ๊ณผ๋ฌผ:** `docs/prc07_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": [
"\"\"\"๋ฌด๊ธฐ์ฒด๊ณ ํต์ฌ ๋ถํ ๊ณต๊ธ๋ง(๋จ์ข
๋ถํ, ํด์ธ ์์กด๋, ๋ฆฌ๋ํ์) ๋ฐ์ดํฐ์
์คํค๋ง๋ฅผ ์ ์ยท๊ฒ์ฆํ๊ณ , ๊ตญ์ฐํ ํ๋ณด ์ ์ ์
๋ฌด๋ฅผ AS-IS/TO-BE ๋จ๊ณ๋ก ๋ถํดํด ๋ณ๋ชฉ ๊ตฌ๊ฐ์ ๋์ถํ๋ค.\"\"\"\n",
"import json\n",
"import re\n",
"import numpy as np\n",
"import pandas as pd\n",
"import matplotlib.pyplot as plt\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# [๊ต์ก์ฉ๊ฐ์์๋ฃ] ๊ฐ์ ๋ฌด๊ธฐ์ฒด๊ณ ๋ถํยท๊ณต๊ธ๋ง ๋ฐ์ดํฐ ์์ฑ๊ธฐ (์ค์ ์ฒด๊ณยท์
์ฒดยท๊ตญ๊ฐยท๋จ๊ฐ ์ ๋ณด ์์)\n",
"# ๋ถํ 100์ข
(ํด์ธ ์กฐ๋ฌ ์ฝ 80%)์ ๊ณต๊ธ๋ง ์์ฑ๊ณผ, ๊ตญ๋ด ๋ถํ ์นดํ๋ก๊ทธ(๊ณผ๊ฑฐ ์น์ธ๋ ๊ตญ์ฐ ๋์ฒดํ +\n",
"# ์ค๋ช
์ ๋น์ทํ์ง๋ง ๊ท๊ฒฉ์ด ๋ง์ง ์๋ ์ ์ฌ ํ๋ณด)๋ฅผ ํจ๊ป ๋ง๋ ๋ค.\n",
"# ---------------------------------------------------------------------------\n",
"COUNTRIES = [\"๊ฐ์๊ตญ๊ฐA\", \"๊ฐ์๊ตญ๊ฐB\", \"๊ฐ์๊ตญ๊ฐC\", \"๊ตญ๋ด\"]\n",
"CATS = {\n",
" \"์ ์๋ชจ๋\": dict(words=[\"DC-DC\", \"์ปจ๋ฒํฐ\", \"์ ์ฐํ\", \"์ ์\", \"์ ์ถ๋ ฅ\"], syn={\"์ปจ๋ฒํฐ\": \"๋ณํ์ฅ์น\", \"์ ์ฐํ\": \"์์ด์๋ ์ด์
\", \"์ ์ถ๋ ฅ\": \"์์ ํ\"}, std=\"MIL-STD-704\", iface=\"๋จ์๋\"),\n",
" \"์ปค๋ฅํฐ\": dict(words=[\"์ํ\", \"์ปค๋ฅํฐ\", \"๋ฐฉ์ํ\", \"๋คํ\", \"๋์ฌ์ฒด๊ฒฐ\"], syn={\"์ปค๋ฅํฐ\": \"์ ์์\", \"๋ฐฉ์ํ\": \"๋ฐํํ\", \"๋์ฌ์ฒด๊ฒฐ\": \"์คํฌ๋ฃจ๊ฒฐํฉ\"}, std=\"MIL-DTL-38999\", iface=\"์ํํ\"),\n",
" \"์ ์๋ฐธ๋ธ\": dict(words=[\"์ ์\", \"์๋ ๋
ธ์ด๋\", \"๋ฐธ๋ธ\", \"๋ฐฉํฅ์ ์ด\", \"4ํฌํธ\"], syn={\"๋ฐธ๋ธ\": \"๊ฐํ์ฅ์น\", \"๋ฐฉํฅ์ ์ด\": \"์ ๋ก์ ํ\", \"์๋ ๋
ธ์ด๋\": \"์ ์์\"}, std=\"SAE-AS5440\", iface=\"ํ๋์ง\"),\n",
" \"๋ฒ ์ด๋ง\": dict(words=[\"๋ณผ\", \"๋ฒ ์ด๋ง\", \"๊ณ ์\", \"๋ฐ๋ดํ\", \"ํ์ ์ถ\"], syn={\"๋ฒ ์ด๋ง\": \"์ถ๋ฐ์ด\", \"๋ฐ๋ดํ\": \"์ค๋ํ\", \"๊ณ ์\": \"๊ณ ํ์ \"}, std=\"ISO-492\", iface=\"์ถ๊ฒฐํฉ\"),\n",
" \"๊ดํ๋ ์ฆ\": dict(words=[\"์ ์ธ์ \", \"๋ ์ฆ\", \"๊ฒ๋ฅด๋ง๋\", \"๋ฐ์ฌ๋ฐฉ์ง\", \"์ง๊ด\"], syn={\"๋ ์ฆ\": \"๊ดํ์ฐฝ\", \"์ง๊ด\": \"์ด์ ํ์ฑ\", \"๋ฐ์ฌ๋ฐฉ์ง\": \"AR์ฝํ
\"}, std=\"MIL-PRF-13830\", iface=\"๋ง์ดํธ\"),\n",
" \"์์ด๋ก์ผ์\": dict(words=[\"๊ด์ฌ์ \", \"์์ด๋ก\", \"๊ฐ์๋\", \"์ผ์\", \"๊ด์ฑ\"], syn={\"์ผ์\": \"๊ฐ์ง๊ธฐ\", \"๊ฐ์๋\": \"ํ์ ์จ\", \"๊ด์ฑ\": \"INS์ฉ\"}, std=\"IEEE-952\", iface=\"์ง๋ ฌํต์ \"),\n",
" \"RF์ฆํญ๊ธฐ\": dict(words=[\"RF\", \"์ ๋ ฅ์ฆํญ๊ธฐ\", \"๊ด๋์ญ\", \"๋ชจ๋\", \"์ก์ \"], syn={\"์ ๋ ฅ์ฆํญ๊ธฐ\": \"ํ์์ฐํ\", \"์ก์ \": \"์ถ๋ ฅ๋จ\", \"๊ด๋์ญ\": \"์์ด๋๋ฐด๋\"}, std=\"MIL-STD-461\", iface=\"SMA\"),\n",
" \"์ ์ด๋ณด๋\": dict(words=[\"FPGA\", \"์ ์ด๋ณด๋\", \"์๋ฒ ๋๋\", \"์ฐ์ฐ\", \"์
์ถ๋ ฅ\"], syn={\"์ ์ด๋ณด๋\": \"์ปจํธ๋กค์นด๋\", \"์ฐ์ฐ\": \"์ ํธ์ฒ๋ฆฌ\", \"์๋ฒ ๋๋\": \"๋ด์ฅํ\"}, std=\"VITA-46\", iface=\"๋ฐฑํ๋ ์ธ\"),\n",
"}\n",
"V_OPTS, W_OPTS = [12, 24, 28], [25, 50, 100]\n",
"VENDORS = [\"๊ฐ์์ ๋ฐ(์ฃผ)\", \"๊ฐ์์ ์(์ฃผ)\", \"๊ฐ์์ ๊ณต์(์ฃผ)\", \"๊ฐ์๊ดํ(์ฃผ)\", \"๊ฐ์ํ
ํฌ(์ฃผ)\", \"๊ฐ์์์คํ
(์ฃผ)\"]\n",
"\n",
"\n",
"def describe(cat, words, s):\n",
" return f\"{cat} {' '.join(words)} {s['v']}V {s['w']}W {s['tmin']}~{s['tmax']}C {s['std']}\"\n",
"\n",
"\n",
"def _sig(x):\n",
" return 1 / (1 + np.exp(-x))\n",
"\n",
"\n",
"def make_parts(n=100, seed=7):\n",
" \"\"\"๋ถํ ๋ชฉ๋ก(๊ณต๊ธ๋ง ์์ฑ ํฌํจ)๊ณผ ๊ตญ๋ด ์นดํ๋ก๊ทธ๋ฅผ ๋ง๋ ๋ค. ๋ฐํ: (parts DataFrame, catalog DataFrame)\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" cats, rows, cat_rows = list(CATS), [], []\n",
" for i in range(n):\n",
" cat = cats[i % len(cats)]; c = CATS[cat]\n",
" spec = {\"v\": int(rng.choice(V_OPTS)), \"w\": int(rng.choice(W_OPTS)), \"tmin\": int(rng.choice([-55, -40])),\n",
" \"tmax\": int(rng.choice([85, 125])), \"size_mm\": float(round(rng.uniform(20, 120), 1)), \"std\": c[\"std\"]}\n",
" words = [str(x) for x in rng.permutation(c[\"words\"])[:4]]\n",
" country = str(rng.choice(COUNTRIES, p=[0.35, 0.3, 0.15, 0.2]))\n",
" overseas = country != \"๊ตญ๋ด\"\n",
" n_sup = int(rng.choice([1, 2, 3], p=[0.45, 0.35, 0.2])) if overseas else int(rng.choice([1, 2, 3]))\n",
" lead = float(round(rng.gamma(3, 45 if overseas else 25) + 20))\n",
" eol = str(rng.choice([\"์ ์\", \"๋จ์ข
์๊ณ \", \"๋จ์ข
\"], p=[0.7, 0.2, 0.1]))\n",
" crit = str(rng.choice([\"A\", \"B\", \"C\"], p=[0.3, 0.4, 0.3]))\n",
" z = (-2.6 + 1.1 * (n_sup == 1) + 0.9 * overseas + 0.006 * (lead - 100) + 1.2 * (eol == \"๋จ์ข
์๊ณ \")\n",
" + 2.0 * (eol == \"๋จ์ข
\") + rng.normal(0, 0.6))\n",
" p_late = float(np.clip(_sig(z) * 0.8 + 0.05, 0, 0.9))\n",
" late = int(rng.binomial(12, p_late))\n",
" demand = int(rng.integers(20, 400))\n",
" rows.append({\"part_no\": f\"P-{i + 1:04d}\", \"category\": cat, \"desc\": describe(cat, words, spec), **spec,\n",
" \"iface\": c[\"iface\"], \"country\": country, \"overseas\": overseas, \"n_suppliers\": n_sup,\n",
" \"lead_time_days\": lead, \"orders_12m\": 12, \"late_orders\": late,\n",
" \"avg_delay_days\": float(round(late and rng.gamma(2, 12 + 20 * p_late), 1)),\n",
" \"eol_status\": eol, \"ltb_months\": int(rng.integers(1, 25)) if eol == \"๋จ์ข
์๊ณ \" else None,\n",
" \"price_change_pct\": float(round(rng.normal(4 + 12 * p_late, 8), 1)), \"criticality\": crit,\n",
" \"annual_demand\": demand, \"stock_qty\": int(rng.integers(0, demand)),\n",
" \"defect_rate\": float(round(abs(rng.normal(0.8 + 2 * p_late, 0.8)), 2)),\n",
" \"export_license\": bool(overseas and rng.random() < 0.35),\n",
" \"disrupted_next12m\": bool(rng.random() < _sig(z + 0.4))})\n",
" if not overseas:\n",
" continue\n",
" # ๊ณผ๊ฑฐ ์น์ธ๋ ๊ตญ์ฐ ๋์ฒดํ(์ ๋ต): ๋์์ด๋ก ์์ , ๊ท๊ฒฉ ํธํ(์ ๋ ฅ ์ฌ์ ํ์ฉ, ์ผ๋ถ๋ ํฌ๊ธฐ ์ฐจ์ด๊ฐ ์ปค ์ฅ์ฐฉ ๊ฒํ ํ์)\n",
" eq_words = [c[\"syn\"].get(w, w) for w in words]\n",
" hard = rng.random() < 0.15 # ์์ ์ด ๋ง์ด ๋ค๋ฅธ ์ฌ๋ก\n",
" if hard:\n",
" eq_words = [c[\"syn\"].get(w, w) for w in c[\"words\"] if w not in words][:1] + eq_words[1:3]\n",
" eq = {**spec, \"w\": spec[\"w\"] if rng.random() < 0.7 else W_OPTS[min(W_OPTS.index(spec[\"w\"]) + 1, 2)],\n",
" \"size_mm\": float(round(spec[\"size_mm\"] * (rng.uniform(1.11, 1.15) if rng.random() < 0.12 else rng.uniform(0.95, 1.05)), 1))}\n",
" cat_rows.append({\"cand_no\": f\"D-{len(cat_rows) + 1:04d}\", \"category\": cat, \"desc\": describe(cat, eq_words, eq), **eq,\n",
" \"iface\": c[\"iface\"], \"vendor\": str(rng.choice(VENDORS)), \"approved_for\": f\"P-{i + 1:04d}\", \"kind\": \"equivalent\"})\n",
" # ์ ์ฌ ์ค๋ต ํ๋ณด: ์ค๋ช
์ ๊ฑฐ์ ๊ฐ์ง๋ง ์ ์์ด ๋ค๋ฅด๊ฑฐ๋(5V/48V) ์์ฉ ์จ๋ ๋ฑ๊ธ(0~70C)\n",
" for kind in (\"volt_mismatch\", \"temp_mismatch\"):\n",
" d = dict(spec)\n",
" if kind == \"volt_mismatch\":\n",
" d[\"v\"] = int(rng.choice([5, 48])) # ์ฒด๊ณ์์ ์ฐ์ง ์๋ ์ ์\n",
" else:\n",
" d[\"tmin\"], d[\"tmax\"] = 0, 70\n",
" d[\"size_mm\"] = float(round(spec[\"size_mm\"] * rng.uniform(0.95, 1.05), 1))\n",
" cat_rows.append({\"cand_no\": f\"D-{len(cat_rows) + 1:04d}\", \"category\": cat, \"desc\": describe(cat, words, d), **d,\n",
" \"iface\": c[\"iface\"], \"vendor\": str(rng.choice(VENDORS)), \"approved_for\": None, \"kind\": kind})\n",
" parts = pd.DataFrame(rows)\n",
" catalog = pd.DataFrame(cat_rows).sample(frac=1, random_state=seed).reset_index(drop=True)\n",
" return parts, catalog\n",
"# ---------------------------------------------------------------------------\n",
"\n",
"# 1) ์คํค๋ง ์ ์: ๋ถํ ์๋ณ, ๊ณต๊ธ๋ง(ํด์ธ ์์กด๋ยท๊ณต๊ธ์ ์ยท๋ฆฌ๋ํ์ยท์ง์ฐ ์ด๋ ฅ), ๋จ์ข
(DMSMS), ์กฐ๋ฌ ์์ฑ\n",
"SCHEMA = {\n",
" \"part_no\": {\"type\": str, \"pattern\": r\"^P-\\d{4}$\", \"desc\": \"๋ถํ ๋ฒํธ\"},\n",
" \"category\": {\"type\": str, \"enum\": list(CATS), \"desc\": \"๋ถํ ๋ฒ์ฃผ\"},\n",
" \"country\": {\"type\": str, \"enum\": COUNTRIES, \"desc\": \"์ฃผ ๊ณต๊ธ ๊ตญ๊ฐ(๊ฐ์)\"},\n",
" \"n_suppliers\": {\"type\": int, \"min\": 1, \"max\": 10, \"desc\": \"๊ณต๊ธ์ ์\"},\n",
" \"lead_time_days\": {\"type\": (int, float), \"min\": 1, \"max\": 1000, \"desc\": \"์กฐ๋ฌ ๋ฆฌ๋ํ์(์ผ)\"},\n",
" \"orders_12m\": {\"type\": int, \"min\": 0, \"max\": 100, \"desc\": \"์ต๊ทผ 12๊ฐ์ ๋ฐ์ฃผ ๊ฑด์\"},\n",
" \"late_orders\": {\"type\": int, \"min\": 0, \"max\": 100, \"desc\": \"๋ฉ๊ธฐ ์ง์ฐ ๊ฑด์\"},\n",
" \"avg_delay_days\": {\"type\": (int, float), \"min\": 0, \"max\": 365, \"desc\": \"ํ๊ท ์ง์ฐ์ผ\"},\n",
" \"eol_status\": {\"type\": str, \"enum\": [\"์ ์\", \"๋จ์ข
์๊ณ \", \"๋จ์ข
\"], \"desc\": \"๋จ์ข
์ํ\"},\n",
" \"ltb_months\": {\"type\": (int, float), \"min\": 0, \"max\": 60, \"optional\": True, \"desc\": \"์ต์ข
๊ตฌ๋งค(LTB)๊น์ง ๋จ์ ๊ฐ์\"},\n",
" \"price_change_pct\": {\"type\": (int, float), \"min\": -100, \"max\": 500, \"desc\": \"์ ๋
๋๋น ๋จ๊ฐ ๋ณ๋(%)\"},\n",
" \"criticality\": {\"type\": str, \"enum\": [\"A\", \"B\", \"C\"], \"desc\": \"์ฒด๊ณ ์ค์๋\"},\n",
" \"annual_demand\": {\"type\": int, \"min\": 0, \"max\": 100000, \"desc\": \"์ฐ๊ฐ ์์๋\"},\n",
" \"stock_qty\": {\"type\": int, \"min\": 0, \"max\": 100000, \"desc\": \"ํ ์ฌ๊ณ \"},\n",
" \"defect_rate\": {\"type\": (int, float), \"min\": 0, \"max\": 100, \"desc\": \"์
๊ณ ๋ถ๋๋ฅ (%)\"},\n",
" \"export_license\": {\"type\": bool, \"desc\": \"์์ถ ํ๊ฐ ํ์ ์ฌ๋ถ\"},\n",
"}\n",
"\n",
"\n",
"def check(name, v, r):\n",
" if v is None or (isinstance(v, float) and np.isnan(v)):\n",
" return [] if r.get(\"optional\") else [f\"{name}: ํ์ ๊ฐ ๋๋ฝ\"]\n",
" if not isinstance(v, r[\"type\"]) or (isinstance(v, bool) and r[\"type\"] is not bool):\n",
" return [f\"{name}: ์๋ฃํ ์ค๋ฅ({type(v).__name__})\"]\n",
" errs = []\n",
" if \"enum\" in r and v not in r[\"enum\"]:\n",
" errs.append(f\"{name}: ํ์ฉ๋์ง ์์ ๊ฐ '{v}'\")\n",
" if \"min\" in r and v < r[\"min\"]:\n",
" errs.append(f\"{name}: ์ต์๊ฐ {r['min']} ๋ฏธ๋ง\")\n",
" if \"max\" in r and v > r[\"max\"]:\n",
" errs.append(f\"{name}: ์ต๋๊ฐ {r['max']} ์ด๊ณผ\")\n",
" if \"pattern\" in r and not re.match(r[\"pattern\"], v):\n",
" errs.append(f\"{name}: ํ์ ๋ถ์ผ์น '{v}'\")\n",
" return errs\n",
"\n",
"\n",
"def validate(rec):\n",
" errs = [f\"{k}: ํ๋ ๋๋ฝ\" for k in SCHEMA if k not in rec]\n",
" for k, r in SCHEMA.items():\n",
" if k in rec:\n",
" errs += check(k, rec[k], r)\n",
" if not errs and rec[\"late_orders\"] > rec[\"orders_12m\"]: # ํ๋ ๊ฐ ๋
ผ๋ฆฌ ๊ท์น\n",
" errs.append(\"late_orders: ๋ฐ์ฃผ ๊ฑด์๋ณด๋ค ์ง์ฐ ๊ฑด์๊ฐ ๋ง์\")\n",
" if not errs and rec[\"eol_status\"] == \"๋จ์ข
์๊ณ \" and rec[\"ltb_months\"] is None:\n",
" errs.append(\"ltb_months: ๋จ์ข
์๊ณ ๋ถํ์ LTB ๊ฐ์ ํ์\")\n",
" return errs\n",
"\n",
"\n",
"def to_record(row):\n",
" rec = {k: (row[k].item() if hasattr(row[k], \"item\") else row[k]) for k in SCHEMA}\n",
" rec[\"ltb_months\"] = None if pd.isna(rec[\"ltb_months\"]) else int(rec[\"ltb_months\"])\n",
" return rec\n",
"\n",
"\n",
"parts, _ = make_parts(100, seed=7)\n",
"records = [to_record(r) for _, r in parts.iterrows()]\n",
"ok = sum(not validate(r) for r in records)\n",
"print(f\"[์คํค๋ง ๊ฒ์ฆ] ๋ถํ {len(records)}๊ฑด ์ค ํต๊ณผ {ok}๊ฑด, ์ค๋ฅ {len(records) - ok}๊ฑด / ํ๋ {len(SCHEMA)}๊ฐ\")\n",
"print(f\"ํด์ธ ์์กด {sum(r['country'] != '๊ตญ๋ด' for r in records)}๊ฑด, ๋จ์ผ ๊ณต๊ธ์ {sum(r['n_suppliers'] == 1 for r in records)}๊ฑด, \"\n",
" f\"๋จ์ข
์๊ณ ยท๋จ์ข
{sum(r['eol_status'] != '์ ์' for r in records)}๊ฑด, ํ๊ท ๋ฆฌ๋ํ์ {np.mean([r['lead_time_days'] for r in records]):.0f}์ผ\")\n",
"\n",
"# 2) ์ค๋ฅ ์ฃผ์
์ํ: ํ์ยทํ์ฉ๊ฐยท๋ฒ์ยท์๋ฃํยท๋
ผ๋ฆฌ ๊ท์น ์๋ฐ 6์ข
\n",
"bad = json.loads(json.dumps(records[2], ensure_ascii=False))\n",
"bad.update({\"part_no\": \"P12\", \"country\": \"๋ฏธ์\", \"lead_time_days\": -5, \"n_suppliers\": \"2\"})\n",
"bad2 = dict(records[5], late_orders=15, orders_12m=12)\n",
"bad3 = dict(records[0], eol_status=\"๋จ์ข
์๊ณ \", ltb_months=None)\n",
"print(\"\\n[์ค๋ฅ ์ฃผ์
๊ฒฐ๊ณผ]\")\n",
"for e in validate(bad) + validate(bad2) + validate(bad3):\n",
" print(\" -\", e)\n",
"\n",
"# 3) AS-IS ์
๋ฌด ๋จ๊ณ ๋ถํด: [๊ต์ก์ฉ๊ฐ์์๋ฃ] ๊ตญ์ฐํ ํ๋ณด ์ ์ 30๊ฑด์ ๋จ๊ณ๋ณ ์์์ผ(์์
+ ๋๊ธฐ)\n",
"rng = np.random.default_rng(3)\n",
"STEPS = [(\"์ฒด๊ณ ๋ถํ๋ชฉ๋ก(BOM) ์์ง\", 2, 1), (\"๊ณต๊ธ์
์ฒด ์๋ฃ ์์ฒญยทํ์ \", 3, 9), (\"๋จ์ข
(DMSMS) ๊ณต๊ณ ์์งยทํ์ธ\", 2, 6),\n",
" (\"์กฐ๋ฌ ์ด๋ ฅยท๋ฉ๊ธฐ ์ง์ฐ ์์์
์ง๊ณ\", 5, 2), (\"ํด์ธ ์์กด๋ ๋ถ๋ฅ\", 2, 1), (\"์ํ ๋ถํ ์ ์ ํ์\", 1, 7),\n",
" (\"๊ตญ๋ด ์
์ฒด ํ์(์ ํยท์๋ฃ)\", 6, 8), (\"๊ท๊ฒฉ ๋น๊ตํ ์์ฑ\", 5, 2), (\"๊ธฐ์ ๊ฒํ ์๋ขฐ\", 1, 10),\n",
" (\"๊ตญ์ฐํ ํ๋ณด ๋ณด๊ณ ์ ์์ฑ\", 3, 1), (\"๊ฒฐ์ฌ(์น์ธ)\", 1, 5), (\"๊ณ์ฝ ์๋ขฐ ์ค๋น\", 2, 2)]\n",
"TOBE = {1: (\"ํ์ค ์์ ์๋ ์์ง\", 0.3), 2: (\"๊ณต๊ณ ์๋ ํ์ฑ(๋ฏธ์
2)\", 0.2), 3: (\"๋ฃฐ ์์ง ์๋ ์ง๊ณ(๋ฏธ์
3)\", 0.1),\n",
" 4: (\"์๋ ๋ถ๋ฅ(๋ฏธ์
3)\", 0.1), 6: (\"๋์ฒดํ ์๋ ์ถ์ฒ(๋ฏธ์
4)\", 0.3), 7: (\"๊ท๊ฒฉ ์๋ ๋์กฐ(๋ฏธ์
4)\", 0.2),\n",
" 9: (\"๋ณด๊ณ ์ ์ด์ ์๋ ์์ฑ(๋ฏธ์
10)\", 0.4)}\n",
"rows = []\n",
"for i, (name, work, wait) in enumerate(STEPS):\n",
" w = rng.gamma(4, work / 4, 30); q = rng.gamma(2, wait / 2, 30)\n",
" f = TOBE.get(i, (\"์ ์ง(์ฌ๋ ํ๋จ)\", 1.0))\n",
" rows.append({\"๋จ๊ณ\": f\"{i + 1:02d}. {name}\", \"์์
(์ผ)\": w.mean(), \"๋๊ธฐ(์ผ)\": q.mean(), \"AS-IS(์ผ)\": w.mean() + q.mean(),\n",
" \"TO-BE ๋ฐฉ์\": f[0], \"TO-BE(์ผ)\": w.mean() * f[1] + q.mean() * (f[1] if f[1] < 1 else 1.0)})\n",
"proc = pd.DataFrame(rows)\n",
"share = proc[\"AS-IS(์ผ)\"] / proc[\"AS-IS(์ผ)\"].sum()\n",
"proc[\"๋ณ๋ชฉ\"] = np.where((share >= share.sort_values().iloc[-3]) | (proc[\"๋๊ธฐ(์ผ)\"] / proc[\"AS-IS(์ผ)\"] > 0.75), \"๋ณ๋ชฉ\", \"\")\n",
"print(\"\\n[AS-IS / TO-BE ์
๋ฌด ๋จ๊ณ ๋น๊ต]\")\n",
"print(proc.round(1).to_string(index=False))\n",
"n_bn = int((proc[\"๋ณ๋ชฉ\"] == \"๋ณ๋ชฉ\").sum())\n",
"asis, tobe = proc[\"AS-IS(์ผ)\"].sum(), proc[\"TO-BE(์ผ)\"].sum()\n",
"print(f\"\\n์
๋ฌด ๋จ๊ณ {len(proc)}๊ฐ(๋ชฉํ 10๊ฐ ์ด์: {'๋ฌ์ฑ' if len(proc) >= 10 else '๋ฏธ๋ฌ'}), ๋ณ๋ชฉ {n_bn}๊ฐ์(๋ชฉํ 3๊ฐ์ ์ด์: {'๋ฌ์ฑ' if n_bn >= 3 else '๋ฏธ๋ฌ'})\")\n",
"print(f\"ํ๋ณด ์ ์ 1๊ฑด ์์: AS-IS {asis:.1f}์ผ -> TO-BE {tobe:.1f}์ผ ({1 - tobe / asis:.0%} ๋จ์ถ ์์). ๊ฒฐ์ฌยท๊ธฐ์ ๊ฒํ ยทํ์๋ ์ฌ๋์ด ์ํํ๋ค.\")\n",
"\n",
"# 4) ์
์ถ๋ ฅ ๋ฐ์ดํฐ ๋ช
์ธ ์์ฝ\n",
"print(\"\\n[์
์ถ๋ ฅ ๋ช
์ธ] ์
๋ ฅ: ๋ถํ ๊ณต๊ธ๋ง ๋ ์ฝ๋(์ 16๊ฐ ํ๋) / ์ถ๋ ฅ: ์ํ ๋ฑ๊ธ, ๊ตญ์ฐํ ํ๋ณด Top-3, ์ถ์ฒ ๊ทผ๊ฑฐ, ๊ฒฐ์ฌ ์ด๋ ฅ\")\n",
"\n",
"plt.figure(figsize=(9, 5))\n",
"y = np.arange(len(proc))\n",
"plt.barh(y - 0.2, proc[\"AS-IS(์ผ)\"], 0.4, label=\"AS-IS\", color=\"tab:gray\")\n",
"plt.barh(y + 0.2, proc[\"TO-BE(์ผ)\"], 0.4, label=\"TO-BE\", color=\"tab:blue\")\n",
"plt.yticks(y, [f\"step {i + 1}{' *' if b else ''}\" for i, b in enumerate(proc[\"๋ณ๋ชฉ\"])])\n",
"plt.gca().invert_yaxis(); plt.xlabel(\"days per case\"); plt.legend()\n",
"plt.title(\"Localization candidate process: AS-IS vs TO-BE (* = bottleneck, synthetic)\")\n",
"plt.tight_layout(); plt.show()\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"๐ก [์ฐธ๊ณ ] ๋ฏธ์
01 ์์ ์คํ ๊ฒฐ๊ณผ (Output)
\n",
"\n",
"```text\n",
"[์คํค๋ง ๊ฒ์ฆ] ๋ถํ 100๊ฑด ์ค ํต๊ณผ 100๊ฑด, ์ค๋ฅ 0๊ฑด / ํ๋ 16๊ฐ\n",
"ํด์ธ ์์กด 85๊ฑด, ๋จ์ผ ๊ณต๊ธ์ 42๊ฑด, ๋จ์ข
์๊ณ ยท๋จ์ข
37๊ฑด, ํ๊ท ๋ฆฌ๋ํ์ 145์ผ\n",
"\n",
"[์ค๋ฅ ์ฃผ์
๊ฒฐ๊ณผ]\n",
" - part_no: ํ์ ๋ถ์ผ์น 'P12'\n",
" - country: ํ์ฉ๋์ง ์์ ๊ฐ '๋ฏธ์'\n",
" - n_suppliers: ์๋ฃํ ์ค๋ฅ(str)\n",
" - lead_time_days: ์ต์๊ฐ 1 ๋ฏธ๋ง\n",
" - late_orders: ๋ฐ์ฃผ ๊ฑด์๋ณด๋ค ์ง์ฐ ๊ฑด์๊ฐ ๋ง์\n",
" - ltb_months: ๋จ์ข
์๊ณ ๋ถํ์ LTB ๊ฐ์ ํ์\n",
"\n",
"[AS-IS / TO-BE ์
๋ฌด ๋จ๊ณ ๋น๊ต]\n",
" ๋จ๊ณ ์์
(์ผ) ๋๊ธฐ(์ผ) AS-IS(์ผ) TO-BE ๋ฐฉ์ TO-BE(์ผ) ๋ณ๋ชฉ\n",
" 01. ์ฒด๊ณ ๋ถํ๋ชฉ๋ก(BOM) ์์ง 2.0 1.0 3.0 ์ ์ง(์ฌ๋ ํ๋จ) 3.0 \n",
" 02. ๊ณต๊ธ์
์ฒด ์๋ฃ ์์ฒญยทํ์ 3.9 8.5 12.4 ํ์ค ์์ ์๋ ์์ง 3.7 ๋ณ๋ชฉ\n",
"03. ๋จ์ข
(DMSMS) ๊ณต๊ณ ์์งยทํ์ธ 2.1 6.6 8.7 ๊ณต๊ณ ์๋ ํ์ฑ(๋ฏธ์
2) 1.7 ๋ณ๋ชฉ\n",
"04. ์กฐ๋ฌ ์ด๋ ฅยท๋ฉ๊ธฐ ์ง์ฐ ์์์
์ง๊ณ 5.9 2.0 7.9 ๋ฃฐ ์์ง ์๋ ์ง๊ณ(๋ฏธ์
3) 0.8 \n",
" 05. ํด์ธ ์์กด๋ ๋ถ๋ฅ 2.4 1.1 3.5 ์๋ ๋ถ๋ฅ(๋ฏธ์
3) 0.4 \n",
" 06. ์ํ ๋ถํ ์ ์ ํ์ 1.0 7.2 8.3 ์ ์ง(์ฌ๋ ํ๋จ) 8.3 ๋ณ๋ชฉ\n",
" 07. ๊ตญ๋ด ์
์ฒด ํ์(์ ํยท์๋ฃ) 5.3 7.5 12.8 ๋์ฒดํ ์๋ ์ถ์ฒ(๋ฏธ์
4) 3.8 ๋ณ๋ชฉ\n",
" 08. ๊ท๊ฒฉ ๋น๊ตํ ์์ฑ 4.7 2.0 6.7 ๊ท๊ฒฉ ์๋ ๋์กฐ(๋ฏธ์
4) 1.3 \n",
" 09. ๊ธฐ์ ๊ฒํ ์๋ขฐ 1.1 9.6 10.7 ์ ์ง(์ฌ๋ ํ๋จ) 10.7 ๋ณ๋ชฉ\n",
" 10. ๊ตญ์ฐํ ํ๋ณด ๋ณด๊ณ ์ ์์ฑ 3.1 1.2 4.3 ๋ณด๊ณ ์ ์ด์ ์๋ ์์ฑ(๋ฏธ์
10) 1.7 \n",
" 11. ๊ฒฐ์ฌ(์น์ธ) 0.9 5.7 6.6 ์ ์ง(์ฌ๋ ํ๋จ) 6.6 ๋ณ๋ชฉ\n",
" 12. ๊ณ์ฝ ์๋ขฐ ์ค๋น 2.1 2.1 4.1 ์ ์ง(์ฌ๋ ํ๋จ) 4.1 \n",
"\n",
"์
๋ฌด ๋จ๊ณ 12๊ฐ(๋ชฉํ 10๊ฐ ์ด์: ๋ฌ์ฑ), ๋ณ๋ชฉ 6๊ฐ์(๋ชฉํ 3๊ฐ์ ์ด์: ๋ฌ์ฑ)\n",
"ํ๋ณด ์ ์ 1๊ฑด ์์: AS-IS 89.1์ผ -> TO-BE 46.3์ผ (48% ๋จ์ถ ์์). ๊ฒฐ์ฌยท๊ธฐ์ ๊ฒํ ยทํ์๋ ์ฌ๋์ด ์ํํ๋ค.\n",
"\n",
"[์
์ถ๋ ฅ ๋ช
์ธ] ์
๋ ฅ: ๋ถํ ๊ณต๊ธ๋ง ๋ ์ฝ๋(์ 16๊ฐ ํ๋) / ์ถ๋ ฅ: ์ํ ๋ฑ๊ธ, ๊ตญ์ฐํ ํ๋ณด Top-3, ์ถ์ฒ ๊ทผ๊ฑฐ, ๊ฒฐ์ฌ ์ด๋ ฅ\n",
"```\n",
" \n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## ๐ฏ ๋ฏธ์
02. ๊ตญ๋ด์ธ ๊ณต๊ธ์
์ฒด ๋ฐ์ดํฐ ์ ์ , ๋ถํ ๋จ์ข
(DMSMS) ๊ณต๊ณ ํ์ฑ ์ ์ฒ๋ฆฌ ๋ชจ๋ ๊ตฌํ\n",
"**์์์๊ฐ**: ์ฝ 2.0์๊ฐ\n",
"\n",
"### ๐ ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ์ํ ์ง์นจ\n",
"#### ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ๋น์์ฑ\n",
"\n",
"๊ณต๊ธ์
์ฒด ๋ฐ์ดํฐ ์ ์ ๋ฐ ๋ถํ ๋จ์ข
๊ณต๊ณ ํ์ฑ ์ ์ฒ๋ฆฌ ๋ชจ๋์ ๊ตฌํํ๋ค. ์ค์ ์ ์ ํ์ฅ์์๋ ๋จ์ ํ์ง์ ๊ทธ์น์ง ์๊ณ ์ ์ํ ์ํ ํ๋จ๊ณผ ์ ๋ขฐ๋ ๋์ ์์ฌ๊ฒฐ์ ์ด ํ์์ ์ด๋ฏ๋ก, **๊ตญ๋ด์ธ ๊ณต๊ธ์
์ฒด ์๋ฃ์ ํ๊ธฐ ์ฐจ์ด๋ฅผ ์ ์ ํ๊ณ ์์์ด ๋ค๋ฅธ ๋ถํ ๋จ์ข
(DMSMS) ๊ณต๊ณ ๋ฌธ์์ ํ์ํ ํญ๋ชฉ์ ์ถ์ถํ๋ ์ ์ฒ๋ฆฌ ๋ชจ๋์ ๊ตฌํํ๋ค.** ๊ณผ์
์ ์์ํ์ฌ ์คํ์ ๋ฐฉ์งํ๊ณ ์์ ๋์๋ ฅ์ ๊ทน๋ํํด์ผ ํฉ๋๋ค.\n",
"\n",
"์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ณธ ๋ฏธ์
์์๋ **CSV, ์์
๋ด๋ณด๋ด๊ธฐ ํํ(ํญ ๊ตฌ๋ถ), JSON ์ธ ํ์์ ๊ณต๊ธ์
์ฒด ์๋ฃ๋ฅผ ์ด ์ด๋ฆ ๋ณ์นญ ์ฌ์ ์ผ๋ก ํ๋์ ํ์ค ์คํค๋ง๋ก ๋ฐ๊พธ๊ณ , ์
์ฒด๋ช
ํ๊ธฐ์ ๊ตญ๊ฐ ์ฝ๋, ๋ฆฌ๋ํ์ ๋จ์(์ผยท์ฃผยท๊ฐ์ยท๋
), ๋ ์ง ํ์์ ํต์ผ ์ํํ๊ณ ๊ฒ์ฆํ๋** ์ค์ต์ ์ฒด๊ณ์ ์ผ๋ก ์ํํฉ๋๋ค.\n",
"\n",
"ํ์ต์๋ ์ค์ต๊ฐ์ด๋์ ์ ์๋ ์ด 4๊ฐ ์ธ๋ถ ํ๋(๊ธฐ๋ณธ ๊ธฐ๋ฅ ๊ตฌํ โ ๊ฒฐ๊ณผ ์งํ ๊ฒ์ฆ โ ํ๋ผ๋ฏธํฐ ํ๋ ๋ฐ ๋์ ๊ณผ์ )์ ์์ฐจ์ ์ผ๋ก ์งํํ๋ฉฐ ํต์ฌ ํ์ ๊ธฐ์ค์ ๋ฌ์ฑํฉ๋๋ค.\n",
"\n",
"**์ด๋ฒ ๊ฒฐ๊ณผ๋ฌผ:** `src/connectors/prc07_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ยท์์
๋ด๋ณด๋ด๊ธฐ TSVยทJSON)๋ฅผ ํ๋์ ํ์ค ์คํค๋ง๋ก ์ ์ ํ๊ณ , ๋ฌธ์๋ง๋ค ์์์ด ๋ค๋ฅธ ๋ถํ ๋จ์ข
(DMSMS) ๊ณต๊ณ ๋ฌธ ํ
์คํธ์์ ๋ถํ๋ฒํธยท๋จ์ข
์ผยท์ต์ข
๊ตฌ๋งค(LTB) ๊ธฐํยท๋์ฒดํ ์ ๋ณด๋ฅผ ํ์ฑํ๋ ์ ์ฒ๋ฆฌ ๋ชจ๋์ ๊ตฌํํ๋ค.\"\"\"\n",
"import csv\n",
"import io\n",
"import json\n",
"import re\n",
"from datetime import date\n",
"import numpy as np\n",
"import pandas as pd\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# [๊ต์ก์ฉ๊ฐ์์๋ฃ] ์๋ก ๋ค๋ฅธ ํ์์ ์์ ์๋ฃ (์ค์ ์
์ฒดยท๊ณต๊ณ ์๋). ํ๊ธฐ๊ฐ ์ผ๋ถ๋ฌ ์ ๊ฐ๊ฐ์ด๋ค.\n",
"# ---------------------------------------------------------------------------\n",
"RAW_CSV = \"\"\"์
์ฒด์ฝ๋,์
์ฒด๋ช
,๊ตญ๊ฐ,๊ณต๊ธ๋ถํ,๋ฆฌ๋ํ์,์ต๊ทผ๊ฑฐ๋์ผ\n",
"V001,(์ฃผ)๊ฐ์์ ๋ฐ,๊ตญ๋ด,P-0001,45์ผ,2026.03.02\n",
"V002, ๊ฐ์์ ์ ์ฃผ์ํ์ฌ ,KR,P-0004,2๊ฐ์,2026-3-15\n",
"V003,Virtual Parts Inc.,A๊ตญ,P-0003,180,20260110\n",
"V004,๊ฐ์์ ๊ณต์ใ,๊ตญ๋ด,P-0003,60์ผ,26/02/27\n",
"V005,VIRTUAL PARTS INC,๊ฐ์๊ตญ๊ฐA,P-0011,6๊ฐ์,2025.12.30\n",
"V006,Sample Optics GmbH,B,P-0005,210์ผ,2026-01-05\n",
"\"\"\"\n",
"RAW_TSV = \"๊ณต๊ธ์
์ฒด\\t๊ตญ์ \\tํ๋ชฉ๋ฒํธ\\t๋ฉ๊ธฐ(์ผ)\\t์ต์ข
๊ฑฐ๋\\n\" \\\n",
" \"๊ฐ์๊ดํ(์ฃผ)\\t๋ํ๋ฏผ๊ตญ\\tP-0005\\t75\\t2026.02.11\\n\" \\\n",
" \"Sample Optics GmbH\\t๊ฐ์๊ตญ๊ฐB\\tP-0013\\t200\\t2026.01.20\\n\" \\\n",
" \"๊ฐ์ํ
ํฌ (์ฃผ)\\t๊ตญ๋ด\\tP-0008\\t90\\t2026/03/05\\n\" \\\n",
" \"Model Sensor Co.\\tC๊ตญ\\tP-0006\\t1๋
\\t2025.11.03\\n\" \\\n",
" \"Virtual Parts Inc\\tA๊ตญ\\tP-0003\\t180\\t2026.01.10\\n\"\n",
"RAW_JSON = json.dumps({\"result\": \"OK\", \"items\": [\n",
" {\"vendor\": {\"name\": \"๊ฐ์์์คํ
(์ฃผ)\", \"nation\": \"KR\"}, \"part\": \"P-0016\", \"leadTime\": {\"value\": 3, \"unit\": \"month\"}, \"lastTrade\": \"2026-02-01\"},\n",
" {\"vendor\": {\"name\": \"Model Sensor Co\", \"nation\": \"C\"}, \"part\": \"P-0014\", \"leadTime\": {\"value\": 150, \"unit\": \"day\"}, \"lastTrade\": \"2026-01-09\"},\n",
" {\"vendor\": {\"name\": \"๊ฐ์์ ๋ฐ ์ฃผ์ํ์ฌ\", \"nation\": \"KR\"}, \"part\": \"P-0002\", \"leadTime\": {\"value\": 8, \"unit\": \"week\"}, \"lastTrade\": \"2026-03-20\"}]},\n",
" ensure_ascii=False)\n",
"NOTICES = [\n",
" \"์ ๋ชฉ: ๋ถํ ๋จ์ข
์์ ํต๋ณด\\n๋ถํ๋ฒํธ: P-0003\\n๋จ์ข
์์ ์ผ: 2027๋
3์ 31์ผ\\n์ต์ข
๊ตฌ๋งค(LTB) ๋ง๊ฐ: 2026๋
12์ 31์ผ\\n๋์ฒดํ: ์์\",\n",
" \"[DMSMS NOTICE] Part No. P-0011 / End of Life 2027-06-30 / Last Time Buy 2027-01-15 / Replacement: P-0011A\",\n",
" \"์์ : ํ๋๋ด๋น ์ฐธ์กฐ\\n1. ์๋ ๋ถํ์ ์์ฐ ์ค๋จ์ ์๋ ค ๋๋ฆฝ๋๋ค.\\n ๊ฐ. ํ๋ฒ : P-0013\\n ๋. ์์ฐ์ค๋จ์ผ : 2026.09.30.\\n ๋ค. ์ต์ข
์ฃผ๋ฌธ ์ ์ : 2026.06.30.\\n2. ๋์ฒด ๊ถ๊ณ ํ : ํ์ ํ์\",\n",
" \"๋ถํ P-0006 ์(๋) 2026-12-31 ๋ถ๋ก ๋จ์ข
๋๋ฉฐ ์ต์ข
์ฃผ๋ฌธ์ 2026-10-31 ๊น์ง ๋ฐ์ต๋๋ค. ๋์ฒดํ P-0006-R ๊ฒํ ๋ฐ๋๋๋ค.\",\n",
" \"๋จ์ข
๊ณต๊ณ / ๋์:P-0020 / ๋จ์ข
์ผ:27.02.28 / LTB:26.11.30 / ๋์ฒด:์์\",\n",
"]\n",
"\n",
"# ---- ์ ๊ทํ ๊ท์น(์์์ด ๋ฐ๋๋ฉด ์ด ์ฌ์ ๋ง ๊ณ ์น๋ฉด ๋๋ค) ----\n",
"COL_ALIAS = {\"vendor\": [\"์
์ฒด๋ช
\", \"๊ณต๊ธ์
์ฒด\", \"vendor\"], \"country\": [\"๊ตญ๊ฐ\", \"๊ตญ์ \", \"nation\"], \"part_no\": [\"๊ณต๊ธ๋ถํ\", \"ํ๋ชฉ๋ฒํธ\", \"part\"],\n",
" \"lead\": [\"๋ฆฌ๋ํ์\", \"๋ฉ๊ธฐ(์ผ)\", \"leadTime\"], \"last_trade\": [\"์ต๊ทผ๊ฑฐ๋์ผ\", \"์ต์ข
๊ฑฐ๋\", \"lastTrade\"]}\n",
"COUNTRY_MAP = {\"๊ตญ๋ด\": \"๊ตญ๋ด\", \"KR\": \"๊ตญ๋ด\", \"๋ํ๋ฏผ๊ตญ\": \"๊ตญ๋ด\", \"A๊ตญ\": \"๊ฐ์๊ตญ๊ฐA\", \"A\": \"๊ฐ์๊ตญ๊ฐA\", \"๊ฐ์๊ตญ๊ฐA\": \"๊ฐ์๊ตญ๊ฐA\",\n",
" \"B๊ตญ\": \"๊ฐ์๊ตญ๊ฐB\", \"B\": \"๊ฐ์๊ตญ๊ฐB\", \"๊ฐ์๊ตญ๊ฐB\": \"๊ฐ์๊ตญ๊ฐB\", \"C๊ตญ\": \"๊ฐ์๊ตญ๊ฐC\", \"C\": \"๊ฐ์๊ตญ๊ฐC\", \"๊ฐ์๊ตญ๊ฐC\": \"๊ฐ์๊ตญ๊ฐC\"}\n",
"UNIT_DAYS = {\"์ผ\": 1, \"day\": 1, \"์ฃผ\": 7, \"week\": 7, \"๊ฐ์\": 30, \"month\": 30, \"๋
\": 365, \"year\": 365}\n",
"\n",
"\n",
"def norm_vendor(s):\n",
" s = re.sub(r\"\\(์ฃผ\\)|ใ|์ฃผ์ํ์ฌ|\\bInc\\.?|\\bCo\\.?|\\bGmbH\", \"\", str(s), flags=re.I)\n",
" return re.sub(r\"[\\s.,]+\", \" \", s).strip().upper()\n",
"\n",
"\n",
"def norm_lead(v):\n",
" if isinstance(v, dict):\n",
" return int(v[\"value\"] * UNIT_DAYS[v[\"unit\"]])\n",
" m = re.match(r\"^\\s*(\\d+)\\s*(์ผ|์ฃผ|๊ฐ์|๋
)?\\s*$\", str(v))\n",
" if not m:\n",
" raise ValueError(f\"๋ฆฌ๋ํ์ ํด์ ๋ถ๊ฐ: {v}\")\n",
" return int(m.group(1)) * UNIT_DAYS.get(m.group(2) or \"์ผ\", 1)\n",
"\n",
"\n",
"def norm_date(s):\n",
" s = str(s).strip().rstrip(\".\")\n",
" m = re.match(r\"^(\\d{2,4})[.\\-/๋
\\s]*(\\d{1,2})[.\\-/์\\s]*(\\d{1,2})์ผ?$\", s) or re.match(r\"^(\\d{4})(\\d{2})(\\d{2})$\", s)\n",
" if not m:\n",
" raise ValueError(f\"๋ ์ง ํด์ ๋ถ๊ฐ: {s}\")\n",
" y, mo, d = (int(x) for x in m.groups())\n",
" return date(y + 2000 if y < 100 else y, mo, d).isoformat()\n",
"\n",
"\n",
"def pick(row, key):\n",
" for k in COL_ALIAS[key]:\n",
" if k in row:\n",
" return row[k]\n",
" raise KeyError(f\"ํ์ ์ด ์์: {key}\")\n",
"\n",
"\n",
"def to_std(row, src):\n",
" return {\"vendor\": norm_vendor(pick(row, \"vendor\")), \"country\": COUNTRY_MAP[str(pick(row, \"country\")).strip()],\n",
" \"part_no\": str(pick(row, \"part_no\")).strip(), \"lead_days\": norm_lead(pick(row, \"lead\")),\n",
" \"last_trade\": norm_date(pick(row, \"last_trade\")), \"source\": src}\n",
"\n",
"\n",
"def load(text, fmt):\n",
" \"\"\"ํ์๋ณ ๋ก๋: csv / tsv(์์
๋ด๋ณด๋ด๊ธฐ) / json. ํ๋ง๋ค ์ฑ๊ณตยท์คํจ๋ฅผ ๋ฐ๋ก ์ผ๋ค.\"\"\"\n",
" if fmt == \"json\":\n",
" rows = [{\"vendor\": r[\"vendor\"][\"name\"], \"nation\": r[\"vendor\"][\"nation\"], \"part\": r[\"part\"], \"leadTime\": r[\"leadTime\"],\n",
" \"lastTrade\": r[\"lastTrade\"]} for r in json.loads(text)[\"items\"]]\n",
" else:\n",
" rows = list(csv.DictReader(io.StringIO(text), delimiter=\"\\t\" if fmt == \"tsv\" else \",\"))\n",
" ok, fail = [], []\n",
" for r in rows:\n",
" try:\n",
" ok.append(to_std(r, fmt))\n",
" except (KeyError, ValueError) as e:\n",
" fail.append((r, str(e)))\n",
" return ok, fail\n",
"\n",
"\n",
"def parse_notice(text):\n",
" \"\"\"์์์ด ๋ค๋ฅธ ๋จ์ข
๊ณต๊ณ ์์ ๋ถํ๋ฒํธยท๋จ์ข
์ผยทLTBยท๋์ฒดํ์ ์ถ์ถํ๋ค.\"\"\"\n",
" part = re.search(r\"P-\\d{4}\", text)\n",
" dt = r\"(\\d{2,4}[๋
.\\-/]\\s*\\d{1,2}[์.\\-/]\\s*\\d{1,2}์ผ?\\.?)\"\n",
" eol = re.search(r\"(?:๋จ์ข
(?: ์์ )?์ผ|์์ฐ์ค๋จ์ผ|End of Life)\\s*[:๏ผ]?\\s*\" + dt, text) or re.search(dt + r\"\\s*๋ถ๋ก\\s*๋จ์ข
\", text)\n",
" ltb = re.search(r\"(?:LTB\\)?\\s*๋ง๊ฐ|Last Time Buy|์ต์ข
์ฃผ๋ฌธ ์ ์|์ต์ข
์ฃผ๋ฌธ์|LTB)\\s*[:๏ผ]?\\s*\" + dt, text)\n",
" rep = re.search(r\"(?:๋์ฒดํ|๋์ฒด ๊ถ๊ณ ํ|Replacement|๋์ฒด)\\s*[:๏ผ]?\\s*(P-[\\w-]+|์์|ํ์ ํ์)\", text)\n",
" if not (part and eol and ltb):\n",
" raise ValueError(\"ํ์ ํญ๋ชฉ(๋ถํ๋ฒํธยท๋จ์ข
์ผยทLTB) ์ถ์ถ ์คํจ\")\n",
" return {\"part_no\": part.group(0), \"eol_date\": norm_date(eol.group(1)), \"ltb_date\": norm_date(ltb.group(1)),\n",
" \"replacement\": rep.group(1) if rep else \"๋ฏธ๊ธฐ์ฌ\"}\n",
"\n",
"\n",
"# 1) ํ์๋ณ ํ์ฑ\n",
"results, total, success = [], 0, 0\n",
"for fmt, text in [(\"csv\", RAW_CSV), (\"tsv\", RAW_TSV), (\"json\", RAW_JSON)]:\n",
" ok, fail = load(text, fmt)\n",
" results += ok; total += len(ok) + len(fail); success += len(ok)\n",
" print(f\"[{fmt.upper():4s}] {len(ok) + len(fail)}ํ ์ค {len(ok)}ํ ์ฑ๊ณต\" + (f\", ์คํจ {[e for _, e in fail]}\" if fail else \"\"))\n",
"notices, n_fail = [], 0\n",
"for t in NOTICES:\n",
" try:\n",
" notices.append(parse_notice(t))\n",
" except ValueError as e:\n",
" n_fail += 1; print(\"๊ณต๊ณ ํ์ฑ ์คํจ:\", e)\n",
"total += len(NOTICES); success += len(notices)\n",
"print(f\"[๊ณต๋ฌธ ํ
์คํธ] ๋จ์ข
๊ณต๊ณ {len(NOTICES)}๊ฑด ์ค {len(notices)}๊ฑด ์ฑ๊ณต\")\n",
"rate = success / total\n",
"print(f\"\\n์ ์ฒด ํ์ฑ ์ฑ๊ณต๋ฅ {rate:.0%} ({success}/{total}) - ๋ชฉํ 100%: {'๋ฌ์ฑ' if rate == 1 else '๋ฏธ๋ฌ'}\")\n",
"\n",
"# 2) ์ ์ ๊ฒฐ๊ณผ(์
์ฒด๋ช
ํ๊ธฐ ํต์ผ, ์ค๋ณต ์ ๊ฑฐ)\n",
"std = pd.DataFrame(results)\n",
"before = len(std)\n",
"std = std.drop_duplicates([\"vendor\", \"part_no\"]).sort_values([\"part_no\", \"vendor\"])\n",
"print(f\"\\n[์ ์ ๋ ๊ณต๊ธ์
์ฒด ์๋ฃ] {before}ํ -> ์ค๋ณต ์ ๊ฑฐ ํ {len(std)}ํ, ์
์ฒด {std['vendor'].nunique()}๊ณณ\")\n",
"print(std.to_string(index=False))\n",
"print(\"\\n[๋จ์ข
๊ณต๊ณ ํ์ฑ ๊ฒฐ๊ณผ]\")\n",
"print(pd.DataFrame(notices).to_string(index=False))\n",
"\n",
"# 3) ์์ ๋ณ๊ฒฝ ๋์ ์ํ: ์ด ์ด๋ฆ์ด ๋ฐ๋ ์ ์์ -> ๋ณ์นญ ์ฌ์ ํ ์ค ์ถ๊ฐ๋ก ์ฒ๋ฆฌ\n",
"new_form = \"ํ๋ ฅ์
์ฒด,์์ฌ๊ตญ,๋ถํ,์กฐ๋ฌ๊ธฐ๊ฐ,๊ฑฐ๋์ผ\\n๊ฐ์์ ๋ฐ(์ฃผ),๊ตญ๋ด,P-0021,30์ผ,2026.04.01\\n\"\n",
"ok, fail = load(new_form, \"csv\")\n",
"print(f\"\\n์ ์์(์ด ์ด๋ฆ ๋ณ๊ฒฝ) 1์ฐจ ์๋: ์ฑ๊ณต {len(ok)} / ์คํจ {len(fail)} -> {fail[0][1] if fail else '-'}\")\n",
"for k, alias in {\"vendor\": \"ํ๋ ฅ์
์ฒด\", \"country\": \"์์ฌ๊ตญ\", \"part_no\": \"๋ถํ\", \"lead\": \"์กฐ๋ฌ๊ธฐ๊ฐ\", \"last_trade\": \"๊ฑฐ๋์ผ\"}.items():\n",
" COL_ALIAS[k].append(alias)\n",
"ok, fail = load(new_form, \"csv\")\n",
"print(f\"๋ณ์นญ ์ฌ์ ๊ฐฑ์ ํ: ์ฑ๊ณต {len(ok)} / ์คํจ {len(fail)} (์ฝ๋ ์์ ์์ด ์ค์ ๋ง ๋ณ๊ฒฝ)\")\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"๐ก [์ฐธ๊ณ ] ๋ฏธ์
02 ์์ ์คํ ๊ฒฐ๊ณผ (Output)
\n",
"\n",
"```text\n",
"[CSV ] 6ํ ์ค 6ํ ์ฑ๊ณต\n",
"[TSV ] 5ํ ์ค 5ํ ์ฑ๊ณต\n",
"[JSON] 3ํ ์ค 3ํ ์ฑ๊ณต\n",
"[๊ณต๋ฌธ ํ
์คํธ] ๋จ์ข
๊ณต๊ณ 5๊ฑด ์ค 5๊ฑด ์ฑ๊ณต\n",
"\n",
"์ ์ฒด ํ์ฑ ์ฑ๊ณต๋ฅ 100% (19/19) - ๋ชฉํ 100%: ๋ฌ์ฑ\n",
"\n",
"[์ ์ ๋ ๊ณต๊ธ์
์ฒด ์๋ฃ] 14ํ -> ์ค๋ณต ์ ๊ฑฐ ํ 13ํ, ์
์ฒด 9๊ณณ\n",
" vendor country part_no lead_days last_trade source\n",
" ๊ฐ์์ ๋ฐ ๊ตญ๋ด P-0001 45 2026-03-02 csv\n",
" ๊ฐ์์ ๋ฐ ๊ตญ๋ด P-0002 56 2026-03-20 json\n",
"VIRTUAL PARTS ๊ฐ์๊ตญ๊ฐA P-0003 180 2026-01-10 csv\n",
" ๊ฐ์์ ๊ณต์ ๊ตญ๋ด P-0003 60 2026-02-27 csv\n",
" ๊ฐ์์ ์ ๊ตญ๋ด P-0004 60 2026-03-15 csv\n",
"SAMPLE OPTICS ๊ฐ์๊ตญ๊ฐB P-0005 210 2026-01-05 csv\n",
" ๊ฐ์๊ดํ ๊ตญ๋ด P-0005 75 2026-02-11 tsv\n",
" MODEL SENSOR ๊ฐ์๊ตญ๊ฐC P-0006 365 2025-11-03 tsv\n",
" ๊ฐ์ํ
ํฌ ๊ตญ๋ด P-0008 90 2026-03-05 tsv\n",
"VIRTUAL PARTS ๊ฐ์๊ตญ๊ฐA P-0011 180 2025-12-30 csv\n",
"SAMPLE OPTICS ๊ฐ์๊ตญ๊ฐB P-0013 200 2026-01-20 tsv\n",
" MODEL SENSOR ๊ฐ์๊ตญ๊ฐC P-0014 150 2026-01-09 json\n",
" ๊ฐ์์์คํ
๊ตญ๋ด P-0016 90 2026-02-01 json\n",
"\n",
"[๋จ์ข
๊ณต๊ณ ํ์ฑ ๊ฒฐ๊ณผ]\n",
"part_no eol_date ltb_date replacement\n",
" P-0003 2027-03-31 2026-12-31 ์์\n",
" P-0011 2027-06-30 2027-01-15 P-0011A\n",
" P-0013 2026-09-30 2026-06-30 ํ์ ํ์\n",
" P-0006 2026-12-31 2026-10-31 P-0006-R\n",
" P-0020 2027-02-28 2026-11-30 ์์\n",
"\n",
"์ ์์(์ด ์ด๋ฆ ๋ณ๊ฒฝ) 1์ฐจ ์๋: ์ฑ๊ณต 0 / ์คํจ 1 -> 'ํ์ ์ด ์์: vendor'\n",
"๋ณ์นญ ์ฌ์ ๊ฐฑ์ ํ: ์ฑ๊ณต 1 / ์คํจ 0 (์ฝ๋ ์์ ์์ด ์ค์ ๋ง ๋ณ๊ฒฝ)\n",
"```\n",
" \n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## ๐ฏ ๋ฏธ์
03. ๊ณต๊ธ์ ๋จ์ผํ ๋ฐ ๋ฉ๊ธฐ ์ง์ฐ ์ด๋ ฅ ๊ธฐ๋ฐ 1์ฐจ ์กฐ๋ฌ ์ํ ์คํฌ๋ฆฌ๋ ์๊ณ ๋ฆฌ์ฆ ๊ตฌํ\n",
"**์์์๊ฐ**: ์ฝ 2.5์๊ฐ\n",
"\n",
"### ๐ ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ์ํ ์ง์นจ\n",
"#### ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ๋น์์ฑ\n",
"\n",
"๊ณต๊ธ์ ๋จ์ผํ ๋ฐ ๋ฉ๊ธฐ ์ง์ฐ ์ด๋ ฅ ๊ธฐ๋ฐ ์กฐ๋ฌ ์ํ ์คํฌ๋ฆฌ๋ ์๊ณ ๋ฆฌ์ฆ์ ๊ตฌํํ๋ค. ์ค์ ์ ์ ํ์ฅ์์๋ ๋จ์ ํ์ง์ ๊ทธ์น์ง ์๊ณ ์ ์ํ ์ํ ํ๋จ๊ณผ ์ ๋ขฐ๋ ๋์ ์์ฌ๊ฒฐ์ ์ด ํ์์ ์ด๋ฏ๋ก, **๊ณต๊ธ์ ๋จ์ผํ์ ๋ฉ๊ธฐ ์ง์ฐ ์ด๋ ฅ ๋ฑ์ ์ ๊ฒํ๋ ์กฐ๋ฌ ์ํ ๋ฃฐ 15์ข
์ด์์ ์ฝ๋ฉํด ๋ถํ๋ณ 1์ฐจ ์คํฌ๋ฆฌ๋ ๋ฑ๊ธ์ ์ฐ์ถํ๋ค.** ๊ณผ์
์ ์์ํ์ฌ ์คํ์ ๋ฐฉ์งํ๊ณ ์์ ๋์๋ ฅ์ ๊ทน๋ํํด์ผ ํฉ๋๋ค.\n",
"\n",
"์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ณธ ๋ฏธ์
์์๋ **๋จ์ผ ๊ณต๊ธ์ , ํด์ธ ์์กด, ๋ฆฌ๋ํ์ ์ด๊ณผ, ๋ฉ๊ธฐ ์ง์ฐ๋ฅ , ๋จ์ข
์๊ณ ยทํ์ , ์ต์ข
๊ตฌ๋งค ์๋ฐ, ๋จ๊ฐ ๊ธ๋ฑ, ์ฌ๊ณ ๊ฐ์ฉ๊ธฐ๊ฐ ๋ถ์กฑ ๋ฑ 16์ข
๋ฃฐ์ YAML ์ค์ ์ผ๋ก ๋๊ณ ๊ฐ์ค์น ํฉ์ผ๋ก ๊ณ ์ํยท์ฃผ์ยท๊ด์ฌ ๋ฑ๊ธ์ ๋ธ๋ค ๊ณผ์ ์ ์งํํ๋** ์ค์ต์ ์ฒด๊ณ์ ์ผ๋ก ์ํํฉ๋๋ค.\n",
"\n",
"ํ์ต์๋ ์ค์ต๊ฐ์ด๋์ ์ ์๋ ์ด 4๊ฐ ์ธ๋ถ ํ๋(๊ธฐ๋ณธ ๊ธฐ๋ฅ ๊ตฌํ โ ๊ฒฐ๊ณผ ์งํ ๊ฒ์ฆ โ ํ๋ผ๋ฏธํฐ ํ๋ ๋ฐ ๋์ ๊ณผ์ )์ ์์ฐจ์ ์ผ๋ก ์งํํ๋ฉฐ ํต์ฌ ํ์ ๊ธฐ์ค์ ๋ฌ์ฑํฉ๋๋ค.\n",
"\n",
"**์ด๋ฒ ๊ฒฐ๊ณผ๋ฌผ:** `src/engine/prc07_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": [
"\"\"\"๊ณต๊ธ์ ๋จ์ผํยท๋ฉ๊ธฐ ์ง์ฐ ์ด๋ ฅ ๋ฑ 16์ข
์กฐ๋ฌ ์ํ ๋ฃฐ(YAML ์ค์ )์ ์ฝ๋ฉํด ๋ถํ๋ณ 1์ฐจ ์คํฌ๋ฆฌ๋ ๋ฑ๊ธ์ ์ฐ์ถํ๊ณ , ๋ฃฐ ์ฒ๋ฆฌ ์ง์ฐ์๊ฐยท๊ณผ๊ฑฐ ๋จ์ ์ด๋ ฅ ๋๋น ํ์ง๋ ฅยท๋ฃฐ๋ณ ์ ์ค ํํฉยท๊ท์ ๋ณ๊ฒฝ ์ ์ค์ ๋ง ๋ฐ๊พธ๋ ํ์ฅ์ฑ์ ํ์ธํ๋ค.\"\"\"\n",
"import time\n",
"import numpy as np\n",
"import pandas as pd\n",
"import matplotlib.pyplot as plt\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# [๊ต์ก์ฉ๊ฐ์์๋ฃ] ๊ฐ์ ๋ฌด๊ธฐ์ฒด๊ณ ๋ถํยท๊ณต๊ธ๋ง ๋ฐ์ดํฐ ์์ฑ๊ธฐ (์ค์ ์ฒด๊ณยท์
์ฒดยท๊ตญ๊ฐยท๋จ๊ฐ ์ ๋ณด ์์)\n",
"# ๋ถํ 100์ข
(ํด์ธ ์กฐ๋ฌ ์ฝ 80%)์ ๊ณต๊ธ๋ง ์์ฑ๊ณผ, ๊ตญ๋ด ๋ถํ ์นดํ๋ก๊ทธ(๊ณผ๊ฑฐ ์น์ธ๋ ๊ตญ์ฐ ๋์ฒดํ +\n",
"# ์ค๋ช
์ ๋น์ทํ์ง๋ง ๊ท๊ฒฉ์ด ๋ง์ง ์๋ ์ ์ฌ ํ๋ณด)๋ฅผ ํจ๊ป ๋ง๋ ๋ค.\n",
"# ---------------------------------------------------------------------------\n",
"COUNTRIES = [\"๊ฐ์๊ตญ๊ฐA\", \"๊ฐ์๊ตญ๊ฐB\", \"๊ฐ์๊ตญ๊ฐC\", \"๊ตญ๋ด\"]\n",
"CATS = {\n",
" \"์ ์๋ชจ๋\": dict(words=[\"DC-DC\", \"์ปจ๋ฒํฐ\", \"์ ์ฐํ\", \"์ ์\", \"์ ์ถ๋ ฅ\"], syn={\"์ปจ๋ฒํฐ\": \"๋ณํ์ฅ์น\", \"์ ์ฐํ\": \"์์ด์๋ ์ด์
\", \"์ ์ถ๋ ฅ\": \"์์ ํ\"}, std=\"MIL-STD-704\", iface=\"๋จ์๋\"),\n",
" \"์ปค๋ฅํฐ\": dict(words=[\"์ํ\", \"์ปค๋ฅํฐ\", \"๋ฐฉ์ํ\", \"๋คํ\", \"๋์ฌ์ฒด๊ฒฐ\"], syn={\"์ปค๋ฅํฐ\": \"์ ์์\", \"๋ฐฉ์ํ\": \"๋ฐํํ\", \"๋์ฌ์ฒด๊ฒฐ\": \"์คํฌ๋ฃจ๊ฒฐํฉ\"}, std=\"MIL-DTL-38999\", iface=\"์ํํ\"),\n",
" \"์ ์๋ฐธ๋ธ\": dict(words=[\"์ ์\", \"์๋ ๋
ธ์ด๋\", \"๋ฐธ๋ธ\", \"๋ฐฉํฅ์ ์ด\", \"4ํฌํธ\"], syn={\"๋ฐธ๋ธ\": \"๊ฐํ์ฅ์น\", \"๋ฐฉํฅ์ ์ด\": \"์ ๋ก์ ํ\", \"์๋ ๋
ธ์ด๋\": \"์ ์์\"}, std=\"SAE-AS5440\", iface=\"ํ๋์ง\"),\n",
" \"๋ฒ ์ด๋ง\": dict(words=[\"๋ณผ\", \"๋ฒ ์ด๋ง\", \"๊ณ ์\", \"๋ฐ๋ดํ\", \"ํ์ ์ถ\"], syn={\"๋ฒ ์ด๋ง\": \"์ถ๋ฐ์ด\", \"๋ฐ๋ดํ\": \"์ค๋ํ\", \"๊ณ ์\": \"๊ณ ํ์ \"}, std=\"ISO-492\", iface=\"์ถ๊ฒฐํฉ\"),\n",
" \"๊ดํ๋ ์ฆ\": dict(words=[\"์ ์ธ์ \", \"๋ ์ฆ\", \"๊ฒ๋ฅด๋ง๋\", \"๋ฐ์ฌ๋ฐฉ์ง\", \"์ง๊ด\"], syn={\"๋ ์ฆ\": \"๊ดํ์ฐฝ\", \"์ง๊ด\": \"์ด์ ํ์ฑ\", \"๋ฐ์ฌ๋ฐฉ์ง\": \"AR์ฝํ
\"}, std=\"MIL-PRF-13830\", iface=\"๋ง์ดํธ\"),\n",
" \"์์ด๋ก์ผ์\": dict(words=[\"๊ด์ฌ์ \", \"์์ด๋ก\", \"๊ฐ์๋\", \"์ผ์\", \"๊ด์ฑ\"], syn={\"์ผ์\": \"๊ฐ์ง๊ธฐ\", \"๊ฐ์๋\": \"ํ์ ์จ\", \"๊ด์ฑ\": \"INS์ฉ\"}, std=\"IEEE-952\", iface=\"์ง๋ ฌํต์ \"),\n",
" \"RF์ฆํญ๊ธฐ\": dict(words=[\"RF\", \"์ ๋ ฅ์ฆํญ๊ธฐ\", \"๊ด๋์ญ\", \"๋ชจ๋\", \"์ก์ \"], syn={\"์ ๋ ฅ์ฆํญ๊ธฐ\": \"ํ์์ฐํ\", \"์ก์ \": \"์ถ๋ ฅ๋จ\", \"๊ด๋์ญ\": \"์์ด๋๋ฐด๋\"}, std=\"MIL-STD-461\", iface=\"SMA\"),\n",
" \"์ ์ด๋ณด๋\": dict(words=[\"FPGA\", \"์ ์ด๋ณด๋\", \"์๋ฒ ๋๋\", \"์ฐ์ฐ\", \"์
์ถ๋ ฅ\"], syn={\"์ ์ด๋ณด๋\": \"์ปจํธ๋กค์นด๋\", \"์ฐ์ฐ\": \"์ ํธ์ฒ๋ฆฌ\", \"์๋ฒ ๋๋\": \"๋ด์ฅํ\"}, std=\"VITA-46\", iface=\"๋ฐฑํ๋ ์ธ\"),\n",
"}\n",
"V_OPTS, W_OPTS = [12, 24, 28], [25, 50, 100]\n",
"VENDORS = [\"๊ฐ์์ ๋ฐ(์ฃผ)\", \"๊ฐ์์ ์(์ฃผ)\", \"๊ฐ์์ ๊ณต์(์ฃผ)\", \"๊ฐ์๊ดํ(์ฃผ)\", \"๊ฐ์ํ
ํฌ(์ฃผ)\", \"๊ฐ์์์คํ
(์ฃผ)\"]\n",
"\n",
"\n",
"def describe(cat, words, s):\n",
" return f\"{cat} {' '.join(words)} {s['v']}V {s['w']}W {s['tmin']}~{s['tmax']}C {s['std']}\"\n",
"\n",
"\n",
"def _sig(x):\n",
" return 1 / (1 + np.exp(-x))\n",
"\n",
"\n",
"def make_parts(n=100, seed=7):\n",
" \"\"\"๋ถํ ๋ชฉ๋ก(๊ณต๊ธ๋ง ์์ฑ ํฌํจ)๊ณผ ๊ตญ๋ด ์นดํ๋ก๊ทธ๋ฅผ ๋ง๋ ๋ค. ๋ฐํ: (parts DataFrame, catalog DataFrame)\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" cats, rows, cat_rows = list(CATS), [], []\n",
" for i in range(n):\n",
" cat = cats[i % len(cats)]; c = CATS[cat]\n",
" spec = {\"v\": int(rng.choice(V_OPTS)), \"w\": int(rng.choice(W_OPTS)), \"tmin\": int(rng.choice([-55, -40])),\n",
" \"tmax\": int(rng.choice([85, 125])), \"size_mm\": float(round(rng.uniform(20, 120), 1)), \"std\": c[\"std\"]}\n",
" words = [str(x) for x in rng.permutation(c[\"words\"])[:4]]\n",
" country = str(rng.choice(COUNTRIES, p=[0.35, 0.3, 0.15, 0.2]))\n",
" overseas = country != \"๊ตญ๋ด\"\n",
" n_sup = int(rng.choice([1, 2, 3], p=[0.45, 0.35, 0.2])) if overseas else int(rng.choice([1, 2, 3]))\n",
" lead = float(round(rng.gamma(3, 45 if overseas else 25) + 20))\n",
" eol = str(rng.choice([\"์ ์\", \"๋จ์ข
์๊ณ \", \"๋จ์ข
\"], p=[0.7, 0.2, 0.1]))\n",
" crit = str(rng.choice([\"A\", \"B\", \"C\"], p=[0.3, 0.4, 0.3]))\n",
" z = (-2.6 + 1.1 * (n_sup == 1) + 0.9 * overseas + 0.006 * (lead - 100) + 1.2 * (eol == \"๋จ์ข
์๊ณ \")\n",
" + 2.0 * (eol == \"๋จ์ข
\") + rng.normal(0, 0.6))\n",
" p_late = float(np.clip(_sig(z) * 0.8 + 0.05, 0, 0.9))\n",
" late = int(rng.binomial(12, p_late))\n",
" demand = int(rng.integers(20, 400))\n",
" rows.append({\"part_no\": f\"P-{i + 1:04d}\", \"category\": cat, \"desc\": describe(cat, words, spec), **spec,\n",
" \"iface\": c[\"iface\"], \"country\": country, \"overseas\": overseas, \"n_suppliers\": n_sup,\n",
" \"lead_time_days\": lead, \"orders_12m\": 12, \"late_orders\": late,\n",
" \"avg_delay_days\": float(round(late and rng.gamma(2, 12 + 20 * p_late), 1)),\n",
" \"eol_status\": eol, \"ltb_months\": int(rng.integers(1, 25)) if eol == \"๋จ์ข
์๊ณ \" else None,\n",
" \"price_change_pct\": float(round(rng.normal(4 + 12 * p_late, 8), 1)), \"criticality\": crit,\n",
" \"annual_demand\": demand, \"stock_qty\": int(rng.integers(0, demand)),\n",
" \"defect_rate\": float(round(abs(rng.normal(0.8 + 2 * p_late, 0.8)), 2)),\n",
" \"export_license\": bool(overseas and rng.random() < 0.35),\n",
" \"disrupted_next12m\": bool(rng.random() < _sig(z + 0.4))})\n",
" if not overseas:\n",
" continue\n",
" # ๊ณผ๊ฑฐ ์น์ธ๋ ๊ตญ์ฐ ๋์ฒดํ(์ ๋ต): ๋์์ด๋ก ์์ , ๊ท๊ฒฉ ํธํ(์ ๋ ฅ ์ฌ์ ํ์ฉ, ์ผ๋ถ๋ ํฌ๊ธฐ ์ฐจ์ด๊ฐ ์ปค ์ฅ์ฐฉ ๊ฒํ ํ์)\n",
" eq_words = [c[\"syn\"].get(w, w) for w in words]\n",
" hard = rng.random() < 0.15 # ์์ ์ด ๋ง์ด ๋ค๋ฅธ ์ฌ๋ก\n",
" if hard:\n",
" eq_words = [c[\"syn\"].get(w, w) for w in c[\"words\"] if w not in words][:1] + eq_words[1:3]\n",
" eq = {**spec, \"w\": spec[\"w\"] if rng.random() < 0.7 else W_OPTS[min(W_OPTS.index(spec[\"w\"]) + 1, 2)],\n",
" \"size_mm\": float(round(spec[\"size_mm\"] * (rng.uniform(1.11, 1.15) if rng.random() < 0.12 else rng.uniform(0.95, 1.05)), 1))}\n",
" cat_rows.append({\"cand_no\": f\"D-{len(cat_rows) + 1:04d}\", \"category\": cat, \"desc\": describe(cat, eq_words, eq), **eq,\n",
" \"iface\": c[\"iface\"], \"vendor\": str(rng.choice(VENDORS)), \"approved_for\": f\"P-{i + 1:04d}\", \"kind\": \"equivalent\"})\n",
" # ์ ์ฌ ์ค๋ต ํ๋ณด: ์ค๋ช
์ ๊ฑฐ์ ๊ฐ์ง๋ง ์ ์์ด ๋ค๋ฅด๊ฑฐ๋(5V/48V) ์์ฉ ์จ๋ ๋ฑ๊ธ(0~70C)\n",
" for kind in (\"volt_mismatch\", \"temp_mismatch\"):\n",
" d = dict(spec)\n",
" if kind == \"volt_mismatch\":\n",
" d[\"v\"] = int(rng.choice([5, 48])) # ์ฒด๊ณ์์ ์ฐ์ง ์๋ ์ ์\n",
" else:\n",
" d[\"tmin\"], d[\"tmax\"] = 0, 70\n",
" d[\"size_mm\"] = float(round(spec[\"size_mm\"] * rng.uniform(0.95, 1.05), 1))\n",
" cat_rows.append({\"cand_no\": f\"D-{len(cat_rows) + 1:04d}\", \"category\": cat, \"desc\": describe(cat, words, d), **d,\n",
" \"iface\": c[\"iface\"], \"vendor\": str(rng.choice(VENDORS)), \"approved_for\": None, \"kind\": kind})\n",
" parts = pd.DataFrame(rows)\n",
" catalog = pd.DataFrame(cat_rows).sample(frac=1, random_state=seed).reset_index(drop=True)\n",
" return parts, catalog\n",
"# ---------------------------------------------------------------------------\n",
"\n",
"# ---- ์กฐ๋ฌ ์ํ ์คํฌ๋ฆฌ๋ ๋ฃฐ ์ค์ (YAML ํ์ ๋ฌธ์์ด: ๊ท์ ์ด ๋ฐ๋๋ฉด ์ฝ๋๊ฐ ์๋๋ผ ์ด ์ค์ ๋ง ๊ณ ์น๋ค) ----\n",
"RULES_YAML = \"\"\"\n",
"- id: R01\n",
" name: ๋จ์ผ ๊ณต๊ธ์ \n",
" field: n_suppliers\n",
" op: \"==\"\n",
" value: 1\n",
" weight: 3\n",
"- id: R02\n",
" name: ํด์ธ ์กฐ๋ฌ ์์กด\n",
" field: overseas\n",
" op: \"==\"\n",
" value: true\n",
" weight: 1\n",
"- id: R03\n",
" name: ๋ฆฌ๋ํ์ 180์ผ ์ด๊ณผ\n",
" field: lead_time_days\n",
" op: \">\"\n",
" value: 180\n",
" weight: 2\n",
"- id: R04\n",
" name: ๋ฉ๊ธฐ ์ง์ฐ๋ฅ 30% ์ด์\n",
" field: delay_rate\n",
" op: \">=\"\n",
" value: 0.3\n",
" weight: 3\n",
"- id: R05\n",
" name: ํ๊ท ์ง์ฐ 30์ผ ์ด๊ณผ\n",
" field: avg_delay_days\n",
" op: \">\"\n",
" value: 30\n",
" weight: 2\n",
"- id: R06\n",
" name: ๋จ์ข
์๊ณ \n",
" field: eol_status\n",
" op: \"==\"\n",
" value: ๋จ์ข
์๊ณ \n",
" weight: 3\n",
"- id: R07\n",
" name: ๋จ์ข
ํ์ \n",
" field: eol_status\n",
" op: \"==\"\n",
" value: ๋จ์ข
\n",
" weight: 4\n",
"- id: R08\n",
" name: ์ต์ข
๊ตฌ๋งค 6๊ฐ์ ์ด๋ด\n",
" field: ltb_months\n",
" op: \"<=\"\n",
" value: 6\n",
" weight: 3\n",
"- id: R09\n",
" name: ๋จ๊ฐ 20% ์ด์ ๊ธ๋ฑ\n",
" field: price_change_pct\n",
" op: \">=\"\n",
" value: 20\n",
" weight: 2\n",
"- id: R10\n",
" name: ํต์ฌ(A) ๋ถํ ๋จ์ผ ๊ณต๊ธ\n",
" field: crit_single\n",
" op: \"==\"\n",
" value: true\n",
" weight: 3\n",
"- id: R11\n",
" name: ์ฌ๊ณ ๊ฐ์ฉ๊ธฐ๊ฐ < ๋ฆฌ๋ํ์\n",
" field: cover_gap_days\n",
" op: \"<\"\n",
" value: 0\n",
" weight: 3\n",
"- id: R12\n",
" name: ์
๊ณ ๋ถ๋๋ฅ 2% ์ด๊ณผ\n",
" field: defect_rate\n",
" op: \">\"\n",
" value: 2.0\n",
" weight: 2\n",
"- id: R13\n",
" name: ์์ถ ํ๊ฐ ํ์\n",
" field: export_license\n",
" op: \"==\"\n",
" value: true\n",
" weight: 2\n",
"- id: R14\n",
" name: ์ฌ๊ณ ์์\n",
" field: stock_qty\n",
" op: \"==\"\n",
" value: 0\n",
" weight: 2\n",
"- id: R15\n",
" name: ๊ณต๊ธ๊ตญ ํธ์ค(30% ์ด์)\n",
" field: country_share\n",
" op: \">=\"\n",
" value: 0.3\n",
" weight: 1\n",
"- id: R16\n",
" name: ์ง์ฐ ์ด๋ ฅ ์๋ ๋จ์ผ ๊ณต๊ธ์ \n",
" field: single_late\n",
" op: \"==\"\n",
" value: true\n",
" weight: 2\n",
"\"\"\"\n",
"GRADE_CUT = {\"๊ณ ์ํ\": 16, \"์ฃผ์\": 10} # ๊ฐ์ค์น ํฉ ๊ธฐ์ค 1์ฐจ ์คํฌ๋ฆฌ๋ ๋ฑ๊ธ\n",
"\n",
"\n",
"def parse_rules(text):\n",
" \"\"\"๊ฐ์ด YAML ํ์(๋ชฉ๋ก + key: value). ์ซ์ยทtrue/falseยท๋ฐ์ดํ ๋ฌธ์์ด์ ํด์ํ๋ค.\"\"\"\n",
" rules, cur = [], None\n",
" for line in text.strip().splitlines():\n",
" if not line.strip():\n",
" continue\n",
" s = line.strip()\n",
" if s.startswith(\"- \"):\n",
" cur = {}; rules.append(cur); s = s[2:]\n",
" k, v = [x.strip() for x in s.split(\":\", 1)]\n",
" v = v.strip('\"')\n",
" if v in (\"true\", \"false\"):\n",
" v = v == \"true\"\n",
" else:\n",
" try:\n",
" v = float(v) if \".\" in v else int(v)\n",
" except ValueError:\n",
" pass\n",
" cur[k] = v\n",
" return rules\n",
"\n",
"\n",
"OPS = {\"==\": lambda a, b: a == b, \"!=\": lambda a, b: a != b, \">\": lambda a, b: a > b, \">=\": lambda a, b: a >= b,\n",
" \"<\": lambda a, b: a < b, \"<=\": lambda a, b: a <= b}\n",
"\n",
"\n",
"def derive(df):\n",
" \"\"\"๋ฃฐ์ ์ฐ๋ ํ์ ํ๋(์ง์ฐ๋ฅ , ํต์ฌ ๋จ์ผ๊ณต๊ธ, ์ฌ๊ณ ๊ฐ์ฉ๊ธฐ๊ฐ ์ฐจ์ด, ๊ณต๊ธ๊ตญ ๋น์ค ๋ฑ)๋ฅผ ๋ง๋ ๋ค.\"\"\"\n",
" d = df.copy()\n",
" d[\"delay_rate\"] = d[\"late_orders\"] / d[\"orders_12m\"].clip(lower=1)\n",
" d[\"crit_single\"] = (d[\"criticality\"] == \"A\") & (d[\"n_suppliers\"] == 1)\n",
" d[\"cover_gap_days\"] = d[\"stock_qty\"] / d[\"annual_demand\"].clip(lower=1) * 365 - d[\"lead_time_days\"]\n",
" d[\"country_share\"] = d[\"country\"].map(d[\"country\"].value_counts(normalize=True)).where(d[\"overseas\"], 0.0)\n",
" d[\"single_late\"] = (d[\"n_suppliers\"] == 1) & (d[\"late_orders\"] >= 3)\n",
" return d\n",
"\n",
"\n",
"def apply_rules(rec, rules):\n",
" \"\"\"๋ ์ฝ๋ 1๊ฑด(dict)์ ๋ฃฐ์ ์ ์ฉํด (์ํ ์ ์, ๊ฑธ๋ฆฐ ๋ฃฐ ๋ชฉ๋ก)์ ๋๋ ค์ค๋ค. ๊ฐ์ด ์์ผ๋ฉด ํด๋น ๋ฃฐ์ ๊ฑด๋๋ด๋ค.\"\"\"\n",
" hits = []\n",
" for r in rules:\n",
" v = rec.get(r[\"field\"])\n",
" if v is None or (isinstance(v, float) and np.isnan(v)):\n",
" continue\n",
" if OPS[r[\"op\"]](v, r[\"value\"]):\n",
" hits.append(r)\n",
" score = sum(r[\"weight\"] for r in hits)\n",
" grade = \"๊ณ ์ํ\" if score >= GRADE_CUT[\"๊ณ ์ํ\"] else (\"์ฃผ์\" if score >= GRADE_CUT[\"์ฃผ์\"] else \"๊ด์ฌ\")\n",
" return score, grade, [r[\"id\"] for r in hits]\n",
"\n",
"# [๊ต์ก์ฉ๊ฐ์์๋ฃ] ๋ฃฐ๊ณผ ๊ฐ์ ์ง์นจ ์กฐํญ์ ๋์ํ(์ค์ ๊ท์ ์กฐ๋ฌธ์ด ์๋๋ผ ์ฐ๊ฒฐ ๋ฐฉ์์ ๋ณด์ฌ ์ฃผ๋ ์์)\n",
"CLAUSE = {\"R01\": \"๊ฐ์์ง์นจ 3์กฐ1ํญ(๊ณต๊ธ์ ๋ค์ํ)\", \"R02\": \"๊ฐ์์ง์นจ 3์กฐ2ํญ(ํด์ธ ์์กด ๊ด๋ฆฌ)\", \"R03\": \"๊ฐ์์ง์นจ 4์กฐ1ํญ(๋ฆฌ๋ํ์ ๊ด๋ฆฌ)\",\n",
" \"R04\": \"๊ฐ์์ง์นจ 4์กฐ2ํญ(๋ฉ๊ธฐ ์ค์)\", \"R05\": \"๊ฐ์์ง์นจ 4์กฐ2ํญ(๋ฉ๊ธฐ ์ค์)\", \"R06\": \"๊ฐ์์ง์นจ 5์กฐ1ํญ(๋จ์ข
๊ด๋ฆฌ)\",\n",
" \"R07\": \"๊ฐ์์ง์นจ 5์กฐ1ํญ(๋จ์ข
๊ด๋ฆฌ)\", \"R08\": \"๊ฐ์์ง์นจ 5์กฐ2ํญ(์ต์ข
๊ตฌ๋งค ํ๋จ)\", \"R09\": \"๊ฐ์์ง์นจ 6์กฐ1ํญ(๊ฐ๊ฒฉ ๋ณ๋ ์ ๊ฒ)\",\n",
" \"R10\": \"๊ฐ์์ง์นจ 3์กฐ3ํญ(ํต์ฌ ๋ถํ ๊ด๋ฆฌ)\", \"R11\": \"๊ฐ์์ง์นจ 4์กฐ3ํญ(์ฌ๊ณ ํ๋ณด)\", \"R12\": \"๊ฐ์์ง์นจ 7์กฐ1ํญ(ํ์ง ๊ด๋ฆฌ)\",\n",
" \"R13\": \"๊ฐ์์ง์นจ 3์กฐ4ํญ(์์ถํต์ ํ์ธ)\", \"R14\": \"๊ฐ์์ง์นจ 4์กฐ3ํญ(์ฌ๊ณ ํ๋ณด)\", \"R15\": \"๊ฐ์์ง์นจ 3์กฐ2ํญ(ํด์ธ ์์กด ๊ด๋ฆฌ)\",\n",
" \"R16\": \"๊ฐ์์ง์นจ 3์กฐ1ํญ(๊ณต๊ธ์ ๋ค์ํ)\"}\n",
"\n",
"rules = parse_rules(RULES_YAML)\n",
"parts, _ = make_parts(100, seed=7)\n",
"df = derive(parts)\n",
"recs = df.to_dict(\"records\")\n",
"\n",
"# 1) ์ ์ฒด ๋ถํ ์คํฌ๋ฆฌ๋๊ณผ ์ฒ๋ฆฌ ์ง์ฐ์๊ฐ(๋ ์ฝ๋ 1๊ฑด๋น)\n",
"lat = []\n",
"res = []\n",
"for r in recs:\n",
" t0 = time.perf_counter()\n",
" out = apply_rules(r, rules)\n",
" lat.append((time.perf_counter() - t0) * 1000)\n",
" res.append(out)\n",
"df[\"score\"], df[\"grade\"], df[\"hits\"] = [x[0] for x in res], [x[1] for x in res], [x[2] for x in res]\n",
"print(f\"[๋ฃฐ ์ค์ ] ๊ฒ์ฆ ๋ฃฐ {len(rules)}์ข
(๋ชฉํ 15์ข
์ด์: {'๋ฌ์ฑ' if len(rules) >= 15 else '๋ฏธ๋ฌ'}), ๋ชจ๋ ๋ฃฐ์ ๊ฐ์ ์ง์นจ ์กฐํญ ์ฐ๊ฒฐ {all(r['id'] in CLAUSE for r in rules)}\")\n",
"print(f\"๋ฃฐ ์ฒ๋ฆฌ ์ง์ฐ์๊ฐ: ํ๊ท {np.mean(lat):.3f}ms, ์ต๋ {np.max(lat):.3f}ms (๋ชฉํ 50ms ์ดํ: {'๋ฌ์ฑ' if max(lat) <= 50 else '๋ฏธ๋ฌ'})\")\n",
"\n",
"# 2) ๋ฑ๊ธ ๋ถํฌ์ ๊ณผ๊ฑฐ ๋จ์ ์ด๋ ฅ(๋ค์ 12๊ฐ์ ์ค์ ๋จ์ ์ฌ๋ถ) ๋๋น ํ์ง๋ ฅ\n",
"print(\"\\n[1์ฐจ ์คํฌ๋ฆฌ๋ ๋ฑ๊ธ ๋ถํฌ]\")\n",
"print(df.groupby(\"grade\").agg(๋ถํ์=(\"part_no\", \"size\"), ์ค์ ๋จ์ ๋ฅ =(\"disrupted_next12m\", \"mean\")).reindex([\"๊ณ ์ํ\", \"์ฃผ์\", \"๊ด์ฌ\"]).round(2).to_string())\n",
"y = df[\"disrupted_next12m\"].to_numpy()\n",
"flag = df[\"grade\"].isin([\"๊ณ ์ํ\", \"์ฃผ์\"]).to_numpy()\n",
"tp = int((flag & y).sum())\n",
"print(f\"๊ณ ์ํยท์ฃผ์ ์ ๋ณ: ์ค์ ๋จ์ ๋ถํ {y.sum()}๊ฑด ์ค {tp}๊ฑด ํฌ์ฐฉ(์ฌํ์จ {tp / y.sum():.0%}), ์ ๋ณ {flag.sum()}๊ฑด ์ค ์ ๋ฐ๋ {tp / max(flag.sum(), 1):.0%}\")\n",
"\n",
"# 3) ๋ฃฐ๋ณ ์ ์ค ๊ฑด์(์ด๋ค ๋ฃฐ์ด ์ผ๋ง๋ ๊ฑธ๋ฆฌ๋๊ฐ)\n",
"cnt = pd.Series([h for hs in df[\"hits\"] for h in hs]).value_counts()\n",
"tbl = pd.DataFrame([{\"๋ฃฐ\": r[\"id\"], \"์ด๋ฆ\": r[\"name\"], \"๊ฐ์ค์น\": r[\"weight\"], \"์ ์ค\": int(cnt.get(r[\"id\"], 0)), \"๊ทผ๊ฑฐ ์กฐํญ\": CLAUSE[r[\"id\"]]} for r in rules])\n",
"print(\"\\n[๋ฃฐ๋ณ ์ ์ค ํํฉ]\")\n",
"print(tbl.to_string(index=False))\n",
"\n",
"# 4) ์์ ์ํ ๋ถํ 5๊ฑด๊ณผ ํ์ ๊ทผ๊ฑฐ(์ค๋ช
๊ฐ๋ฅ์ฑ)\n",
"print(\"\\n[๊ณ ์ํ ์์ 5๊ฑด๊ณผ ๊ทผ๊ฑฐ]\")\n",
"for _, r in df.sort_values(\"score\", ascending=False).head(5).iterrows():\n",
" names = \", \".join(next(x[\"name\"] for x in rules if x[\"id\"] == h) for h in r[\"hits\"])\n",
" print(f\" {r['part_no']} {r['category']} ์ ์ {r['score']:2d} {r['grade']} (์ ์ค ๋ฃฐ {len(r['hits'])}์ข
): {names[:60]}...\")\n",
"\n",
"# 5) ๊ท์ ๋ณ๊ฒฝ ๋์: ๋ฆฌ๋ํ์ ๊ธฐ์ค์ 180์ผ์์ 150์ผ๋ก ๋ฐ๊พธ๋ ์ค์ ๋ณ๊ฒฝ(์ฝ๋ ์์ ์์)\n",
"new_yaml = RULES_YAML.replace(\"value: 180\", \"value: 120\")\n",
"rules2 = parse_rules(new_yaml)\n",
"o2 = [apply_rules(r, rules2) for r in recs]\n",
"n_score = sum(a != b[0] for a, b in zip(df[\"score\"], o2)); n_grade = sum(a != b[1] for a, b in zip(df[\"grade\"], o2))\n",
"print(f\"\\n[์ค์ ๋ณ๊ฒฝ] R03 ๋ฆฌ๋ํ์ ๊ธฐ์ค 180์ผ -> 120์ผ: ์ ์๊ฐ ๋ฐ๋ ๋ถํ {n_score}๊ฑด, ๋ฑ๊ธ์ด ๋ฐ๋ ๋ถํ {n_grade}๊ฑด (์ฝ๋ ์์ ์์ด YAML๋ง ๋ณ๊ฒฝ)\")\n",
"print(\"์ฒ๋ฆฌ ํ ์ฌ๋ ๊ฒํ : ์คํฌ๋ฆฌ๋์ 1์ฐจ ์ ๋ณ์ด๋ฉฐ, ์ต์ข
์ํ ํ๋จ๊ณผ ์กฐ์น๋ ์กฐ๋ฌ ๋ด๋น์๊ฐ ๊ฒฐ์ ํ๋ค.\")\n",
"\n",
"fig, ax = plt.subplots(1, 2, figsize=(11, 4))\n",
"ax[0].bar(tbl[\"๋ฃฐ\"], tbl[\"์ ์ค\"], color=\"tab:blue\"); ax[0].set_title(\"Rule hits by rule (synthetic)\"); ax[0].set_ylabel(\"parts\")\n",
"g = df.groupby(\"grade\")[\"disrupted_next12m\"].mean().reindex([\"๊ด์ฌ\", \"์ฃผ์\", \"๊ณ ์ํ\"])\n",
"ax[1].bar([\"low\", \"watch\", \"high\"], g.values, color=[\"tab:green\", \"gold\", \"tab:red\"]); ax[1].set_title(\"Actual disruption rate by screening grade\"); ax[1].set_ylim(0, 1)\n",
"plt.tight_layout(); plt.show()\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"๐ก [์ฐธ๊ณ ] ๋ฏธ์
03 ์์ ์คํ ๊ฒฐ๊ณผ (Output)
\n",
"\n",
"```text\n",
"[๋ฃฐ ์ค์ ] ๊ฒ์ฆ ๋ฃฐ 16์ข
(๋ชฉํ 15์ข
์ด์: ๋ฌ์ฑ), ๋ชจ๋ ๋ฃฐ์ ๊ฐ์ ์ง์นจ ์กฐํญ ์ฐ๊ฒฐ True\n",
"๋ฃฐ ์ฒ๋ฆฌ ์ง์ฐ์๊ฐ: ํ๊ท 0.012ms, ์ต๋ 0.043ms (๋ชฉํ 50ms ์ดํ: ๋ฌ์ฑ)\n",
"\n",
"[1์ฐจ ์คํฌ๋ฆฌ๋ ๋ฑ๊ธ ๋ถํฌ]\n",
" ๋ถํ์ ์ค์ ๋จ์ ๋ฅ \n",
"grade \n",
"๊ณ ์ํ 20 0.60\n",
"์ฃผ์ 38 0.39\n",
"๊ด์ฌ 42 0.29\n",
"๊ณ ์ํยท์ฃผ์ ์ ๋ณ: ์ค์ ๋จ์ ๋ถํ 39๊ฑด ์ค 27๊ฑด ํฌ์ฐฉ(์ฌํ์จ 69%), ์ ๋ณ 58๊ฑด ์ค ์ ๋ฐ๋ 47%\n",
"\n",
"[๋ฃฐ๋ณ ์ ์ค ํํฉ]\n",
" ๋ฃฐ ์ด๋ฆ ๊ฐ์ค์น ์ ์ค ๊ทผ๊ฑฐ ์กฐํญ\n",
"R01 ๋จ์ผ ๊ณต๊ธ์ 3 42 ๊ฐ์์ง์นจ 3์กฐ1ํญ(๊ณต๊ธ์ ๋ค์ํ)\n",
"R02 ํด์ธ ์กฐ๋ฌ ์์กด 1 85 ๊ฐ์์ง์นจ 3์กฐ2ํญ(ํด์ธ ์์กด ๊ด๋ฆฌ)\n",
"R03 ๋ฆฌ๋ํ์ 180์ผ ์ด๊ณผ 2 27 ๊ฐ์์ง์นจ 4์กฐ1ํญ(๋ฆฌ๋ํ์ ๊ด๋ฆฌ)\n",
"R04 ๋ฉ๊ธฐ ์ง์ฐ๋ฅ 30% ์ด์ 3 49 ๊ฐ์์ง์นจ 4์กฐ2ํญ(๋ฉ๊ธฐ ์ค์)\n",
"R05 ํ๊ท ์ง์ฐ 30์ผ ์ด๊ณผ 2 64 ๊ฐ์์ง์นจ 4์กฐ2ํญ(๋ฉ๊ธฐ ์ค์)\n",
"R06 ๋จ์ข
์๊ณ 3 26 ๊ฐ์์ง์นจ 5์กฐ1ํญ(๋จ์ข
๊ด๋ฆฌ)\n",
"R07 ๋จ์ข
ํ์ 4 11 ๊ฐ์์ง์นจ 5์กฐ1ํญ(๋จ์ข
๊ด๋ฆฌ)\n",
"R08 ์ต์ข
๊ตฌ๋งค 6๊ฐ์ ์ด๋ด 3 6 ๊ฐ์์ง์นจ 5์กฐ2ํญ(์ต์ข
๊ตฌ๋งค ํ๋จ)\n",
"R09 ๋จ๊ฐ 20% ์ด์ ๊ธ๋ฑ 2 8 ๊ฐ์์ง์นจ 6์กฐ1ํญ(๊ฐ๊ฒฉ ๋ณ๋ ์ ๊ฒ)\n",
"R10 ํต์ฌ(A) ๋ถํ ๋จ์ผ ๊ณต๊ธ 3 14 ๊ฐ์์ง์นจ 3์กฐ3ํญ(ํต์ฌ ๋ถํ ๊ด๋ฆฌ)\n",
"R11 ์ฌ๊ณ ๊ฐ์ฉ๊ธฐ๊ฐ < ๋ฆฌ๋ํ์ 3 40 ๊ฐ์์ง์นจ 4์กฐ3ํญ(์ฌ๊ณ ํ๋ณด)\n",
"R12 ์
๊ณ ๋ถ๋๋ฅ 2% ์ด๊ณผ 2 24 ๊ฐ์์ง์นจ 7์กฐ1ํญ(ํ์ง ๊ด๋ฆฌ)\n",
"R13 ์์ถ ํ๊ฐ ํ์ 2 35 ๊ฐ์์ง์นจ 3์กฐ4ํญ(์์ถํต์ ํ์ธ)\n",
"R14 ์ฌ๊ณ ์์ 2 1 ๊ฐ์์ง์นจ 4์กฐ3ํญ(์ฌ๊ณ ํ๋ณด)\n",
"R15 ๊ณต๊ธ๊ตญ ํธ์ค(30% ์ด์) 1 70 ๊ฐ์์ง์นจ 3์กฐ2ํญ(ํด์ธ ์์กด ๊ด๋ฆฌ)\n",
"R16 ์ง์ฐ ์ด๋ ฅ ์๋ ๋จ์ผ ๊ณต๊ธ์ 2 35 ๊ฐ์์ง์นจ 3์กฐ1ํญ(๊ณต๊ธ์ ๋ค์ํ)\n",
"\n",
"[๊ณ ์ํ ์์ 5๊ฑด๊ณผ ๊ทผ๊ฑฐ]\n",
" P-0087 RF์ฆํญ๊ธฐ ์ ์ 26 ๊ณ ์ํ (์ ์ค ๋ฃฐ 11์ข
): ๋จ์ผ ๊ณต๊ธ์ , ํด์ธ ์กฐ๋ฌ ์์กด, ๋ฉ๊ธฐ ์ง์ฐ๋ฅ 30% ์ด์, ํ๊ท ์ง์ฐ 30์ผ ์ด๊ณผ, ๋จ์ข
์๊ณ , ์ต์ข
๊ตฌ๋งค 6...\n",
" P-0028 ๋ฒ ์ด๋ง ์ ์ 25 ๊ณ ์ํ (์ ์ค ๋ฃฐ 11์ข
): ๋จ์ผ ๊ณต๊ธ์ , ํด์ธ ์กฐ๋ฌ ์์กด, ๋ฆฌ๋ํ์ 180์ผ ์ด๊ณผ, ๋ฉ๊ธฐ ์ง์ฐ๋ฅ 30% ์ด์, ํ๊ท ์ง์ฐ 30์ผ ์ด๊ณผ,...\n",
" P-0066 ์ปค๋ฅํฐ ์ ์ 25 ๊ณ ์ํ (์ ์ค ๋ฃฐ 11์ข
): ๋จ์ผ ๊ณต๊ธ์ , ํด์ธ ์กฐ๋ฌ ์์กด, ๋ฆฌ๋ํ์ 180์ผ ์ด๊ณผ, ๋ฉ๊ธฐ ์ง์ฐ๋ฅ 30% ์ด์, ํ๊ท ์ง์ฐ 30์ผ ์ด๊ณผ,...\n",
" P-0033 ์ ์๋ชจ๋ ์ ์ 24 ๊ณ ์ํ (์ ์ค ๋ฃฐ 11์ข
): ๋จ์ผ ๊ณต๊ธ์ , ํด์ธ ์กฐ๋ฌ ์์กด, ๋ฆฌ๋ํ์ 180์ผ ์ด๊ณผ, ๋ฉ๊ธฐ ์ง์ฐ๋ฅ 30% ์ด์, ํ๊ท ์ง์ฐ 30์ผ ์ด๊ณผ,...\n",
" P-0077 ๊ดํ๋ ์ฆ ์ ์ 24 ๊ณ ์ํ (์ ์ค ๋ฃฐ 10์ข
): ๋จ์ผ ๊ณต๊ธ์ , ํด์ธ ์กฐ๋ฌ ์์กด, ๋ฉ๊ธฐ ์ง์ฐ๋ฅ 30% ์ด์, ํ๊ท ์ง์ฐ 30์ผ ์ด๊ณผ, ๋จ์ข
ํ์ , ํต์ฌ(A) ...\n",
"\n",
"[์ค์ ๋ณ๊ฒฝ] R03 ๋ฆฌ๋ํ์ ๊ธฐ์ค 180์ผ -> 120์ผ: ์ ์๊ฐ ๋ฐ๋ ๋ถํ 27๊ฑด, ๋ฑ๊ธ์ด ๋ฐ๋ ๋ถํ 8๊ฑด (์ฝ๋ ์์ ์์ด YAML๋ง ๋ณ๊ฒฝ)\n",
"์ฒ๋ฆฌ ํ ์ฌ๋ ๊ฒํ : ์คํฌ๋ฆฌ๋์ 1์ฐจ ์ ๋ณ์ด๋ฉฐ, ์ต์ข
์ํ ํ๋จ๊ณผ ์กฐ์น๋ ์กฐ๋ฌ ๋ด๋น์๊ฐ ๊ฒฐ์ ํ๋ค.\n",
"```\n",
" \n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## ๐ฏ ๋ฏธ์
04. NLP ๋ฐ ์ง์ ๊ทธ๋ํ ๊ธฐ๋ฐ ๋์ฒด ๊ฐ๋ฅ ํธํ ๋ถํ(Alternative Parts) ์๋ ์ถ์ฒ ๋ชจ๋ธ ๊ณ ๋ํ\n",
"**์์์๊ฐ**: ์ฝ 3.0์๊ฐ\n",
"\n",
"### ๐ ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ์ํ ์ง์นจ\n",
"#### ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ๋น์์ฑ\n",
"\n",
"์ง์ ๊ทธ๋ํ ๊ธฐ๋ฐ ๋์ฒด ๊ฐ๋ฅ ํธํ ๋ถํ(Alternative Parts) ์๋ ์ถ์ฒ ๋ชจ๋ธ์ ๊ณ ๋ํํ๋ค. ์ค์ ์ ์ ํ์ฅ์์๋ ๋จ์ ํ์ง์ ๊ทธ์น์ง ์๊ณ ์ ์ํ ์ํ ํ๋จ๊ณผ ์ ๋ขฐ๋ ๋์ ์์ฌ๊ฒฐ์ ์ด ํ์์ ์ด๋ฏ๋ก, **ํ
์คํธ ์ ์ฌ๋์ ์ง์ ๊ทธ๋ํ, ๊ท๊ฒฉ ์ ํฉ์ฑ์ ๊ฒฐํฉํด ํด์ธ ๋ถํ๋ณ ๊ตญ์ฐ ๋์ฒด ํ๋ณด๋ฅผ ์ถ์ฒํ๊ณ ์ถ์ฒ ๊ทผ๊ฑฐ๋ฅผ ์ ์ํ๋ค.** ๊ณผ์
์ ์์ํ์ฌ ์คํ์ ๋ฐฉ์งํ๊ณ ์์ ๋์๋ ฅ์ ๊ทน๋ํํด์ผ ํฉ๋๋ค.\n",
"\n",
"์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ณธ ๋ฏธ์
์์๋ **๋ถํ ์ค๋ช
๋ฌธ์ TF-IDF ์ ์ฌ๋, ์ ์ยท์ ๋ ฅยท์จ๋ยทํฌ๊ธฐยท์ธํฐํ์ด์ค 6๊ฐ ๊ท๊ฒฉ ์ ํฉ์ฑ, ๋์์ด๋ฅผ ๊ฐ์ ์ผ๋ก ์ฐ๊ฒฐํ ์ง์ ๊ทธ๋ํ ์ด์ ์ ์ฌ๋๋ฅผ ๊ฐ์คํฉํด ๋ถํ๋ณ Top-3 ํ๋ณด ์์ฑํ์ฌ ์์ธ์ ๋ถ์ํ๋** ์ค์ต์ ์ฒด๊ณ์ ์ผ๋ก ์ํํฉ๋๋ค.\n",
"\n",
"ํ์ต์๋ ์ค์ต๊ฐ์ด๋์ ์ ์๋ ์ด 4๊ฐ ์ธ๋ถ ํ๋(๊ธฐ๋ณธ ๊ธฐ๋ฅ ๊ตฌํ โ ๊ฒฐ๊ณผ ์งํ ๊ฒ์ฆ โ ํ๋ผ๋ฏธํฐ ํ๋ ๋ฐ ๋์ ๊ณผ์ )์ ์์ฐจ์ ์ผ๋ก ์งํํ๋ฉฐ ํต์ฌ ํ์ ๊ธฐ์ค์ ๋ฌ์ฑํฉ๋๋ค.\n",
"\n",
"**์ด๋ฒ ๊ฒฐ๊ณผ๋ฌผ:** `src/ai/prc07_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": [
"\"\"\"๋ถํ ์ค๋ช
๋ฌธ์ TF-IDF ํ
์คํธ ์ ์ฌ๋, ๊ท๊ฒฉ ์ ํฉ์ฑ, ๋์์ด๋ฅผ ์ฐ๊ฒฐํ ์ง์ ๊ทธ๋ํ ์ด์ ์ ์ฌ๋๋ฅผ ๊ฒฐํฉํด ํด์ธ ๋ถํ๋ณ ๊ตญ์ฐ ๋์ฒด ํ๋ณด Top-3๋ฅผ ์ถ์ฒํ๊ณ , ๊ณผ๊ฑฐ ์น์ธ ์ด๋ ฅ ๊ธฐ์ค ์ ํ๋ยท์์๋ณ ๊ธฐ์ฌยท์ค์ถ์ฒ ์ ํยท์ถ์ฒ ์ฌ์ ๋ฌธ์ฅ์ ํ์ธํ๋ค.\"\"\"\n",
"import time\n",
"import numpy as np\n",
"import pandas as pd\n",
"import matplotlib.pyplot as plt\n",
"from sklearn.feature_extraction.text import TfidfVectorizer\n",
"from sklearn.metrics.pairwise import cosine_similarity\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# [๊ต์ก์ฉ๊ฐ์์๋ฃ] ๊ฐ์ ๋ฌด๊ธฐ์ฒด๊ณ ๋ถํยท๊ณต๊ธ๋ง ๋ฐ์ดํฐ ์์ฑ๊ธฐ (์ค์ ์ฒด๊ณยท์
์ฒดยท๊ตญ๊ฐยท๋จ๊ฐ ์ ๋ณด ์์)\n",
"# ๋ถํ 100์ข
(ํด์ธ ์กฐ๋ฌ ์ฝ 80%)์ ๊ณต๊ธ๋ง ์์ฑ๊ณผ, ๊ตญ๋ด ๋ถํ ์นดํ๋ก๊ทธ(๊ณผ๊ฑฐ ์น์ธ๋ ๊ตญ์ฐ ๋์ฒดํ +\n",
"# ์ค๋ช
์ ๋น์ทํ์ง๋ง ๊ท๊ฒฉ์ด ๋ง์ง ์๋ ์ ์ฌ ํ๋ณด)๋ฅผ ํจ๊ป ๋ง๋ ๋ค.\n",
"# ---------------------------------------------------------------------------\n",
"COUNTRIES = [\"๊ฐ์๊ตญ๊ฐA\", \"๊ฐ์๊ตญ๊ฐB\", \"๊ฐ์๊ตญ๊ฐC\", \"๊ตญ๋ด\"]\n",
"CATS = {\n",
" \"์ ์๋ชจ๋\": dict(words=[\"DC-DC\", \"์ปจ๋ฒํฐ\", \"์ ์ฐํ\", \"์ ์\", \"์ ์ถ๋ ฅ\"], syn={\"์ปจ๋ฒํฐ\": \"๋ณํ์ฅ์น\", \"์ ์ฐํ\": \"์์ด์๋ ์ด์
\", \"์ ์ถ๋ ฅ\": \"์์ ํ\"}, std=\"MIL-STD-704\", iface=\"๋จ์๋\"),\n",
" \"์ปค๋ฅํฐ\": dict(words=[\"์ํ\", \"์ปค๋ฅํฐ\", \"๋ฐฉ์ํ\", \"๋คํ\", \"๋์ฌ์ฒด๊ฒฐ\"], syn={\"์ปค๋ฅํฐ\": \"์ ์์\", \"๋ฐฉ์ํ\": \"๋ฐํํ\", \"๋์ฌ์ฒด๊ฒฐ\": \"์คํฌ๋ฃจ๊ฒฐํฉ\"}, std=\"MIL-DTL-38999\", iface=\"์ํํ\"),\n",
" \"์ ์๋ฐธ๋ธ\": dict(words=[\"์ ์\", \"์๋ ๋
ธ์ด๋\", \"๋ฐธ๋ธ\", \"๋ฐฉํฅ์ ์ด\", \"4ํฌํธ\"], syn={\"๋ฐธ๋ธ\": \"๊ฐํ์ฅ์น\", \"๋ฐฉํฅ์ ์ด\": \"์ ๋ก์ ํ\", \"์๋ ๋
ธ์ด๋\": \"์ ์์\"}, std=\"SAE-AS5440\", iface=\"ํ๋์ง\"),\n",
" \"๋ฒ ์ด๋ง\": dict(words=[\"๋ณผ\", \"๋ฒ ์ด๋ง\", \"๊ณ ์\", \"๋ฐ๋ดํ\", \"ํ์ ์ถ\"], syn={\"๋ฒ ์ด๋ง\": \"์ถ๋ฐ์ด\", \"๋ฐ๋ดํ\": \"์ค๋ํ\", \"๊ณ ์\": \"๊ณ ํ์ \"}, std=\"ISO-492\", iface=\"์ถ๊ฒฐํฉ\"),\n",
" \"๊ดํ๋ ์ฆ\": dict(words=[\"์ ์ธ์ \", \"๋ ์ฆ\", \"๊ฒ๋ฅด๋ง๋\", \"๋ฐ์ฌ๋ฐฉ์ง\", \"์ง๊ด\"], syn={\"๋ ์ฆ\": \"๊ดํ์ฐฝ\", \"์ง๊ด\": \"์ด์ ํ์ฑ\", \"๋ฐ์ฌ๋ฐฉ์ง\": \"AR์ฝํ
\"}, std=\"MIL-PRF-13830\", iface=\"๋ง์ดํธ\"),\n",
" \"์์ด๋ก์ผ์\": dict(words=[\"๊ด์ฌ์ \", \"์์ด๋ก\", \"๊ฐ์๋\", \"์ผ์\", \"๊ด์ฑ\"], syn={\"์ผ์\": \"๊ฐ์ง๊ธฐ\", \"๊ฐ์๋\": \"ํ์ ์จ\", \"๊ด์ฑ\": \"INS์ฉ\"}, std=\"IEEE-952\", iface=\"์ง๋ ฌํต์ \"),\n",
" \"RF์ฆํญ๊ธฐ\": dict(words=[\"RF\", \"์ ๋ ฅ์ฆํญ๊ธฐ\", \"๊ด๋์ญ\", \"๋ชจ๋\", \"์ก์ \"], syn={\"์ ๋ ฅ์ฆํญ๊ธฐ\": \"ํ์์ฐํ\", \"์ก์ \": \"์ถ๋ ฅ๋จ\", \"๊ด๋์ญ\": \"์์ด๋๋ฐด๋\"}, std=\"MIL-STD-461\", iface=\"SMA\"),\n",
" \"์ ์ด๋ณด๋\": dict(words=[\"FPGA\", \"์ ์ด๋ณด๋\", \"์๋ฒ ๋๋\", \"์ฐ์ฐ\", \"์
์ถ๋ ฅ\"], syn={\"์ ์ด๋ณด๋\": \"์ปจํธ๋กค์นด๋\", \"์ฐ์ฐ\": \"์ ํธ์ฒ๋ฆฌ\", \"์๋ฒ ๋๋\": \"๋ด์ฅํ\"}, std=\"VITA-46\", iface=\"๋ฐฑํ๋ ์ธ\"),\n",
"}\n",
"V_OPTS, W_OPTS = [12, 24, 28], [25, 50, 100]\n",
"VENDORS = [\"๊ฐ์์ ๋ฐ(์ฃผ)\", \"๊ฐ์์ ์(์ฃผ)\", \"๊ฐ์์ ๊ณต์(์ฃผ)\", \"๊ฐ์๊ดํ(์ฃผ)\", \"๊ฐ์ํ
ํฌ(์ฃผ)\", \"๊ฐ์์์คํ
(์ฃผ)\"]\n",
"\n",
"\n",
"def describe(cat, words, s):\n",
" return f\"{cat} {' '.join(words)} {s['v']}V {s['w']}W {s['tmin']}~{s['tmax']}C {s['std']}\"\n",
"\n",
"\n",
"def _sig(x):\n",
" return 1 / (1 + np.exp(-x))\n",
"\n",
"\n",
"def make_parts(n=100, seed=7):\n",
" \"\"\"๋ถํ ๋ชฉ๋ก(๊ณต๊ธ๋ง ์์ฑ ํฌํจ)๊ณผ ๊ตญ๋ด ์นดํ๋ก๊ทธ๋ฅผ ๋ง๋ ๋ค. ๋ฐํ: (parts DataFrame, catalog DataFrame)\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" cats, rows, cat_rows = list(CATS), [], []\n",
" for i in range(n):\n",
" cat = cats[i % len(cats)]; c = CATS[cat]\n",
" spec = {\"v\": int(rng.choice(V_OPTS)), \"w\": int(rng.choice(W_OPTS)), \"tmin\": int(rng.choice([-55, -40])),\n",
" \"tmax\": int(rng.choice([85, 125])), \"size_mm\": float(round(rng.uniform(20, 120), 1)), \"std\": c[\"std\"]}\n",
" words = [str(x) for x in rng.permutation(c[\"words\"])[:4]]\n",
" country = str(rng.choice(COUNTRIES, p=[0.35, 0.3, 0.15, 0.2]))\n",
" overseas = country != \"๊ตญ๋ด\"\n",
" n_sup = int(rng.choice([1, 2, 3], p=[0.45, 0.35, 0.2])) if overseas else int(rng.choice([1, 2, 3]))\n",
" lead = float(round(rng.gamma(3, 45 if overseas else 25) + 20))\n",
" eol = str(rng.choice([\"์ ์\", \"๋จ์ข
์๊ณ \", \"๋จ์ข
\"], p=[0.7, 0.2, 0.1]))\n",
" crit = str(rng.choice([\"A\", \"B\", \"C\"], p=[0.3, 0.4, 0.3]))\n",
" z = (-2.6 + 1.1 * (n_sup == 1) + 0.9 * overseas + 0.006 * (lead - 100) + 1.2 * (eol == \"๋จ์ข
์๊ณ \")\n",
" + 2.0 * (eol == \"๋จ์ข
\") + rng.normal(0, 0.6))\n",
" p_late = float(np.clip(_sig(z) * 0.8 + 0.05, 0, 0.9))\n",
" late = int(rng.binomial(12, p_late))\n",
" demand = int(rng.integers(20, 400))\n",
" rows.append({\"part_no\": f\"P-{i + 1:04d}\", \"category\": cat, \"desc\": describe(cat, words, spec), **spec,\n",
" \"iface\": c[\"iface\"], \"country\": country, \"overseas\": overseas, \"n_suppliers\": n_sup,\n",
" \"lead_time_days\": lead, \"orders_12m\": 12, \"late_orders\": late,\n",
" \"avg_delay_days\": float(round(late and rng.gamma(2, 12 + 20 * p_late), 1)),\n",
" \"eol_status\": eol, \"ltb_months\": int(rng.integers(1, 25)) if eol == \"๋จ์ข
์๊ณ \" else None,\n",
" \"price_change_pct\": float(round(rng.normal(4 + 12 * p_late, 8), 1)), \"criticality\": crit,\n",
" \"annual_demand\": demand, \"stock_qty\": int(rng.integers(0, demand)),\n",
" \"defect_rate\": float(round(abs(rng.normal(0.8 + 2 * p_late, 0.8)), 2)),\n",
" \"export_license\": bool(overseas and rng.random() < 0.35),\n",
" \"disrupted_next12m\": bool(rng.random() < _sig(z + 0.4))})\n",
" if not overseas:\n",
" continue\n",
" # ๊ณผ๊ฑฐ ์น์ธ๋ ๊ตญ์ฐ ๋์ฒดํ(์ ๋ต): ๋์์ด๋ก ์์ , ๊ท๊ฒฉ ํธํ(์ ๋ ฅ ์ฌ์ ํ์ฉ, ์ผ๋ถ๋ ํฌ๊ธฐ ์ฐจ์ด๊ฐ ์ปค ์ฅ์ฐฉ ๊ฒํ ํ์)\n",
" eq_words = [c[\"syn\"].get(w, w) for w in words]\n",
" hard = rng.random() < 0.15 # ์์ ์ด ๋ง์ด ๋ค๋ฅธ ์ฌ๋ก\n",
" if hard:\n",
" eq_words = [c[\"syn\"].get(w, w) for w in c[\"words\"] if w not in words][:1] + eq_words[1:3]\n",
" eq = {**spec, \"w\": spec[\"w\"] if rng.random() < 0.7 else W_OPTS[min(W_OPTS.index(spec[\"w\"]) + 1, 2)],\n",
" \"size_mm\": float(round(spec[\"size_mm\"] * (rng.uniform(1.11, 1.15) if rng.random() < 0.12 else rng.uniform(0.95, 1.05)), 1))}\n",
" cat_rows.append({\"cand_no\": f\"D-{len(cat_rows) + 1:04d}\", \"category\": cat, \"desc\": describe(cat, eq_words, eq), **eq,\n",
" \"iface\": c[\"iface\"], \"vendor\": str(rng.choice(VENDORS)), \"approved_for\": f\"P-{i + 1:04d}\", \"kind\": \"equivalent\"})\n",
" # ์ ์ฌ ์ค๋ต ํ๋ณด: ์ค๋ช
์ ๊ฑฐ์ ๊ฐ์ง๋ง ์ ์์ด ๋ค๋ฅด๊ฑฐ๋(5V/48V) ์์ฉ ์จ๋ ๋ฑ๊ธ(0~70C)\n",
" for kind in (\"volt_mismatch\", \"temp_mismatch\"):\n",
" d = dict(spec)\n",
" if kind == \"volt_mismatch\":\n",
" d[\"v\"] = int(rng.choice([5, 48])) # ์ฒด๊ณ์์ ์ฐ์ง ์๋ ์ ์\n",
" else:\n",
" d[\"tmin\"], d[\"tmax\"] = 0, 70\n",
" d[\"size_mm\"] = float(round(spec[\"size_mm\"] * rng.uniform(0.95, 1.05), 1))\n",
" cat_rows.append({\"cand_no\": f\"D-{len(cat_rows) + 1:04d}\", \"category\": cat, \"desc\": describe(cat, words, d), **d,\n",
" \"iface\": c[\"iface\"], \"vendor\": str(rng.choice(VENDORS)), \"approved_for\": None, \"kind\": kind})\n",
" parts = pd.DataFrame(rows)\n",
" catalog = pd.DataFrame(cat_rows).sample(frac=1, random_state=seed).reset_index(drop=True)\n",
" return parts, catalog\n",
"# ---------------------------------------------------------------------------\n",
"\n",
"# ---- ๋์ฒด ๋ถํ ์ถ์ฒ ๊ณตํต ํจ์: ํ
์คํธ ์ ์ฌ๋ + ๊ท๊ฒฉ ์ ํฉ์ฑ + ์ง์ ๊ทธ๋ํ ์ด์ ์ ์ฌ๋ ----\n",
"WEIGHTS = {\"text\": 0.35, \"spec\": 0.45, \"graph\": 0.20}\n",
"\n",
"\n",
"def spec_fit(p, c):\n",
" \"\"\"๊ท๊ฒฉ ์ ํฉ์ฑ 0~1: 6๊ฐ ์กฐ๊ฑด ์ถฉ์กฑ ๋น์จ. ์ ์ยท์จ๋ ํ๊ณ(ํ์ ์กฐ๊ฑด)๋ฅผ ๋ชป ๋ง์ถ๋ฉด ์ ์๋ฅผ ์ ๋ฐ์ผ๋ก ๊น๋๋ค.\"\"\"\n",
" conds = [p[\"v\"] == c[\"v\"], c[\"w\"] >= p[\"w\"], c[\"tmin\"] <= p[\"tmin\"], c[\"tmax\"] >= p[\"tmax\"],\n",
" abs(c[\"size_mm\"] - p[\"size_mm\"]) / p[\"size_mm\"] <= 0.10, p[\"iface\"] == c[\"iface\"]]\n",
" s = sum(conds) / len(conds)\n",
" return (s * 0.5 if not (conds[0] and conds[2] and conds[3]) else s), conds\n",
"\n",
"\n",
"def graph_nodes(row):\n",
" \"\"\"์ง์ ๊ทธ๋ํ์์ ๋ถํ ๋
ธ๋์ ์ฐ๊ฒฐ๋ ์ด์ ๋
ธ๋ ์งํฉ(๋ฒ์ฃผยทํ์คยท์ธํฐํ์ด์คยท๊ธฐ๋ฅ์ดยท๋ฑ๊ธ).\"\"\"\n",
" words = set(row[\"desc\"].split()[1:-4])\n",
" grade = \"๊ตฐ์ฉ๋ฑ๊ธ\" if row[\"tmin\"] <= -40 else \"์์ฉ๋ฑ๊ธ\"\n",
" return {f\"cat:{row['category']}\", f\"std:{row['std']}\", f\"if:{row['iface']}\", f\"grade:{grade}\", f\"v:{row['v']}\"} | {f\"w:{w}\" for w in words}\n",
"\n",
"\n",
"def build_graph(catalog):\n",
" \"\"\"๋์์ด ์ฌ์ ์ ๊ทธ๋ํ ๊ฐ์ (๊ธฐ๋ฅ์ด = ๋์์ด)์ผ๋ก ๋ฃ์ด ์์ ์ด ๋ฌ๋ผ๋ ๊ฐ์ ๊ธฐ๋ฅ ๋
ธ๋๋ก ์ฐ๊ฒฐ๋๊ฒ ํ๋ค.\"\"\"\n",
" syn = {f\"w:{b}\": f\"w:{a}\" for c in CATS.values() for a, b in c[\"syn\"].items()}\n",
" canon = lambda nodes: {syn.get(x, x) for x in nodes}\n",
" return {\"canon\": canon, \"nodes\": [canon(graph_nodes(r)) for _, r in catalog.iterrows()]}\n",
"\n",
"\n",
"def graph_sim(g, pn, cn):\n",
" \"\"\"๋ ๋ถํ์ด ๊ณต์ ํ๋ ์ด์ ๋
ธ๋ ๋น์จ(Jaccard).\"\"\"\n",
" pn = g[\"canon\"](pn)\n",
" return len(pn & cn) / max(len(pn | cn), 1)\n",
"\n",
"\n",
"def score_table(parts_df, catalog):\n",
" \"\"\"ํด์ธ ๋ถํ x ๊ฐ์ ๋ฒ์ฃผ ๊ตญ๋ด ํ๋ณด ์๋ง๋ค ํ
์คํธยท๊ท๊ฒฉยท๊ทธ๋ํ ์ ์๋ฅผ ๊ณ์ฐํ ํ(๊ฐ์ค์น ์ ์ฉ ์ ).\"\"\"\n",
" vec = TfidfVectorizer(token_pattern=r\"[^ ]+\").fit(list(parts_df[\"desc\"]) + list(catalog[\"desc\"]))\n",
" text = cosine_similarity(vec.transform(parts_df[\"desc\"]), vec.transform(catalog[\"desc\"]))\n",
" g = build_graph(catalog)\n",
" rows = []\n",
" for i, (_, p) in enumerate(parts_df.iterrows()):\n",
" pn = graph_nodes(p)\n",
" for j, (_, c) in enumerate(catalog.iterrows()):\n",
" if c[\"category\"] != p[\"category\"]:\n",
" continue\n",
" sf, conds = spec_fit(p, c)\n",
" rows.append({\"part_no\": p[\"part_no\"], \"cand_no\": c[\"cand_no\"], \"vendor\": c[\"vendor\"], \"text\": text[i, j],\n",
" \"spec\": sf, \"graph\": graph_sim(g, pn, g[\"nodes\"][j]),\n",
" \"unmet\": \",\".join(n for n, ok in zip([\"์ ์\", \"์ ๋ ฅ\", \"์ ์จ\", \"๊ณ ์จ\", \"ํฌ๊ธฐ\", \"์ธํฐํ์ด์ค\"], conds) if not ok) or \"-\",\n",
" \"kind\": c[\"kind\"], \"correct\": c[\"approved_for\"] == p[\"part_no\"]})\n",
" return pd.DataFrame(rows)\n",
"\n",
"\n",
"def recommend(table, weights=None, top=3):\n",
" \"\"\"๊ฐ์คํฉ ์ ์๋ก ๋ถํ๋ณ ์์ top๊ฐ ํ๋ณด๋ฅผ ๊ณ ๋ฅธ๋ค.\"\"\"\n",
" w = weights or WEIGHTS\n",
" t = table.assign(score=w[\"text\"] * table[\"text\"] + w[\"spec\"] * table[\"spec\"] + w[\"graph\"] * table[\"graph\"])\n",
" return t.sort_values([\"part_no\", \"score\"], ascending=[True, False]).groupby(\"part_no\").head(top)\n",
"\n",
"\n",
"def hit_rate(recs, k=1):\n",
" \"\"\"๊ณผ๊ฑฐ ์น์ธ ์ด๋ ฅ(์ ๋ต ๋์ฒดํ)์ด ์์ k๊ฐ ์์ ๋ค์ด๊ฐ ๋ถํ ๋น์จ.\"\"\"\n",
" return recs.groupby(\"part_no\").apply(lambda d: d[\"correct\"].head(k).any(), include_groups=False).mean()\n",
"\n",
"parts, catalog = make_parts(100, seed=7)\n",
"over = parts[parts[\"overseas\"]].reset_index(drop=True)\n",
"print(f\"[๋ฐ์ดํฐ] ํด์ธ ์กฐ๋ฌ ๋ถํ {len(over)}์ข
, ๊ตญ๋ด ํ๋ณด ์นดํ๋ก๊ทธ {len(catalog)}๊ฑด(์น์ธ ์ด๋ ฅ ์๋ ๋์ฒดํ {int((catalog['kind'] == 'equivalent').sum())}๊ฑด, ๊ท๊ฒฉ ๋ถ์ผ์น ์ ์ฌ ํ๋ณด {int((catalog['kind'] != 'equivalent').sum())}๊ฑด)\")\n",
"\n",
"t0 = time.perf_counter()\n",
"table = score_table(over, catalog)\n",
"rec = recommend(table)\n",
"ms = (time.perf_counter() - t0) * 1000 / len(over)\n",
"print(f\"์ถ์ฒ ์ฒ๋ฆฌ: ๋ถํ 1์ข
๋น {ms:.1f}ms (ํ๋ณด ์ {len(table)}๊ฐ ์ ์ ๊ณ์ฐ ํฌํจ)\")\n",
"\n",
"# 1) ์์๋ณ ๊ธฐ์ฌ: ์ด๋ ์ ํธ๋ฅผ ์ฐ๋๋์ ๋ฐ๋ผ ์ ํ๋๊ฐ ์ด๋ป๊ฒ ๋ฌ๋ผ์ง๋๊ฐ\n",
"combos = {\"ํ
์คํธ ์ ์ฌ๋๋ง\": {\"text\": 1, \"spec\": 0, \"graph\": 0}, \"๊ท๊ฒฉ ์ ํฉ์ฑ๋ง\": {\"text\": 0, \"spec\": 1, \"graph\": 0},\n",
" \"๊ทธ๋ํ๋ง\": {\"text\": 0, \"spec\": 0, \"graph\": 1}, \"ํ
์คํธ+๊ทธ๋ํ\": {\"text\": .5, \"spec\": 0, \"graph\": .5},\n",
" \"ํ
์คํธ+๊ท๊ฒฉ\": {\"text\": .4, \"spec\": .6, \"graph\": 0}, \"์ธ ์ ํธ ๊ฒฐํฉ(์ด์ฉ)\": WEIGHTS}\n",
"rows = []\n",
"for name, w in combos.items():\n",
" r = recommend(table, w)\n",
" bad = (r.groupby(\"part_no\").head(1)[\"kind\"] != \"equivalent\").mean()\n",
" rows.append({\"๋ฐฉ์\": name, \"Top-1 ์ ํ๋\": hit_rate(r, 1), \"Top-3 ์ ํ๋\": hit_rate(r, 3), \"1์์๊ฐ ๊ท๊ฒฉ๋ถ์ผ์น ํ๋ณด์ธ ๋น์จ\": bad})\n",
"ab = pd.DataFrame(rows)\n",
"print(\"\\n[์์๋ณ ๊ธฐ์ฌ(๊ณผ๊ฑฐ ์น์ธ ๋์ฒดํ์ ์์์ ์ฌ๋ ธ๋๊ฐ)]\")\n",
"print(ab.round(3).to_string(index=False))\n",
"acc1, acc3 = hit_rate(rec, 1), hit_rate(rec, 3)\n",
"print(f\"\\n์ด์ฉ ๋ฐฉ์ Top-1 ์ ํ๋ {acc1:.1%}, Top-3 ์ ํ๋ {acc3:.1%} (๋ชฉํ 85% ์ด์: {'๋ฌ์ฑ' if acc1 >= 0.85 else '๋ฏธ๋ฌ'})\")\n",
"\n",
"# 2) ์ค์ถ์ฒ ๋ถ์: 1์์๊ฐ ์ ๋ต์ด ์๋ ๋ถํ\n",
"top1 = rec.groupby(\"part_no\").head(1)\n",
"wrong = top1[~top1[\"correct\"]]\n",
"print(f\"\\n[1์์ ์ค์ถ์ฒ {len(wrong)}๊ฑด ๋ถ์]\")\n",
"for _, w in wrong.iterrows():\n",
" right = table[(table[\"part_no\"] == w[\"part_no\"]) & table[\"correct\"]].iloc[0]\n",
" if w[\"kind\"] != \"equivalent\":\n",
" why = f\"๊ท๊ฒฉ ๋ถ์ผ์น ํ๋ณด๊ฐ ์์(๋ฏธ์ถฉ์กฑ {w['unmet']}) -> ๊ท๊ฒฉ ๊ฐ์ค์น ๋ณด๊ฐ ํ์\"\n",
" elif w[\"unmet\"] == \"-\":\n",
" why = \"๋ค๋ฅธ ๋ถํ์ฉ์ผ๋ก ์น์ธ๋ ๊ฐ์ ๋ฒ์ฃผ ๋์ฒดํ์ด ๊ท๊ฒฉ๋ ์ถฉ์กฑ -> ์น์ธ ์ด๋ ฅ์ ์ค๋ต, ์ค๋ฌด์ ๊ฒํ ํ๋ณด\"\n",
" else:\n",
" why = f\"๋ค๋ฅธ ๋ถํ์ฉ ์น์ธ ๋์ฒดํ์ด ์์(์ ๋ต ํ๋ณด๋ ํฌ๊ธฐ ์ฐจ์ด๋ก ๊ฐ์ ) -> ํฌ๊ธฐ ํ์ฉ ์ค์ฐจ ์ฌ๊ฒํ \"\n",
" print(f\" {w['part_no']}: ์ถ์ฒ {w['cand_no']}(์ ์ {w['score']:.2f}, {w['kind']}) / ์ ๋ต {right['cand_no']}(๊ท๊ฒฉ ๋ฏธ์ถฉ์กฑ {right['unmet']}) -> {why}\")\n",
"\n",
"# 3) ์ถ์ฒ ์ฌ์ ๋ฌธ์ฅ(์ค๋ฌด ์์ฉ์ฑ: ๊ทผ๊ฑฐ๋ฅผ ์ฌ๋์ด ์ฝ์ ์ ์๊ฒ)\n",
"print(\"\\n[์ถ์ฒ ์ฌ์ ์์]\")\n",
"for pn in over[\"part_no\"].head(3):\n",
" r = rec[rec[\"part_no\"] == pn].iloc[0]\n",
" p = parts[parts[\"part_no\"] == pn].iloc[0]\n",
" ok = \"๊ท๊ฒฉ 6๊ฐ ํญ๋ชฉ ๋ชจ๋ ์ถฉ์กฑ\" if r[\"unmet\"] == \"-\" else f\"๊ท๊ฒฉ ๋ฏธ์ถฉ์กฑ ํญ๋ชฉ {r['unmet']}(๊ธฐ์ ๊ฒํ ํ์)\"\n",
" print(f\" {pn} {p['category']} -> {r['cand_no']}({r['vendor']}): ์ค๋ช
์ ์ฌ๋ {r['text']:.2f}, ๊ท๊ฒฉ ์ ํฉ์ฑ {r['spec']:.2f}, ๊ทธ๋ํ ์ฐ๊ฒฐ {r['graph']:.2f} / {ok}\")\n",
"print(\"โป ์ถ์ฒ์ ํ๋ณด ์ ์์ด๋ฉฐ ๊ท๊ฒฉ ์ ํฉ์ฑ ์ต์ข
ํ๋จ๊ณผ ๊ณ์ฝ ์๋ขฐ๋ ๊ธฐ์ ๊ฒํ ๊ดยท๊ณ์ฝ ๋ด๋น์๊ฐ ๊ฒฐ์ ํ๋ค.\")\n",
"\n",
"fig, ax = plt.subplots(1, 2, figsize=(11, 4))\n",
"x = np.arange(len(ab)); ax[0].bar(x - .2, ab[\"Top-1 ์ ํ๋\"], .4, label=\"top-1\"); ax[0].bar(x + .2, ab[\"Top-3 ์ ํ๋\"], .4, label=\"top-3\")\n",
"ax[0].set_xticks(x, [\"text\", \"spec\", \"graph\", \"text+graph\", \"text+spec\", \"all 3\"], rotation=20); ax[0].axhline(.85, color=\"r\", ls=\"--\"); ax[0].legend(); ax[0].set_title(\"Accuracy by signal combination\")\n",
"allsc = recommend(table, WEIGHTS, 999)\n",
"ax[1].hist([allsc[allsc[\"correct\"]][\"score\"], allsc[~allsc[\"correct\"]][\"score\"]], bins=15, label=[\"approved\", \"other\"], stacked=True)\n",
"ax[1].legend(); ax[1].set_title(\"Score distribution (candidates)\"); ax[1].set_xlabel(\"weighted score\")\n",
"plt.tight_layout(); plt.show()\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"๐ก [์ฐธ๊ณ ] ๋ฏธ์
04 ์์ ์คํ ๊ฒฐ๊ณผ (Output)
\n",
"\n",
"```text\n",
"[๋ฐ์ดํฐ] ํด์ธ ์กฐ๋ฌ ๋ถํ 85์ข
, ๊ตญ๋ด ํ๋ณด ์นดํ๋ก๊ทธ 255๊ฑด(์น์ธ ์ด๋ ฅ ์๋ ๋์ฒดํ 85๊ฑด, ๊ท๊ฒฉ ๋ถ์ผ์น ์ ์ฌ ํ๋ณด 170๊ฑด)\n",
"์ถ์ฒ ์ฒ๋ฆฌ: ๋ถํ 1์ข
๋น 14.0ms (ํ๋ณด ์ 2745๊ฐ ์ ์ ๊ณ์ฐ ํฌํจ)\n",
"\n",
"[์์๋ณ ๊ธฐ์ฌ(๊ณผ๊ฑฐ ์น์ธ ๋์ฒดํ์ ์์์ ์ฌ๋ ธ๋๊ฐ)]\n",
" ๋ฐฉ์ Top-1 ์ ํ๋ Top-3 ์ ํ๋ 1์์๊ฐ ๊ท๊ฒฉ๋ถ์ผ์น ํ๋ณด์ธ ๋น์จ\n",
" ํ
์คํธ ์ ์ฌ๋๋ง 0.000 0.000 1.000\n",
" ๊ท๊ฒฉ ์ ํฉ์ฑ๋ง 0.812 1.000 0.000\n",
" ๊ทธ๋ํ๋ง 0.553 0.741 0.047\n",
" ํ
์คํธ+๊ทธ๋ํ 0.000 0.082 1.000\n",
" ํ
์คํธ+๊ท๊ฒฉ 0.871 0.988 0.000\n",
"์ธ ์ ํธ ๊ฒฐํฉ(์ด์ฉ) 0.894 0.976 0.012\n",
"\n",
"์ด์ฉ ๋ฐฉ์ Top-1 ์ ํ๋ 89.4%, Top-3 ์ ํ๋ 97.6% (๋ชฉํ 85% ์ด์: ๋ฌ์ฑ)\n",
"\n",
"[1์์ ์ค์ถ์ฒ 9๊ฑด ๋ถ์]\n",
" P-0009: ์ถ์ฒ D-0187(์ ์ 0.77, equivalent) / ์ ๋ต D-0016(๊ท๊ฒฉ ๋ฏธ์ถฉ์กฑ -) -> ๋ค๋ฅธ ๋ถํ์ฉ์ผ๋ก ์น์ธ๋ ๊ฐ์ ๋ฒ์ฃผ ๋์ฒดํ์ด ๊ท๊ฒฉ๋ ์ถฉ์กฑ -> ์น์ธ ์ด๋ ฅ์ ์ค๋ต, ์ค๋ฌด์ ๊ฒํ ํ๋ณด\n",
" P-0028: ์ถ์ฒ D-0046(์ ์ 0.76, equivalent) / ์ ๋ต D-0070(๊ท๊ฒฉ ๋ฏธ์ถฉ์กฑ ํฌ๊ธฐ) -> ๋ค๋ฅธ ๋ถํ์ฉ์ผ๋ก ์น์ธ๋ ๊ฐ์ ๋ฒ์ฃผ ๋์ฒดํ์ด ๊ท๊ฒฉ๋ ์ถฉ์กฑ -> ์น์ธ ์ด๋ ฅ์ ์ค๋ต, ์ค๋ฌด์ ๊ฒํ ํ๋ณด\n",
" P-0032: ์ถ์ฒ D-0034(์ ์ 0.82, equivalent) / ์ ๋ต D-0082(๊ท๊ฒฉ ๋ฏธ์ถฉ์กฑ ํฌ๊ธฐ) -> ๋ค๋ฅธ ๋ถํ์ฉ์ผ๋ก ์น์ธ๋ ๊ฐ์ ๋ฒ์ฃผ ๋์ฒดํ์ด ๊ท๊ฒฉ๋ ์ถฉ์กฑ -> ์น์ธ ์ด๋ ฅ์ ์ค๋ต, ์ค๋ฌด์ ๊ฒํ ํ๋ณด\n",
" P-0046: ์ถ์ฒ D-0119(์ ์ 0.67, volt_mismatch) / ์ ๋ต D-0118(๊ท๊ฒฉ ๋ฏธ์ถฉ์กฑ ํฌ๊ธฐ) -> ๊ท๊ฒฉ ๋ถ์ผ์น ํ๋ณด๊ฐ ์์(๋ฏธ์ถฉ์กฑ ์ ์) -> ๊ท๊ฒฉ ๊ฐ์ค์น ๋ณด๊ฐ ํ์\n",
" P-0049: ์ถ์ฒ D-0016(์ ์ 0.82, equivalent) / ์ ๋ต D-0124(๊ท๊ฒฉ ๋ฏธ์ถฉ์กฑ -) -> ๋ค๋ฅธ ๋ถํ์ฉ์ผ๋ก ์น์ธ๋ ๊ฐ์ ๋ฒ์ฃผ ๋์ฒดํ์ด ๊ท๊ฒฉ๋ ์ถฉ์กฑ -> ์น์ธ ์ด๋ ฅ์ ์ค๋ต, ์ค๋ฌด์ ๊ฒํ ํ๋ณด\n",
" P-0056: ์ถ์ฒ D-0058(์ ์ 0.84, equivalent) / ์ ๋ต D-0145(๊ท๊ฒฉ ๋ฏธ์ถฉ์กฑ -) -> ๋ค๋ฅธ ๋ถํ์ฉ์ผ๋ก ์น์ธ๋ ๊ฐ์ ๋ฒ์ฃผ ๋์ฒดํ์ด ๊ท๊ฒฉ๋ ์ถฉ์กฑ -> ์น์ธ ์ด๋ ฅ์ ์ค๋ต, ์ค๋ฌด์ ๊ฒํ ํ๋ณด\n",
" P-0060: ์ถ์ฒ D-0235(์ ์ 0.81, equivalent) / ์ ๋ต D-0154(๊ท๊ฒฉ ๋ฏธ์ถฉ์กฑ -) -> ๋ค๋ฅธ ๋ถํ์ฉ์ผ๋ก ์น์ธ๋ ๊ฐ์ ๋ฒ์ฃผ ๋์ฒดํ์ด ๊ท๊ฒฉ๋ ์ถฉ์กฑ -> ์น์ธ ์ด๋ ฅ์ ์ค๋ต, ์ค๋ฌด์ ๊ฒํ ํ๋ณด\n",
" P-0063: ์ถ์ฒ D-0181(์ ์ 0.76, equivalent) / ์ ๋ต D-0163(๊ท๊ฒฉ ๋ฏธ์ถฉ์กฑ -) -> ๋ค๋ฅธ ๋ถํ์ฉ์ผ๋ก ์น์ธ๋ ๊ฐ์ ๋ฒ์ฃผ ๋์ฒดํ์ด ๊ท๊ฒฉ๋ ์ถฉ์กฑ -> ์น์ธ ์ด๋ ฅ์ ์ค๋ต, ์ค๋ฌด์ ๊ฒํ ํ๋ณด\n",
" P-0077: ์ถ์ฒ D-0049(์ ์ 0.76, equivalent) / ์ ๋ต D-0196(๊ท๊ฒฉ ๋ฏธ์ถฉ์กฑ ํฌ๊ธฐ) -> ๋ค๋ฅธ ๋ถํ์ฉ ์น์ธ ๋์ฒดํ์ด ์์(์ ๋ต ํ๋ณด๋ ํฌ๊ธฐ ์ฐจ์ด๋ก ๊ฐ์ ) -> ํฌ๊ธฐ ํ์ฉ ์ค์ฐจ ์ฌ๊ฒํ \n",
"\n",
"[์ถ์ฒ ์ฌ์ ์์]\n",
" P-0002 ์ปค๋ฅํฐ -> D-0001(๊ฐ์์ ๋ฐ(์ฃผ)): ์ค๋ช
์ ์ฌ๋ 0.64, ๊ท๊ฒฉ ์ ํฉ์ฑ 1.00, ๊ทธ๋ํ ์ฐ๊ฒฐ 1.00 / ๊ท๊ฒฉ 6๊ฐ ํญ๋ชฉ ๋ชจ๋ ์ถฉ์กฑ\n",
" P-0003 ์ ์๋ฐธ๋ธ -> D-0004(๊ฐ์์ ๋ฐ(์ฃผ)): ์ค๋ช
์ ์ฌ๋ 0.47, ๊ท๊ฒฉ ์ ํฉ์ฑ 1.00, ๊ทธ๋ํ ์ฐ๊ฒฐ 1.00 / ๊ท๊ฒฉ 6๊ฐ ํญ๋ชฉ ๋ชจ๋ ์ถฉ์กฑ\n",
" P-0004 ๋ฒ ์ด๋ง -> D-0007(๊ฐ์์ ๋ฐ(์ฃผ)): ์ค๋ช
์ ์ฌ๋ 0.54, ๊ท๊ฒฉ ์ ํฉ์ฑ 1.00, ๊ทธ๋ํ ์ฐ๊ฒฐ 1.00 / ๊ท๊ฒฉ 6๊ฐ ํญ๋ชฉ ๋ชจ๋ ์ถฉ์กฑ\n",
"โป ์ถ์ฒ์ ํ๋ณด ์ ์์ด๋ฉฐ ๊ท๊ฒฉ ์ ํฉ์ฑ ์ต์ข
ํ๋จ๊ณผ ๊ณ์ฝ ์๋ขฐ๋ ๊ธฐ์ ๊ฒํ ๊ดยท๊ณ์ฝ ๋ด๋น์๊ฐ ๊ฒฐ์ ํ๋ค.\n",
"```\n",
" \n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## ๐ฏ ๋ฏธ์
05. ๊ณต๊ธ๋ง ๋ถ๊ดด ์ํ๋(Supply Chain Risk Score) ๋ฐ ์กฐ๊ธฐ๊ฒฝ๋ณด ์ฐ์ถ ์์ง ๊ฐ๋ฐ\n",
"**์์์๊ฐ**: ์ฝ 3.0์๊ฐ\n",
"\n",
"### ๐ ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ์ํ ์ง์นจ\n",
"#### ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ๋น์์ฑ\n",
"\n",
"๊ณต๊ธ๋ง ๋ถ๊ดด ์ํ๋ ๋ฐ ์กฐ๊ธฐ๊ฒฝ๋ณด ์ฐ์ถ ์์ง์ ๊ฐ๋ฐํ๋ค. ์ค์ ์ ์ ํ์ฅ์์๋ ๋จ์ ํ์ง์ ๊ทธ์น์ง ์๊ณ ์ ์ํ ์ํ ํ๋จ๊ณผ ์ ๋ขฐ๋ ๋์ ์์ฌ๊ฒฐ์ ์ด ํ์์ ์ด๋ฏ๋ก, **๊ณต๊ธ๋ง ๋ถ๊ดด ์ํ๋(Supply Chain Risk Score)์ ์๋ณ ์ง์ฐ ์ถ์ด ๊ธฐ๋ฐ ์กฐ๊ธฐ๊ฒฝ๋ณด๋ฅผ ์ฐ์ถํ๋ ์์ง์ ๋ง๋ค๊ณ ์
๋ ฅ-๋ถ์-๊ฒํ -๊ฒฐ๊ณผ 4๊ฐ ํ๋ฉด์ผ๋ก ๊ตฌ์ฑํ๋ค.** ๊ณผ์
์ ์์ํ์ฌ ์คํ์ ๋ฐฉ์งํ๊ณ ์์ ๋์๋ ฅ์ ๊ทน๋ํํด์ผ ํฉ๋๋ค.\n",
"\n",
"์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ณธ ๋ฏธ์
์์๋ **๊ณต๊ธ์ ์, ํด์ธ ์กฐ๋ฌ, ๋ฆฌ๋ํ์, ๋ฉ๊ธฐ ์ง์ฐ๋ฅ , ๋จ์ข
์ํ ๋ฑ 12๊ฐ ์งํ๋ก ๋ก์ง์คํฑ ํ๊ท ์ํ๋(0~100์ )๋ฅผ ํ์ตํ๊ณ ๋ณ๋ ๊ฐ์ ๋ถํ๊ตฐ์์ AUC์ ๋ฑ๊ธ๋ณ ์ค์ ๋จ์ ๋ฅ ํ์ธํ๊ณ ์ ํจ์ฑ์ ์ ๊ฒํ๋** ์ค์ต์ ์ฒด๊ณ์ ์ผ๋ก ์ํํฉ๋๋ค.\n",
"\n",
"ํ์ต์๋ ์ค์ต๊ฐ์ด๋์ ์ ์๋ ์ด 4๊ฐ ์ธ๋ถ ํ๋(๊ธฐ๋ณธ ๊ธฐ๋ฅ ๊ตฌํ โ ๊ฒฐ๊ณผ ์งํ ๊ฒ์ฆ โ ํ๋ผ๋ฏธํฐ ํ๋ ๋ฐ ๋์ ๊ณผ์ )์ ์์ฐจ์ ์ผ๋ก ์งํํ๋ฉฐ ํต์ฌ ํ์ ๊ธฐ์ค์ ๋ฌ์ฑํฉ๋๋ค.\n",
"\n",
"**์ด๋ฒ ๊ฒฐ๊ณผ๋ฌผ:** `src/ui/prc07_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": [
"\"\"\"12๊ฐ ๊ณต๊ธ๋ง ์งํ๋ก ๋ถํ๋ณ ๊ณต๊ธ๋ง ๋ถ๊ดด ์ํ๋(0~100์ )๋ฅผ ๋ก์ง์คํฑ ํ๊ท๋ก ์ฐ์ถํ๊ณ ์๋ณ ๋ฉ๊ธฐ ์ง์ฐ ์ถ์ด์ EWMA๋ก ์กฐ๊ธฐ๊ฒฝ๋ณด๋ฅผ ๋ด๋ ์์ง์ ๋ง๋ ๋ค, ์
๋ ฅ-๋ถ์-๊ฒํ -๊ฒฐ๊ณผ 4๊ฐ ํ๋ฉด(ํ
์คํธ ํจ๋๊ณผ ๊ทธ๋ํ)์ ๊ตฌ์ฑํด ๋ณ๋ ๊ฐ์ ๋ถํ๊ตฐ์์ ์ฑ๋ฅ์ ๊ฒ์ฆํ๋ค.\"\"\"\n",
"import numpy as np\n",
"import pandas as pd\n",
"import matplotlib.pyplot as plt\n",
"from sklearn.linear_model import LogisticRegression\n",
"from sklearn.pipeline import make_pipeline\n",
"from sklearn.preprocessing import StandardScaler\n",
"from sklearn.metrics import roc_auc_score, brier_score_loss\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# [๊ต์ก์ฉ๊ฐ์์๋ฃ] ๊ฐ์ ๋ฌด๊ธฐ์ฒด๊ณ ๋ถํยท๊ณต๊ธ๋ง ๋ฐ์ดํฐ ์์ฑ๊ธฐ (์ค์ ์ฒด๊ณยท์
์ฒดยท๊ตญ๊ฐยท๋จ๊ฐ ์ ๋ณด ์์)\n",
"# ๋ถํ 100์ข
(ํด์ธ ์กฐ๋ฌ ์ฝ 80%)์ ๊ณต๊ธ๋ง ์์ฑ๊ณผ, ๊ตญ๋ด ๋ถํ ์นดํ๋ก๊ทธ(๊ณผ๊ฑฐ ์น์ธ๋ ๊ตญ์ฐ ๋์ฒดํ +\n",
"# ์ค๋ช
์ ๋น์ทํ์ง๋ง ๊ท๊ฒฉ์ด ๋ง์ง ์๋ ์ ์ฌ ํ๋ณด)๋ฅผ ํจ๊ป ๋ง๋ ๋ค.\n",
"# ---------------------------------------------------------------------------\n",
"COUNTRIES = [\"๊ฐ์๊ตญ๊ฐA\", \"๊ฐ์๊ตญ๊ฐB\", \"๊ฐ์๊ตญ๊ฐC\", \"๊ตญ๋ด\"]\n",
"CATS = {\n",
" \"์ ์๋ชจ๋\": dict(words=[\"DC-DC\", \"์ปจ๋ฒํฐ\", \"์ ์ฐํ\", \"์ ์\", \"์ ์ถ๋ ฅ\"], syn={\"์ปจ๋ฒํฐ\": \"๋ณํ์ฅ์น\", \"์ ์ฐํ\": \"์์ด์๋ ์ด์
\", \"์ ์ถ๋ ฅ\": \"์์ ํ\"}, std=\"MIL-STD-704\", iface=\"๋จ์๋\"),\n",
" \"์ปค๋ฅํฐ\": dict(words=[\"์ํ\", \"์ปค๋ฅํฐ\", \"๋ฐฉ์ํ\", \"๋คํ\", \"๋์ฌ์ฒด๊ฒฐ\"], syn={\"์ปค๋ฅํฐ\": \"์ ์์\", \"๋ฐฉ์ํ\": \"๋ฐํํ\", \"๋์ฌ์ฒด๊ฒฐ\": \"์คํฌ๋ฃจ๊ฒฐํฉ\"}, std=\"MIL-DTL-38999\", iface=\"์ํํ\"),\n",
" \"์ ์๋ฐธ๋ธ\": dict(words=[\"์ ์\", \"์๋ ๋
ธ์ด๋\", \"๋ฐธ๋ธ\", \"๋ฐฉํฅ์ ์ด\", \"4ํฌํธ\"], syn={\"๋ฐธ๋ธ\": \"๊ฐํ์ฅ์น\", \"๋ฐฉํฅ์ ์ด\": \"์ ๋ก์ ํ\", \"์๋ ๋
ธ์ด๋\": \"์ ์์\"}, std=\"SAE-AS5440\", iface=\"ํ๋์ง\"),\n",
" \"๋ฒ ์ด๋ง\": dict(words=[\"๋ณผ\", \"๋ฒ ์ด๋ง\", \"๊ณ ์\", \"๋ฐ๋ดํ\", \"ํ์ ์ถ\"], syn={\"๋ฒ ์ด๋ง\": \"์ถ๋ฐ์ด\", \"๋ฐ๋ดํ\": \"์ค๋ํ\", \"๊ณ ์\": \"๊ณ ํ์ \"}, std=\"ISO-492\", iface=\"์ถ๊ฒฐํฉ\"),\n",
" \"๊ดํ๋ ์ฆ\": dict(words=[\"์ ์ธ์ \", \"๋ ์ฆ\", \"๊ฒ๋ฅด๋ง๋\", \"๋ฐ์ฌ๋ฐฉ์ง\", \"์ง๊ด\"], syn={\"๋ ์ฆ\": \"๊ดํ์ฐฝ\", \"์ง๊ด\": \"์ด์ ํ์ฑ\", \"๋ฐ์ฌ๋ฐฉ์ง\": \"AR์ฝํ
\"}, std=\"MIL-PRF-13830\", iface=\"๋ง์ดํธ\"),\n",
" \"์์ด๋ก์ผ์\": dict(words=[\"๊ด์ฌ์ \", \"์์ด๋ก\", \"๊ฐ์๋\", \"์ผ์\", \"๊ด์ฑ\"], syn={\"์ผ์\": \"๊ฐ์ง๊ธฐ\", \"๊ฐ์๋\": \"ํ์ ์จ\", \"๊ด์ฑ\": \"INS์ฉ\"}, std=\"IEEE-952\", iface=\"์ง๋ ฌํต์ \"),\n",
" \"RF์ฆํญ๊ธฐ\": dict(words=[\"RF\", \"์ ๋ ฅ์ฆํญ๊ธฐ\", \"๊ด๋์ญ\", \"๋ชจ๋\", \"์ก์ \"], syn={\"์ ๋ ฅ์ฆํญ๊ธฐ\": \"ํ์์ฐํ\", \"์ก์ \": \"์ถ๋ ฅ๋จ\", \"๊ด๋์ญ\": \"์์ด๋๋ฐด๋\"}, std=\"MIL-STD-461\", iface=\"SMA\"),\n",
" \"์ ์ด๋ณด๋\": dict(words=[\"FPGA\", \"์ ์ด๋ณด๋\", \"์๋ฒ ๋๋\", \"์ฐ์ฐ\", \"์
์ถ๋ ฅ\"], syn={\"์ ์ด๋ณด๋\": \"์ปจํธ๋กค์นด๋\", \"์ฐ์ฐ\": \"์ ํธ์ฒ๋ฆฌ\", \"์๋ฒ ๋๋\": \"๋ด์ฅํ\"}, std=\"VITA-46\", iface=\"๋ฐฑํ๋ ์ธ\"),\n",
"}\n",
"V_OPTS, W_OPTS = [12, 24, 28], [25, 50, 100]\n",
"VENDORS = [\"๊ฐ์์ ๋ฐ(์ฃผ)\", \"๊ฐ์์ ์(์ฃผ)\", \"๊ฐ์์ ๊ณต์(์ฃผ)\", \"๊ฐ์๊ดํ(์ฃผ)\", \"๊ฐ์ํ
ํฌ(์ฃผ)\", \"๊ฐ์์์คํ
(์ฃผ)\"]\n",
"\n",
"\n",
"def describe(cat, words, s):\n",
" return f\"{cat} {' '.join(words)} {s['v']}V {s['w']}W {s['tmin']}~{s['tmax']}C {s['std']}\"\n",
"\n",
"\n",
"def _sig(x):\n",
" return 1 / (1 + np.exp(-x))\n",
"\n",
"\n",
"def make_parts(n=100, seed=7):\n",
" \"\"\"๋ถํ ๋ชฉ๋ก(๊ณต๊ธ๋ง ์์ฑ ํฌํจ)๊ณผ ๊ตญ๋ด ์นดํ๋ก๊ทธ๋ฅผ ๋ง๋ ๋ค. ๋ฐํ: (parts DataFrame, catalog DataFrame)\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" cats, rows, cat_rows = list(CATS), [], []\n",
" for i in range(n):\n",
" cat = cats[i % len(cats)]; c = CATS[cat]\n",
" spec = {\"v\": int(rng.choice(V_OPTS)), \"w\": int(rng.choice(W_OPTS)), \"tmin\": int(rng.choice([-55, -40])),\n",
" \"tmax\": int(rng.choice([85, 125])), \"size_mm\": float(round(rng.uniform(20, 120), 1)), \"std\": c[\"std\"]}\n",
" words = [str(x) for x in rng.permutation(c[\"words\"])[:4]]\n",
" country = str(rng.choice(COUNTRIES, p=[0.35, 0.3, 0.15, 0.2]))\n",
" overseas = country != \"๊ตญ๋ด\"\n",
" n_sup = int(rng.choice([1, 2, 3], p=[0.45, 0.35, 0.2])) if overseas else int(rng.choice([1, 2, 3]))\n",
" lead = float(round(rng.gamma(3, 45 if overseas else 25) + 20))\n",
" eol = str(rng.choice([\"์ ์\", \"๋จ์ข
์๊ณ \", \"๋จ์ข
\"], p=[0.7, 0.2, 0.1]))\n",
" crit = str(rng.choice([\"A\", \"B\", \"C\"], p=[0.3, 0.4, 0.3]))\n",
" z = (-2.6 + 1.1 * (n_sup == 1) + 0.9 * overseas + 0.006 * (lead - 100) + 1.2 * (eol == \"๋จ์ข
์๊ณ \")\n",
" + 2.0 * (eol == \"๋จ์ข
\") + rng.normal(0, 0.6))\n",
" p_late = float(np.clip(_sig(z) * 0.8 + 0.05, 0, 0.9))\n",
" late = int(rng.binomial(12, p_late))\n",
" demand = int(rng.integers(20, 400))\n",
" rows.append({\"part_no\": f\"P-{i + 1:04d}\", \"category\": cat, \"desc\": describe(cat, words, spec), **spec,\n",
" \"iface\": c[\"iface\"], \"country\": country, \"overseas\": overseas, \"n_suppliers\": n_sup,\n",
" \"lead_time_days\": lead, \"orders_12m\": 12, \"late_orders\": late,\n",
" \"avg_delay_days\": float(round(late and rng.gamma(2, 12 + 20 * p_late), 1)),\n",
" \"eol_status\": eol, \"ltb_months\": int(rng.integers(1, 25)) if eol == \"๋จ์ข
์๊ณ \" else None,\n",
" \"price_change_pct\": float(round(rng.normal(4 + 12 * p_late, 8), 1)), \"criticality\": crit,\n",
" \"annual_demand\": demand, \"stock_qty\": int(rng.integers(0, demand)),\n",
" \"defect_rate\": float(round(abs(rng.normal(0.8 + 2 * p_late, 0.8)), 2)),\n",
" \"export_license\": bool(overseas and rng.random() < 0.35),\n",
" \"disrupted_next12m\": bool(rng.random() < _sig(z + 0.4))})\n",
" if not overseas:\n",
" continue\n",
" # ๊ณผ๊ฑฐ ์น์ธ๋ ๊ตญ์ฐ ๋์ฒดํ(์ ๋ต): ๋์์ด๋ก ์์ , ๊ท๊ฒฉ ํธํ(์ ๋ ฅ ์ฌ์ ํ์ฉ, ์ผ๋ถ๋ ํฌ๊ธฐ ์ฐจ์ด๊ฐ ์ปค ์ฅ์ฐฉ ๊ฒํ ํ์)\n",
" eq_words = [c[\"syn\"].get(w, w) for w in words]\n",
" hard = rng.random() < 0.15 # ์์ ์ด ๋ง์ด ๋ค๋ฅธ ์ฌ๋ก\n",
" if hard:\n",
" eq_words = [c[\"syn\"].get(w, w) for w in c[\"words\"] if w not in words][:1] + eq_words[1:3]\n",
" eq = {**spec, \"w\": spec[\"w\"] if rng.random() < 0.7 else W_OPTS[min(W_OPTS.index(spec[\"w\"]) + 1, 2)],\n",
" \"size_mm\": float(round(spec[\"size_mm\"] * (rng.uniform(1.11, 1.15) if rng.random() < 0.12 else rng.uniform(0.95, 1.05)), 1))}\n",
" cat_rows.append({\"cand_no\": f\"D-{len(cat_rows) + 1:04d}\", \"category\": cat, \"desc\": describe(cat, eq_words, eq), **eq,\n",
" \"iface\": c[\"iface\"], \"vendor\": str(rng.choice(VENDORS)), \"approved_for\": f\"P-{i + 1:04d}\", \"kind\": \"equivalent\"})\n",
" # ์ ์ฌ ์ค๋ต ํ๋ณด: ์ค๋ช
์ ๊ฑฐ์ ๊ฐ์ง๋ง ์ ์์ด ๋ค๋ฅด๊ฑฐ๋(5V/48V) ์์ฉ ์จ๋ ๋ฑ๊ธ(0~70C)\n",
" for kind in (\"volt_mismatch\", \"temp_mismatch\"):\n",
" d = dict(spec)\n",
" if kind == \"volt_mismatch\":\n",
" d[\"v\"] = int(rng.choice([5, 48])) # ์ฒด๊ณ์์ ์ฐ์ง ์๋ ์ ์\n",
" else:\n",
" d[\"tmin\"], d[\"tmax\"] = 0, 70\n",
" d[\"size_mm\"] = float(round(spec[\"size_mm\"] * rng.uniform(0.95, 1.05), 1))\n",
" cat_rows.append({\"cand_no\": f\"D-{len(cat_rows) + 1:04d}\", \"category\": cat, \"desc\": describe(cat, words, d), **d,\n",
" \"iface\": c[\"iface\"], \"vendor\": str(rng.choice(VENDORS)), \"approved_for\": None, \"kind\": kind})\n",
" parts = pd.DataFrame(rows)\n",
" catalog = pd.DataFrame(cat_rows).sample(frac=1, random_state=seed).reset_index(drop=True)\n",
" return parts, catalog\n",
"# ---------------------------------------------------------------------------\n",
"\n",
"# ---- ๊ณต๊ธ๋ง ๋ถ๊ดด ์ํ๋(Supply Chain Risk Score) ๊ณตํต ํจ์: ๋ก์ง์คํฑ ํ๊ท + ์๋ณ ์ง์ฐ ์ถ์ด ์กฐ๊ธฐ๊ฒฝ๋ณด ----\n",
"RISK_FEATS = [\"n_suppliers\", \"overseas\", \"lead_time_days\", \"delay_rate\", \"avg_delay_days\", \"eol_ord\", \"ltb_short\", \"price_change_pct\",\n",
" \"crit_ord\", \"cover_gap_days\", \"defect_rate\", \"export_license\"]\n",
"FEAT_KO = {\"n_suppliers\": \"๊ณต๊ธ์ ์\", \"overseas\": \"ํด์ธ ์กฐ๋ฌ\", \"lead_time_days\": \"๋ฆฌ๋ํ์\", \"delay_rate\": \"๋ฉ๊ธฐ ์ง์ฐ๋ฅ \", \"avg_delay_days\": \"ํ๊ท ์ง์ฐ์ผ\",\n",
" \"eol_ord\": \"๋จ์ข
์ํ\", \"ltb_short\": \"์ต์ข
๊ตฌ๋งค ์๋ฐ\", \"price_change_pct\": \"๋จ๊ฐ ๋ณ๋\", \"crit_ord\": \"์ฒด๊ณ ์ค์๋\",\n",
" \"cover_gap_days\": \"์ฌ๊ณ -๋ฆฌ๋ํ์ ๊ฒฉ์ฐจ\", \"defect_rate\": \"์
๊ณ ๋ถ๋๋ฅ \", \"export_license\": \"์์ถ ํ๊ฐ ํ์\"}\n",
"WARN_CUT = {\"๊ฒฝ๋ณด\": 60, \"์ฃผ์\": 35} # ์ํ๋ ์ ์(0~100) ๋ฑ๊ธ ๊ธฐ์ค\n",
"\n",
"\n",
"def risk_frame(parts):\n",
" d = parts.copy()\n",
" d[\"delay_rate\"] = d[\"late_orders\"] / d[\"orders_12m\"].clip(lower=1)\n",
" d[\"eol_ord\"] = d[\"eol_status\"].map({\"์ ์\": 0, \"๋จ์ข
์๊ณ \": 1, \"๋จ์ข
\": 2})\n",
" d[\"ltb_short\"] = (d[\"ltb_months\"].fillna(99) <= 6).astype(int)\n",
" d[\"crit_ord\"] = d[\"criticality\"].map({\"C\": 0, \"B\": 1, \"A\": 2})\n",
" d[\"cover_gap_days\"] = d[\"stock_qty\"] / d[\"annual_demand\"].clip(lower=1) * 365 - d[\"lead_time_days\"]\n",
" for c in (\"overseas\", \"export_license\"):\n",
" d[c] = d[c].astype(int)\n",
" return d\n",
"\n",
"\n",
"def fit_risk(parts):\n",
" d = risk_frame(parts)\n",
" return make_pipeline(StandardScaler(), LogisticRegression(C=1.0, max_iter=500)).fit(d[RISK_FEATS], d[\"disrupted_next12m\"].astype(int))\n",
"\n",
"\n",
"def risk_score(model, parts):\n",
" \"\"\"๋ถ๊ดด ์ํ๋ 0~100์ = ๋ค์ 12๊ฐ์ ๋ด ๊ณต๊ธ ๋จ์ ํ๋ฅ x 100.\"\"\"\n",
" return model.predict_proba(risk_frame(parts)[RISK_FEATS])[:, 1] * 100\n",
"\n",
"\n",
"def drivers(model, parts, k=3):\n",
" \"\"\"๋ถํ๋ณ ์ํ ์์น ์์ธ ์์ k๊ฐ: (ํ์คํ ๊ฐ x ํ๊ท ๊ณ์)๊ฐ ํฐ ์์.\"\"\"\n",
" sc, lr = model[0], model[-1]\n",
" z = sc.transform(risk_frame(parts)[RISK_FEATS]) * lr.coef_[0]\n",
" return [[FEAT_KO[RISK_FEATS[j]] for j in np.argsort(-row)[:k] if row[j] > 0] for row in z]\n",
"\n",
"\n",
"def monthly_delay(parts, seed=5):\n",
" \"\"\"[๊ต์ก์ฉ๊ฐ์์๋ฃ] ๋ถํ๋ณ ์ต๊ทผ 12๊ฐ์ ํ๊ท ๋ฉ๊ธฐ ์ง์ฐ์ผ. ๋จ์ ๋ ๋ถํ์ ๋จ์ 3~6๊ฐ์ ์ ๋ถํฐ ์ง์ฐ์ด ์์ํ ๋์ด๋๋ค.\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" out, onset = [], []\n",
" for _, p in parts.iterrows():\n",
" base = 3 + 0.15 * p[\"avg_delay_days\"]\n",
" s = base + rng.normal(0, 1.2, 12)\n",
" o = -1\n",
" if p[\"disrupted_next12m\"] and rng.random() < 0.7:\n",
" o = int(rng.integers(5, 9)) # 12๊ฐ์ ๊ตฌ๊ฐ ๋ด ์์น ์์ ์\n",
" s[o:] += np.linspace(1.5, 12, 12 - o) + rng.normal(0, 1.0, 12 - o)\n",
" out.append(s); onset.append(o)\n",
" return np.array(out), np.array(onset)\n",
"\n",
"\n",
"def ewma_alarm(series, base_n=5, lam=0.3, k=3.0):\n",
" \"\"\"์ฒ์ base_n๊ฐ์์ ๊ธฐ์ค์ ์ผ๋ก ์ผ์ EWMA๊ฐ ๊ธฐ์ค์ + k x ํ์คํธ์ฐจ๋ฅผ ๋๋ ์ฒซ ๋ฌ(์์ผ๋ฉด -1).\"\"\"\n",
" mu, sd = series[:base_n].mean(), series[:base_n].std() + 0.5\n",
" e = mu\n",
" for t in range(base_n, len(series)):\n",
" e = lam * series[t] + (1 - lam) * e\n",
" if e > mu + k * sd * np.sqrt(lam / (2 - lam)):\n",
" return t\n",
" return -1\n",
"\n",
"train, _ = make_parts(600, seed=11) # ํ์ต์ฉ ๊ฐ์ ๋ถํ๊ตฐ\n",
"test, _ = make_parts(200, seed=7) # ๊ฒ์ฆ์ฉ ๋ณ๋ ๊ฐ์ ๋ถํ๊ตฐ\n",
"model = fit_risk(train)\n",
"test = test.assign(score=risk_score(model, test), why=drivers(model, test))\n",
"y = test[\"disrupted_next12m\"].astype(int)\n",
"test[\"level\"] = np.where(test[\"score\"] >= WARN_CUT[\"๊ฒฝ๋ณด\"], \"๊ฒฝ๋ณด\", np.where(test[\"score\"] >= WARN_CUT[\"์ฃผ์\"], \"์ฃผ์\", \"์ ์\"))\n",
"\n",
"# ===== ํ๋ฉด 1. ์
๋ ฅ: ์
๋ ฅ ์์ฝ๊ณผ ๊ฒ์ฆ =====\n",
"miss = int(test[[\"lead_time_days\", \"n_suppliers\", \"late_orders\"]].isna().sum().sum())\n",
"print(\"[ํ๋ฉด 1. ์
๋ ฅ] [๊ต์ก์ฉ๊ฐ์์๋ฃ]\")\n",
"print(f\" ๊ฒ์ฆ์ฉ ๋ถํ {len(test)}์ข
/ ์
๋ ฅ ์งํ {len(RISK_FEATS)}๊ฐ / ํ์๊ฐ ๊ฒฐ์ธก {miss}๊ฑด / ํ์ต ๋ถํ {len(train)}์ข
\")\n",
"print(f\" ํด์ธ ์กฐ๋ฌ {int(test['overseas'].sum())}์ข
, ๋จ์ข
์๊ณ ยท๋จ์ข
{int((test['eol_status'] != '์ ์').sum())}์ข
, ๋จ์ผ ๊ณต๊ธ์ {int((test['n_suppliers'] == 1).sum())}์ข
\")\n",
"\n",
"# ===== ํ๋ฉด 2. ๋ถ์: ์ํ๋ ๋ถํฌ์ ์ฑ๋ฅ =====\n",
"auc = roc_auc_score(y, test[\"score\"])\n",
"print(\"\\n[ํ๋ฉด 2. ๋ถ์] ์ํ๋ ์ ์ ๋ถํฌ์ ๋ชจ๋ธ ์ฑ๋ฅ\")\n",
"print(f\" ROC-AUC {auc:.3f}, Brier {brier_score_loss(y, test['score'] / 100):.3f} (๋จ์ ์ค์ ๋น์จ {y.mean():.0%})\")\n",
"print(test.groupby(\"level\").agg(๋ถํ์=(\"part_no\", \"size\"), ํ๊ท ์ ์=(\"score\", \"mean\"), ์ค์ ๋จ์ ๋ฅ =(\"disrupted_next12m\", \"mean\")).reindex([\"๊ฒฝ๋ณด\", \"์ฃผ์\", \"์ ์\"]).round(2).to_string())\n",
"coef = pd.Series(model[-1].coef_[0], index=[FEAT_KO[f] for f in RISK_FEATS]).sort_values(ascending=False)\n",
"print(\" ์ํ ์์น ์์ธ(ํ๊ท ๊ณ์ ์์ 4):\", \", \".join(f\"{k} {v:+.2f}\" for k, v in coef.head(4).items()))\n",
"\n",
"# ===== ์กฐ๊ธฐ๊ฒฝ๋ณด: ์๋ณ ์ง์ฐ ์ถ์ด =====\n",
"md, onset = monthly_delay(test)\n",
"alarm = np.array([ewma_alarm(s) for s in md])\n",
"dis = onset >= 0\n",
"det = (alarm[dis] >= 0).mean()\n",
"delay = (alarm[dis & (alarm >= 0)] - onset[dis & (alarm >= 0)]).mean()\n",
"fa = (alarm[~dis] >= 0).mean()\n",
"print(f\"\\n[์กฐ๊ธฐ๊ฒฝ๋ณด] ์ง์ฐ ์์นํ ๋จ์ ๋ถํ {int(dis.sum())}์ข
์ค {det:.0%} ๊ฐ์ง, ์์น ์์ ํ ํ๊ท {delay:.1f}๊ฐ์ ๋ง์ ๊ฒฝ๋ณด, ์ ์ ๋ถํ ์ค๊ฒฝ๋ณด์จ {fa:.1%}\")\n",
"\n",
"# ===== ํ๋ฉด 3. ๊ฒํ : ๊ฒฝ๋ณด ๋ถํ์ ๋ด๋น์๊ฐ ํ์ธ =====\n",
"rev = test[test[\"level\"] == \"๊ฒฝ๋ณด\"].sort_values(\"score\", ascending=False)\n",
"print(f\"\\n[ํ๋ฉด 3. ๊ฒํ ] ๊ฒฝ๋ณด {len(rev)}์ข
์ค ์์ 5์ข
(๋ด๋น์๊ฐ ๊ทผ๊ฑฐ๋ฅผ ํ์ธํ๊ณ ์กฐ์น ์ฌ๋ถ๋ฅผ ๊ฒฐ์ )\")\n",
"for _, r in rev.head(5).iterrows():\n",
" print(f\" {r['part_no']} {r['category']} {r['score']:.0f}์ | ์์ธ: {', '.join(r['why']) or '-'} | ๋จ์ข
{r['eol_status']}, ๊ณต๊ธ์ {r['n_suppliers']}๊ฐ\")\n",
"\n",
"# ===== ํ๋ฉด 4. ๊ฒฐ๊ณผ: ์์ฝ๊ณผ ๋ด๋ณด๋ด๊ธฐ =====\n",
"top = rev.head(10)\n",
"print(\"\\n[ํ๋ฉด 4. ๊ฒฐ๊ณผ]\")\n",
"print(f\" ๊ฒฝ๋ณด {len(rev)}์ข
, ์ฃผ์ {int((test['level'] == '์ฃผ์').sum())}์ข
, ์ ์ {int((test['level'] == '์ ์').sum())}์ข
\")\n",
"print(f\" ๊ฒฝ๋ณด ๋ฑ๊ธ์ ์ค์ ๋จ์ {int(rev['disrupted_next12m'].sum())}์ข
({rev['disrupted_next12m'].mean():.0%}) vs ์ ์ ๋ฑ๊ธ {test[test['level'] == '์ ์']['disrupted_next12m'].mean():.0%}\")\n",
"print(\" ๋ด๋ณด๋ด๊ธฐ ํญ๋ชฉ: ๋ถํ๋ฒํธ, ์ ์, ๋ฑ๊ธ, ์์ธ, ์กฐ๊ธฐ๊ฒฝ๋ณด ์, ๊ฒํ ์ํ(๋ฏธ๊ฒํ )\")\n",
"print(\"ํ๋ฉด 4์ข
(์
๋ ฅ-๋ถ์-๊ฒํ -๊ฒฐ๊ณผ) ๊ตฌํ: 4/4 (๋ชฉํ 4์ข
: ๋ฌ์ฑ)\")\n",
"print(\"โป ์ ์๋ ์ฐ์ ์์๋ฅผ ์ ํ๋ ์ฐธ๊ณ ๊ฐ์ด๋ฉฐ, ์ค์ ๋จ์ ์ฌ๋ถ์ ์กฐ์น๋ ๋ด๋น์๊ฐ ํ์ธํ๋ค.\")\n",
"\n",
"fig, ax = plt.subplots(2, 2, figsize=(11, 7))\n",
"ax[0, 0].bar([\"overseas\", \"single src\", \"EOL\"], [test[\"overseas\"].sum(), (test[\"n_suppliers\"] == 1).sum(), (test[\"eol_status\"] != \"์ ์\").sum()]); ax[0, 0].set_title(\"Screen 1: input summary (count)\")\n",
"ax[0, 1].hist([test[y == 0][\"score\"], test[y == 1][\"score\"]], bins=15, label=[\"no disruption\", \"disrupted\"], stacked=True); ax[0, 1].legend(); ax[0, 1].set_title(\"Screen 2: risk score distribution\")\n",
"i = int(np.where(dis & (alarm >= 0))[0][0]); ax[1, 0].plot(md[i], marker=\"o\"); ax[1, 0].axvline(alarm[i], color=\"r\", ls=\"--\"); ax[1, 0].set_title(\"Screen 3: early warning (delay days, red = alarm)\")\n",
"ax[1, 1].barh(top[\"part_no\"], top[\"score\"], color=\"tab:red\"); ax[1, 1].invert_yaxis(); ax[1, 1].set_title(\"Screen 4: top-10 alert parts\")\n",
"plt.tight_layout(); plt.show()\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"๐ก [์ฐธ๊ณ ] ๋ฏธ์
05 ์์ ์คํ ๊ฒฐ๊ณผ (Output)
\n",
"\n",
"```text\n",
"[ํ๋ฉด 1. ์
๋ ฅ] [๊ต์ก์ฉ๊ฐ์์๋ฃ]\n",
" ๊ฒ์ฆ์ฉ ๋ถํ 200์ข
/ ์
๋ ฅ ์งํ 12๊ฐ / ํ์๊ฐ ๊ฒฐ์ธก 0๊ฑด / ํ์ต ๋ถํ 600์ข
\n",
" ํด์ธ ์กฐ๋ฌ 159์ข
, ๋จ์ข
์๊ณ ยท๋จ์ข
60์ข
, ๋จ์ผ ๊ณต๊ธ์ 89์ข
\n",
"\n",
"[ํ๋ฉด 2. ๋ถ์] ์ํ๋ ์ ์ ๋ถํฌ์ ๋ชจ๋ธ ์ฑ๋ฅ\n",
" ROC-AUC 0.693, Brier 0.222 (๋จ์ ์ค์ ๋น์จ 40%)\n",
" ๋ถํ์ ํ๊ท ์ ์ ์ค์ ๋จ์ ๋ฅ \n",
"level \n",
"๊ฒฝ๋ณด 51 76.75 0.59\n",
"์ฃผ์ 59 47.63 0.42\n",
"์ ์ 90 22.39 0.27\n",
" ์ํ ์์น ์์ธ(ํ๊ท ๊ณ์ ์์ 4): ๋ฉ๊ธฐ ์ง์ฐ๋ฅ +0.58, ๋ฆฌ๋ํ์ +0.50, ๋จ์ข
์ํ +0.48, ์์ถ ํ๊ฐ ํ์ +0.12\n",
"\n",
"[์กฐ๊ธฐ๊ฒฝ๋ณด] ์ง์ฐ ์์นํ ๋จ์ ๋ถํ 49์ข
์ค 100% ๊ฐ์ง, ์์น ์์ ํ ํ๊ท 1.9๊ฐ์ ๋ง์ ๊ฒฝ๋ณด, ์ ์ ๋ถํ ์ค๊ฒฝ๋ณด์จ 2.0%\n",
"\n",
"[ํ๋ฉด 3. ๊ฒํ ] ๊ฒฝ๋ณด 51์ข
์ค ์์ 5์ข
(๋ด๋น์๊ฐ ๊ทผ๊ฑฐ๋ฅผ ํ์ธํ๊ณ ์กฐ์น ์ฌ๋ถ๋ฅผ ๊ฒฐ์ )\n",
" P-0066 ์ปค๋ฅํฐ 96์ | ์์ธ: ๋จ์ข
์ํ, ๋ฉ๊ธฐ ์ง์ฐ๋ฅ , ๋ฆฌ๋ํ์ | ๋จ์ข
๋จ์ข
, ๊ณต๊ธ์ 1๊ฐ\n",
" P-0080 ์ ์ด๋ณด๋ 96์ | ์์ธ: ๋จ์ข
์ํ, ๋ฉ๊ธฐ ์ง์ฐ๋ฅ , ๋ฆฌ๋ํ์ | ๋จ์ข
๋จ์ข
, ๊ณต๊ธ์ 1๊ฐ\n",
" P-0036 ๋ฒ ์ด๋ง 94์ | ์์ธ: ๋จ์ข
์ํ, ๋ฉ๊ธฐ ์ง์ฐ๋ฅ , ๊ณต๊ธ์ ์ | ๋จ์ข
๋จ์ข
, ๊ณต๊ธ์ 1๊ฐ\n",
" P-0003 ์ ์๋ฐธ๋ธ 94์ | ์์ธ: ๋จ์ข
์ํ, ๋ฉ๊ธฐ ์ง์ฐ๋ฅ , ๋ฆฌ๋ํ์ | ๋จ์ข
๋จ์ข
, ๊ณต๊ธ์ 1๊ฐ\n",
" P-0140 ๋ฒ ์ด๋ง 94์ | ์์ธ: ๋ฆฌ๋ํ์, ๋ฉ๊ธฐ ์ง์ฐ๋ฅ , ๊ณต๊ธ์ ์ | ๋จ์ข
์ ์, ๊ณต๊ธ์ 1๊ฐ\n",
"\n",
"[ํ๋ฉด 4. ๊ฒฐ๊ณผ]\n",
" ๊ฒฝ๋ณด 51์ข
, ์ฃผ์ 59์ข
, ์ ์ 90์ข
\n",
" ๊ฒฝ๋ณด ๋ฑ๊ธ์ ์ค์ ๋จ์ 30์ข
(59%) vs ์ ์ ๋ฑ๊ธ 27%\n",
" ๋ด๋ณด๋ด๊ธฐ ํญ๋ชฉ: ๋ถํ๋ฒํธ, ์ ์, ๋ฑ๊ธ, ์์ธ, ์กฐ๊ธฐ๊ฒฝ๋ณด ์, ๊ฒํ ์ํ(๋ฏธ๊ฒํ )\n",
"ํ๋ฉด 4์ข
(์
๋ ฅ-๋ถ์-๊ฒํ -๊ฒฐ๊ณผ) ๊ตฌํ: 4/4 (๋ชฉํ 4์ข
: ๋ฌ์ฑ)\n",
"โป ์ ์๋ ์ฐ์ ์์๋ฅผ ์ ํ๋ ์ฐธ๊ณ ๊ฐ์ด๋ฉฐ, ์ค์ ๋จ์ ์ฌ๋ถ์ ์กฐ์น๋ ๋ด๋น์๊ฐ ํ์ธํ๋ค.\n",
"```\n",
" \n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## ๐ฏ ๋ฏธ์
06. ๋ถํ ์กฐ๋ฌ ์ํ ๊ณต๊ธ๋ง ์ง๋ ๋ฐ ๋์ฒด์ ์ถ์ฒ ์ธํฐ๋ํฐ๋ธ ๋์๋ณด๋ ๊ตฌํ\n",
"**์์์๊ฐ**: ์ฝ 2.5์๊ฐ\n",
"\n",
"### ๐ ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ์ํ ์ง์นจ\n",
"#### ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ๋น์์ฑ\n",
"\n",
"๋ถํ ์กฐ๋ฌ ์ํ ๊ณต๊ธ๋ง ์ง๋ ๋ฐ ๋์ฒด์ ์ถ์ฒ ๋์๋ณด๋๋ฅผ ๊ตฌํํ๋ค. ์ค์ ์ ์ ํ์ฅ์์๋ ๋จ์ ํ์ง์ ๊ทธ์น์ง ์๊ณ ์ ์ํ ์ํ ํ๋จ๊ณผ ์ ๋ขฐ๋ ๋์ ์์ฌ๊ฒฐ์ ์ด ํ์์ ์ด๋ฏ๋ก, **๋ถํ ์กฐ๋ฌ ์ํ ๊ณต๊ธ๋ง ์ง๋์ ๋์ฒด์ ์ถ์ฒ ๋์๋ณด๋์ ์
๋ ฅ ๊ฒ์ฆ๊ณผ ์๋ฌ ํธ๋ค๋ฌ๋ฅผ ๋ถ์ฌ ๊นจ์ง ์
๋ ฅ์๋ ๋ฉ์ถ์ง ์๊ฒ ํ๋ค.** ๊ณผ์
์ ์์ํ์ฌ ์คํ์ ๋ฐฉ์งํ๊ณ ์์ ๋์๋ ฅ์ ๊ทน๋ํํด์ผ ํฉ๋๋ค.\n",
"\n",
"์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ณธ ๋ฏธ์
์์๋ **๊ตญ๊ฐ์ ๋ถํ ๋ฒ์ฃผ๋ณ ํ๊ท ์ํ์ ๋ณด์ด๋ ๊ณต๊ธ๋ง ์ง๋, ๋ฆฌ๋ํ์๊ณผ ์ง์ฐ๋ฅ ์ฐ์ ๋, ์ํ ์์ ๋ถํ์ ๋์ฒด ํ๋ณด ๊ท๊ฒฉ ์ ํฉ์ฑ ํจ๋์ ๊ฐ์ง ๋์๋ณด๋ ์์ฑํ์ฌ ์์ธ์ ๋ถ์ํ๋** ์ค์ต์ ์ฒด๊ณ์ ์ผ๋ก ์ํํฉ๋๋ค.\n",
"\n",
"ํ์ต์๋ ์ค์ต๊ฐ์ด๋์ ์ ์๋ ์ด 4๊ฐ ์ธ๋ถ ํ๋(๊ธฐ๋ณธ ๊ธฐ๋ฅ ๊ตฌํ โ ๊ฒฐ๊ณผ ์งํ ๊ฒ์ฆ โ ํ๋ผ๋ฏธํฐ ํ๋ ๋ฐ ๋์ ๊ณผ์ )์ ์์ฐจ์ ์ผ๋ก ์งํํ๋ฉฐ ํต์ฌ ํ์ ๊ธฐ์ค์ ๋ฌ์ฑํฉ๋๋ค.\n",
"\n",
"**์ด๋ฒ ๊ฒฐ๊ณผ๋ฌผ:** `src/utils/prc07_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": [
"\"\"\"์กฐ๋ฌ ์ํ ๊ณต๊ธ๋ง ์ง๋(๊ตญ๊ฐ x ๋ถํ ๋ฒ์ฃผ ์ํ ํํธ๋งต)์ ๋์ฒด์ ์ถ์ฒ ํจ๋์ ๊ทธ๋ฆฌ๋ ๋์๋ณด๋์ ์
๋ ฅ ๊ฒ์ฆ๊ธฐ์ ์๋ฌ ํธ๋ค๋ฌ๋ฅผ ๋ถ์ด๊ณ , ๊นจ์ง ์
๋ ฅ 14์ข
์ ์ฃผ์
ํด ํฌ๋์ 0๊ฑดยท์๋ฌ ๋ก๊ทธ ๊ธฐ๋ก๋ฅ 100%ยท์ฌ์ฉ์์ฉ ์กฐ์น ์๋ด ๋ฌธ๊ตฌ๋ฅผ ํ์ธํ๋ค.\"\"\"\n",
"import csv\n",
"import io\n",
"import json\n",
"import re\n",
"import numpy as np\n",
"import pandas as pd\n",
"import matplotlib.pyplot as plt\n",
"from sklearn.feature_extraction.text import TfidfVectorizer\n",
"from sklearn.metrics.pairwise import cosine_similarity\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# [๊ต์ก์ฉ๊ฐ์์๋ฃ] ๊ฐ์ ๋ฌด๊ธฐ์ฒด๊ณ ๋ถํยท๊ณต๊ธ๋ง ๋ฐ์ดํฐ ์์ฑ๊ธฐ (์ค์ ์ฒด๊ณยท์
์ฒดยท๊ตญ๊ฐยท๋จ๊ฐ ์ ๋ณด ์์)\n",
"# ๋ถํ 100์ข
(ํด์ธ ์กฐ๋ฌ ์ฝ 80%)์ ๊ณต๊ธ๋ง ์์ฑ๊ณผ, ๊ตญ๋ด ๋ถํ ์นดํ๋ก๊ทธ(๊ณผ๊ฑฐ ์น์ธ๋ ๊ตญ์ฐ ๋์ฒดํ +\n",
"# ์ค๋ช
์ ๋น์ทํ์ง๋ง ๊ท๊ฒฉ์ด ๋ง์ง ์๋ ์ ์ฌ ํ๋ณด)๋ฅผ ํจ๊ป ๋ง๋ ๋ค.\n",
"# ---------------------------------------------------------------------------\n",
"COUNTRIES = [\"๊ฐ์๊ตญ๊ฐA\", \"๊ฐ์๊ตญ๊ฐB\", \"๊ฐ์๊ตญ๊ฐC\", \"๊ตญ๋ด\"]\n",
"CATS = {\n",
" \"์ ์๋ชจ๋\": dict(words=[\"DC-DC\", \"์ปจ๋ฒํฐ\", \"์ ์ฐํ\", \"์ ์\", \"์ ์ถ๋ ฅ\"], syn={\"์ปจ๋ฒํฐ\": \"๋ณํ์ฅ์น\", \"์ ์ฐํ\": \"์์ด์๋ ์ด์
\", \"์ ์ถ๋ ฅ\": \"์์ ํ\"}, std=\"MIL-STD-704\", iface=\"๋จ์๋\"),\n",
" \"์ปค๋ฅํฐ\": dict(words=[\"์ํ\", \"์ปค๋ฅํฐ\", \"๋ฐฉ์ํ\", \"๋คํ\", \"๋์ฌ์ฒด๊ฒฐ\"], syn={\"์ปค๋ฅํฐ\": \"์ ์์\", \"๋ฐฉ์ํ\": \"๋ฐํํ\", \"๋์ฌ์ฒด๊ฒฐ\": \"์คํฌ๋ฃจ๊ฒฐํฉ\"}, std=\"MIL-DTL-38999\", iface=\"์ํํ\"),\n",
" \"์ ์๋ฐธ๋ธ\": dict(words=[\"์ ์\", \"์๋ ๋
ธ์ด๋\", \"๋ฐธ๋ธ\", \"๋ฐฉํฅ์ ์ด\", \"4ํฌํธ\"], syn={\"๋ฐธ๋ธ\": \"๊ฐํ์ฅ์น\", \"๋ฐฉํฅ์ ์ด\": \"์ ๋ก์ ํ\", \"์๋ ๋
ธ์ด๋\": \"์ ์์\"}, std=\"SAE-AS5440\", iface=\"ํ๋์ง\"),\n",
" \"๋ฒ ์ด๋ง\": dict(words=[\"๋ณผ\", \"๋ฒ ์ด๋ง\", \"๊ณ ์\", \"๋ฐ๋ดํ\", \"ํ์ ์ถ\"], syn={\"๋ฒ ์ด๋ง\": \"์ถ๋ฐ์ด\", \"๋ฐ๋ดํ\": \"์ค๋ํ\", \"๊ณ ์\": \"๊ณ ํ์ \"}, std=\"ISO-492\", iface=\"์ถ๊ฒฐํฉ\"),\n",
" \"๊ดํ๋ ์ฆ\": dict(words=[\"์ ์ธ์ \", \"๋ ์ฆ\", \"๊ฒ๋ฅด๋ง๋\", \"๋ฐ์ฌ๋ฐฉ์ง\", \"์ง๊ด\"], syn={\"๋ ์ฆ\": \"๊ดํ์ฐฝ\", \"์ง๊ด\": \"์ด์ ํ์ฑ\", \"๋ฐ์ฌ๋ฐฉ์ง\": \"AR์ฝํ
\"}, std=\"MIL-PRF-13830\", iface=\"๋ง์ดํธ\"),\n",
" \"์์ด๋ก์ผ์\": dict(words=[\"๊ด์ฌ์ \", \"์์ด๋ก\", \"๊ฐ์๋\", \"์ผ์\", \"๊ด์ฑ\"], syn={\"์ผ์\": \"๊ฐ์ง๊ธฐ\", \"๊ฐ์๋\": \"ํ์ ์จ\", \"๊ด์ฑ\": \"INS์ฉ\"}, std=\"IEEE-952\", iface=\"์ง๋ ฌํต์ \"),\n",
" \"RF์ฆํญ๊ธฐ\": dict(words=[\"RF\", \"์ ๋ ฅ์ฆํญ๊ธฐ\", \"๊ด๋์ญ\", \"๋ชจ๋\", \"์ก์ \"], syn={\"์ ๋ ฅ์ฆํญ๊ธฐ\": \"ํ์์ฐํ\", \"์ก์ \": \"์ถ๋ ฅ๋จ\", \"๊ด๋์ญ\": \"์์ด๋๋ฐด๋\"}, std=\"MIL-STD-461\", iface=\"SMA\"),\n",
" \"์ ์ด๋ณด๋\": dict(words=[\"FPGA\", \"์ ์ด๋ณด๋\", \"์๋ฒ ๋๋\", \"์ฐ์ฐ\", \"์
์ถ๋ ฅ\"], syn={\"์ ์ด๋ณด๋\": \"์ปจํธ๋กค์นด๋\", \"์ฐ์ฐ\": \"์ ํธ์ฒ๋ฆฌ\", \"์๋ฒ ๋๋\": \"๋ด์ฅํ\"}, std=\"VITA-46\", iface=\"๋ฐฑํ๋ ์ธ\"),\n",
"}\n",
"V_OPTS, W_OPTS = [12, 24, 28], [25, 50, 100]\n",
"VENDORS = [\"๊ฐ์์ ๋ฐ(์ฃผ)\", \"๊ฐ์์ ์(์ฃผ)\", \"๊ฐ์์ ๊ณต์(์ฃผ)\", \"๊ฐ์๊ดํ(์ฃผ)\", \"๊ฐ์ํ
ํฌ(์ฃผ)\", \"๊ฐ์์์คํ
(์ฃผ)\"]\n",
"\n",
"\n",
"def describe(cat, words, s):\n",
" return f\"{cat} {' '.join(words)} {s['v']}V {s['w']}W {s['tmin']}~{s['tmax']}C {s['std']}\"\n",
"\n",
"\n",
"def _sig(x):\n",
" return 1 / (1 + np.exp(-x))\n",
"\n",
"\n",
"def make_parts(n=100, seed=7):\n",
" \"\"\"๋ถํ ๋ชฉ๋ก(๊ณต๊ธ๋ง ์์ฑ ํฌํจ)๊ณผ ๊ตญ๋ด ์นดํ๋ก๊ทธ๋ฅผ ๋ง๋ ๋ค. ๋ฐํ: (parts DataFrame, catalog DataFrame)\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" cats, rows, cat_rows = list(CATS), [], []\n",
" for i in range(n):\n",
" cat = cats[i % len(cats)]; c = CATS[cat]\n",
" spec = {\"v\": int(rng.choice(V_OPTS)), \"w\": int(rng.choice(W_OPTS)), \"tmin\": int(rng.choice([-55, -40])),\n",
" \"tmax\": int(rng.choice([85, 125])), \"size_mm\": float(round(rng.uniform(20, 120), 1)), \"std\": c[\"std\"]}\n",
" words = [str(x) for x in rng.permutation(c[\"words\"])[:4]]\n",
" country = str(rng.choice(COUNTRIES, p=[0.35, 0.3, 0.15, 0.2]))\n",
" overseas = country != \"๊ตญ๋ด\"\n",
" n_sup = int(rng.choice([1, 2, 3], p=[0.45, 0.35, 0.2])) if overseas else int(rng.choice([1, 2, 3]))\n",
" lead = float(round(rng.gamma(3, 45 if overseas else 25) + 20))\n",
" eol = str(rng.choice([\"์ ์\", \"๋จ์ข
์๊ณ \", \"๋จ์ข
\"], p=[0.7, 0.2, 0.1]))\n",
" crit = str(rng.choice([\"A\", \"B\", \"C\"], p=[0.3, 0.4, 0.3]))\n",
" z = (-2.6 + 1.1 * (n_sup == 1) + 0.9 * overseas + 0.006 * (lead - 100) + 1.2 * (eol == \"๋จ์ข
์๊ณ \")\n",
" + 2.0 * (eol == \"๋จ์ข
\") + rng.normal(0, 0.6))\n",
" p_late = float(np.clip(_sig(z) * 0.8 + 0.05, 0, 0.9))\n",
" late = int(rng.binomial(12, p_late))\n",
" demand = int(rng.integers(20, 400))\n",
" rows.append({\"part_no\": f\"P-{i + 1:04d}\", \"category\": cat, \"desc\": describe(cat, words, spec), **spec,\n",
" \"iface\": c[\"iface\"], \"country\": country, \"overseas\": overseas, \"n_suppliers\": n_sup,\n",
" \"lead_time_days\": lead, \"orders_12m\": 12, \"late_orders\": late,\n",
" \"avg_delay_days\": float(round(late and rng.gamma(2, 12 + 20 * p_late), 1)),\n",
" \"eol_status\": eol, \"ltb_months\": int(rng.integers(1, 25)) if eol == \"๋จ์ข
์๊ณ \" else None,\n",
" \"price_change_pct\": float(round(rng.normal(4 + 12 * p_late, 8), 1)), \"criticality\": crit,\n",
" \"annual_demand\": demand, \"stock_qty\": int(rng.integers(0, demand)),\n",
" \"defect_rate\": float(round(abs(rng.normal(0.8 + 2 * p_late, 0.8)), 2)),\n",
" \"export_license\": bool(overseas and rng.random() < 0.35),\n",
" \"disrupted_next12m\": bool(rng.random() < _sig(z + 0.4))})\n",
" if not overseas:\n",
" continue\n",
" # ๊ณผ๊ฑฐ ์น์ธ๋ ๊ตญ์ฐ ๋์ฒดํ(์ ๋ต): ๋์์ด๋ก ์์ , ๊ท๊ฒฉ ํธํ(์ ๋ ฅ ์ฌ์ ํ์ฉ, ์ผ๋ถ๋ ํฌ๊ธฐ ์ฐจ์ด๊ฐ ์ปค ์ฅ์ฐฉ ๊ฒํ ํ์)\n",
" eq_words = [c[\"syn\"].get(w, w) for w in words]\n",
" hard = rng.random() < 0.15 # ์์ ์ด ๋ง์ด ๋ค๋ฅธ ์ฌ๋ก\n",
" if hard:\n",
" eq_words = [c[\"syn\"].get(w, w) for w in c[\"words\"] if w not in words][:1] + eq_words[1:3]\n",
" eq = {**spec, \"w\": spec[\"w\"] if rng.random() < 0.7 else W_OPTS[min(W_OPTS.index(spec[\"w\"]) + 1, 2)],\n",
" \"size_mm\": float(round(spec[\"size_mm\"] * (rng.uniform(1.11, 1.15) if rng.random() < 0.12 else rng.uniform(0.95, 1.05)), 1))}\n",
" cat_rows.append({\"cand_no\": f\"D-{len(cat_rows) + 1:04d}\", \"category\": cat, \"desc\": describe(cat, eq_words, eq), **eq,\n",
" \"iface\": c[\"iface\"], \"vendor\": str(rng.choice(VENDORS)), \"approved_for\": f\"P-{i + 1:04d}\", \"kind\": \"equivalent\"})\n",
" # ์ ์ฌ ์ค๋ต ํ๋ณด: ์ค๋ช
์ ๊ฑฐ์ ๊ฐ์ง๋ง ์ ์์ด ๋ค๋ฅด๊ฑฐ๋(5V/48V) ์์ฉ ์จ๋ ๋ฑ๊ธ(0~70C)\n",
" for kind in (\"volt_mismatch\", \"temp_mismatch\"):\n",
" d = dict(spec)\n",
" if kind == \"volt_mismatch\":\n",
" d[\"v\"] = int(rng.choice([5, 48])) # ์ฒด๊ณ์์ ์ฐ์ง ์๋ ์ ์\n",
" else:\n",
" d[\"tmin\"], d[\"tmax\"] = 0, 70\n",
" d[\"size_mm\"] = float(round(spec[\"size_mm\"] * rng.uniform(0.95, 1.05), 1))\n",
" cat_rows.append({\"cand_no\": f\"D-{len(cat_rows) + 1:04d}\", \"category\": cat, \"desc\": describe(cat, words, d), **d,\n",
" \"iface\": c[\"iface\"], \"vendor\": str(rng.choice(VENDORS)), \"approved_for\": None, \"kind\": kind})\n",
" parts = pd.DataFrame(rows)\n",
" catalog = pd.DataFrame(cat_rows).sample(frac=1, random_state=seed).reset_index(drop=True)\n",
" return parts, catalog\n",
"# ---------------------------------------------------------------------------\n",
"\n",
"# ---- ๋์ฒด ๋ถํ ์ถ์ฒ ๊ณตํต ํจ์: ํ
์คํธ ์ ์ฌ๋ + ๊ท๊ฒฉ ์ ํฉ์ฑ + ์ง์ ๊ทธ๋ํ ์ด์ ์ ์ฌ๋ ----\n",
"WEIGHTS = {\"text\": 0.35, \"spec\": 0.45, \"graph\": 0.20}\n",
"\n",
"\n",
"def spec_fit(p, c):\n",
" \"\"\"๊ท๊ฒฉ ์ ํฉ์ฑ 0~1: 6๊ฐ ์กฐ๊ฑด ์ถฉ์กฑ ๋น์จ. ์ ์ยท์จ๋ ํ๊ณ(ํ์ ์กฐ๊ฑด)๋ฅผ ๋ชป ๋ง์ถ๋ฉด ์ ์๋ฅผ ์ ๋ฐ์ผ๋ก ๊น๋๋ค.\"\"\"\n",
" conds = [p[\"v\"] == c[\"v\"], c[\"w\"] >= p[\"w\"], c[\"tmin\"] <= p[\"tmin\"], c[\"tmax\"] >= p[\"tmax\"],\n",
" abs(c[\"size_mm\"] - p[\"size_mm\"]) / p[\"size_mm\"] <= 0.10, p[\"iface\"] == c[\"iface\"]]\n",
" s = sum(conds) / len(conds)\n",
" return (s * 0.5 if not (conds[0] and conds[2] and conds[3]) else s), conds\n",
"\n",
"\n",
"def graph_nodes(row):\n",
" \"\"\"์ง์ ๊ทธ๋ํ์์ ๋ถํ ๋
ธ๋์ ์ฐ๊ฒฐ๋ ์ด์ ๋
ธ๋ ์งํฉ(๋ฒ์ฃผยทํ์คยท์ธํฐํ์ด์คยท๊ธฐ๋ฅ์ดยท๋ฑ๊ธ).\"\"\"\n",
" words = set(row[\"desc\"].split()[1:-4])\n",
" grade = \"๊ตฐ์ฉ๋ฑ๊ธ\" if row[\"tmin\"] <= -40 else \"์์ฉ๋ฑ๊ธ\"\n",
" return {f\"cat:{row['category']}\", f\"std:{row['std']}\", f\"if:{row['iface']}\", f\"grade:{grade}\", f\"v:{row['v']}\"} | {f\"w:{w}\" for w in words}\n",
"\n",
"\n",
"def build_graph(catalog):\n",
" \"\"\"๋์์ด ์ฌ์ ์ ๊ทธ๋ํ ๊ฐ์ (๊ธฐ๋ฅ์ด = ๋์์ด)์ผ๋ก ๋ฃ์ด ์์ ์ด ๋ฌ๋ผ๋ ๊ฐ์ ๊ธฐ๋ฅ ๋
ธ๋๋ก ์ฐ๊ฒฐ๋๊ฒ ํ๋ค.\"\"\"\n",
" syn = {f\"w:{b}\": f\"w:{a}\" for c in CATS.values() for a, b in c[\"syn\"].items()}\n",
" canon = lambda nodes: {syn.get(x, x) for x in nodes}\n",
" return {\"canon\": canon, \"nodes\": [canon(graph_nodes(r)) for _, r in catalog.iterrows()]}\n",
"\n",
"\n",
"def graph_sim(g, pn, cn):\n",
" \"\"\"๋ ๋ถํ์ด ๊ณต์ ํ๋ ์ด์ ๋
ธ๋ ๋น์จ(Jaccard).\"\"\"\n",
" pn = g[\"canon\"](pn)\n",
" return len(pn & cn) / max(len(pn | cn), 1)\n",
"\n",
"\n",
"def score_table(parts_df, catalog):\n",
" \"\"\"ํด์ธ ๋ถํ x ๊ฐ์ ๋ฒ์ฃผ ๊ตญ๋ด ํ๋ณด ์๋ง๋ค ํ
์คํธยท๊ท๊ฒฉยท๊ทธ๋ํ ์ ์๋ฅผ ๊ณ์ฐํ ํ(๊ฐ์ค์น ์ ์ฉ ์ ).\"\"\"\n",
" vec = TfidfVectorizer(token_pattern=r\"[^ ]+\").fit(list(parts_df[\"desc\"]) + list(catalog[\"desc\"]))\n",
" text = cosine_similarity(vec.transform(parts_df[\"desc\"]), vec.transform(catalog[\"desc\"]))\n",
" g = build_graph(catalog)\n",
" rows = []\n",
" for i, (_, p) in enumerate(parts_df.iterrows()):\n",
" pn = graph_nodes(p)\n",
" for j, (_, c) in enumerate(catalog.iterrows()):\n",
" if c[\"category\"] != p[\"category\"]:\n",
" continue\n",
" sf, conds = spec_fit(p, c)\n",
" rows.append({\"part_no\": p[\"part_no\"], \"cand_no\": c[\"cand_no\"], \"vendor\": c[\"vendor\"], \"text\": text[i, j],\n",
" \"spec\": sf, \"graph\": graph_sim(g, pn, g[\"nodes\"][j]),\n",
" \"unmet\": \",\".join(n for n, ok in zip([\"์ ์\", \"์ ๋ ฅ\", \"์ ์จ\", \"๊ณ ์จ\", \"ํฌ๊ธฐ\", \"์ธํฐํ์ด์ค\"], conds) if not ok) or \"-\",\n",
" \"kind\": c[\"kind\"], \"correct\": c[\"approved_for\"] == p[\"part_no\"]})\n",
" return pd.DataFrame(rows)\n",
"\n",
"\n",
"def recommend(table, weights=None, top=3):\n",
" \"\"\"๊ฐ์คํฉ ์ ์๋ก ๋ถํ๋ณ ์์ top๊ฐ ํ๋ณด๋ฅผ ๊ณ ๋ฅธ๋ค.\"\"\"\n",
" w = weights or WEIGHTS\n",
" t = table.assign(score=w[\"text\"] * table[\"text\"] + w[\"spec\"] * table[\"spec\"] + w[\"graph\"] * table[\"graph\"])\n",
" return t.sort_values([\"part_no\", \"score\"], ascending=[True, False]).groupby(\"part_no\").head(top)\n",
"\n",
"\n",
"def hit_rate(recs, k=1):\n",
" \"\"\"๊ณผ๊ฑฐ ์น์ธ ์ด๋ ฅ(์ ๋ต ๋์ฒดํ)์ด ์์ k๊ฐ ์์ ๋ค์ด๊ฐ ๋ถํ ๋น์จ.\"\"\"\n",
" return recs.groupby(\"part_no\").apply(lambda d: d[\"correct\"].head(k).any(), include_groups=False).mean()\n",
"\n",
"NEED = {\"part_no\": str, \"category\": str, \"country\": str, \"n_suppliers\": float, \"lead_time_days\": float, \"late_orders\": float,\n",
" \"orders_12m\": float, \"eol_status\": str, \"annual_demand\": float, \"stock_qty\": float, \"desc\": str, \"v\": float, \"w\": float,\n",
" \"tmin\": float, \"tmax\": float, \"size_mm\": float, \"std\": str, \"iface\": str}\n",
"RANGE = {\"n_suppliers\": (1, 10), \"lead_time_days\": (1, 1000), \"late_orders\": (0, 100), \"orders_12m\": (1, 100), \"annual_demand\": (1, 100000), \"stock_qty\": (0, 100000)}\n",
"LOG = []\n",
"\n",
"\n",
"class UserError(Exception):\n",
" \"\"\"์ฌ์ฉ์์๊ฒ ๋ณด์ฌ ์ค ๋ฉ์์ง์ ์กฐ์น ๊ฐ์ด๋๋ฅผ ํจ๊ป ๊ฐ์ง ์์ธ.\"\"\"\n",
"\n",
" def __init__(self, code, msg, action):\n",
" super().__init__(msg); self.code, self.msg, self.action = code, msg, action\n",
"\n",
"\n",
"def log(level, code, detail):\n",
" LOG.append({\"seq\": len(LOG) + 1, \"level\": level, \"code\": code, \"detail\": str(detail)[:80]})\n",
"\n",
"\n",
"def read_input(src):\n",
" \"\"\"์
๋ ฅ(CSV ๋ฌธ์์ด / JSON ๋ฌธ์์ด / DataFrame)์ DataFrame์ผ๋ก ์ฝ๋๋ค. ์คํจํ๋ฉด UserError.\"\"\"\n",
" if src is None:\n",
" raise UserError(\"E01\", \"์
๋ ฅ์ด ๋น์ด ์์ต๋๋ค.\", \"ํ์ผ์ ๋ค์ ์ ํํด ์ฃผ์ธ์.\")\n",
" if isinstance(src, pd.DataFrame):\n",
" df = src.copy()\n",
" elif isinstance(src, str):\n",
" if not src.strip():\n",
" raise UserError(\"E02\", \"๋น ํ์ผ์
๋๋ค.\", \"๋ด์ฉ์ด ์๋ ํ์ผ์ธ์ง ํ์ธํด ์ฃผ์ธ์.\")\n",
" try:\n",
" df = pd.DataFrame(json.loads(src)) if src.lstrip()[0] in \"[{\" else pd.DataFrame(list(csv.DictReader(io.StringIO(src))))\n",
" except Exception as e:\n",
" raise UserError(\"E03\", \"ํ์ผ ํ์์ ์ฝ์ ์ ์์ต๋๋ค.\", \"CSV(์ผํ ๊ตฌ๋ถ) ๋๋ JSON(๋ชฉ๋ก) ํ์์ผ๋ก ์ ์ฅํ๋์ง ํ์ธํด ์ฃผ์ธ์.\") from e\n",
" else:\n",
" raise UserError(\"E04\", f\"์ง์ํ์ง ์๋ ์
๋ ฅ ํ์์
๋๋ค({type(src).__name__}).\", \"CSV ๋๋ JSON ํ์ผ์ ์ฌ์ฉํด ์ฃผ์ธ์.\")\n",
" if df.empty:\n",
" raise UserError(\"E02\", \"์ฝ์ ํ์ด 0๊ฑด์
๋๋ค.\", \"๋จธ๋ฆฌ๊ธ๋ง ์๋ ํ์ผ์ด ์๋์ง ํ์ธํด ์ฃผ์ธ์.\")\n",
" return df\n",
"\n",
"\n",
"def validate_table(df):\n",
" \"\"\"์ด ์ ๊ฒ -> ํ ๋จ์ ์ ๊ฒ. ๋ฌธ์ ํ์ ๊ฒฉ๋ฆฌํ๊ณ ๋๋จธ์ง๋ก ๊ณ์ ์งํํ๋ค. ๋ฐํ: (์ ์ ํ, ๊ฒฉ๋ฆฌ ๋ด์ญ)\"\"\"\n",
" miss = [c for c in NEED if c not in df.columns]\n",
" if miss:\n",
" raise UserError(\"E10\", f\"ํ์ ์ด์ด ์์ต๋๋ค: {', '.join(miss[:4])}{' ์ธ' if len(miss) > 4 else ''}\", \"์ด ์ด๋ฆ์ด ์์(๋ฐ์ดํฐ ๋์
๋๋ฆฌ)๊ณผ ๊ฐ์์ง ํ์ธํด ์ฃผ์ธ์.\")\n",
" d = df.copy(); bad = {}\n",
" for c, t in NEED.items():\n",
" if t is float:\n",
" conv = pd.to_numeric(d[c], errors=\"coerce\")\n",
" for i in d.index[conv.isna()]:\n",
" bad.setdefault(i, f\"{c}: ์ซ์๊ฐ ์๋/๋๋ฝ\")\n",
" d[c] = conv\n",
" for c, (lo, hi) in RANGE.items():\n",
" for i in d.index[(d[c] < lo) | (d[c] > hi)]:\n",
" bad.setdefault(i, f\"{c}: ํ์ฉ ๋ฒ์({lo}~{hi}) ๋ฐ\")\n",
" for i in d.index[d[\"country\"].isin(COUNTRIES) == False]:\n",
" bad.setdefault(i, \"country: ํ์ฉ๋์ง ์์ ๊ฐ\")\n",
" for i in d.index[d[\"part_no\"].duplicated(keep=\"first\")]:\n",
" bad.setdefault(i, \"part_no: ์ค๋ณต\")\n",
" for i in d.index[~d[\"part_no\"].astype(str).str.match(r\"^P-\\d{4}$\")]:\n",
" bad.setdefault(i, \"part_no: ํ์ ์ค๋ฅ\")\n",
" if len(bad) == len(d):\n",
" raise UserError(\"E11\", \"์ฌ์ฉํ ์ ์๋ ํ์ด ์์ต๋๋ค.\", \"๊ฒฉ๋ฆฌ ์ฌ์ ๋ฅผ ํ์ธํด ๊ฐ์ ๊ณ ์น ๋ค ๋ค์ ์ฌ๋ ค ์ฃผ์ธ์.\")\n",
" return d.drop(index=list(bad)), bad\n",
"\n",
"\n",
"def risk_points(d):\n",
" rate = d[\"late_orders\"] / d[\"orders_12m\"]\n",
" cover = d[\"stock_qty\"] / d[\"annual_demand\"] * 365 - d[\"lead_time_days\"]\n",
" return (30 * (d[\"n_suppliers\"] == 1) + 25 * np.clip(rate, 0, 1) + 20 * (d[\"eol_status\"] != \"์ ์\") + 15 * (cover < 0) + 10 * (d[\"country\"] != \"๊ตญ๋ด\")).round(1)\n",
"\n",
"\n",
"def dashboard(df, catalog, draw=True):\n",
" d, bad = validate_table(read_input(df))\n",
" d = d.assign(risk=risk_points(d))\n",
" heat = d.pivot_table(index=\"country\", columns=\"category\", values=\"risk\", aggfunc=\"mean\").round(1)\n",
" over = d[d[\"country\"] != \"๊ตญ๋ด\"].sort_values(\"risk\", ascending=False).head(6)\n",
" vec = TfidfVectorizer(token_pattern=r\"[^ ]+\").fit(list(d[\"desc\"]) + list(catalog[\"desc\"]))\n",
" alt = []\n",
" for _, p in over.iterrows():\n",
" cs = catalog[catalog[\"category\"] == p[\"category\"]]\n",
" if cs.empty:\n",
" alt.append((p[\"part_no\"], \"ํ๋ณด ์์\", 0.0)); continue\n",
" fit = np.array([spec_fit(p, c)[0] for _, c in cs.iterrows()])\n",
" sc = 0.45 * fit + 0.35 * cosine_similarity(vec.transform([p[\"desc\"]]), vec.transform(cs[\"desc\"]))[0]\n",
" j = int(np.argmax(sc)); alt.append((p[\"part_no\"], cs.iloc[j][\"cand_no\"], float(fit[j])))\n",
" if draw:\n",
" fig, ax = plt.subplots(1, 3, figsize=(13, 4))\n",
" im = ax[0].imshow(heat.fillna(0).to_numpy(), cmap=\"Reds\"); ax[0].set_xticks(range(heat.shape[1]), [f\"c{i + 1}\" for i in range(heat.shape[1])]); ax[0].set_yticks(range(heat.shape[0]), [\"A\", \"B\", \"C\", \"domestic\"][:heat.shape[0]])\n",
" ax[0].set_title(\"Supply risk map (country x category)\"); plt.colorbar(im, ax=ax[0])\n",
" ax[1].scatter(d[\"lead_time_days\"], d[\"late_orders\"] / d[\"orders_12m\"], c=d[\"risk\"], cmap=\"Reds\", s=25); ax[1].set_xlabel(\"lead time (days)\"); ax[1].set_ylabel(\"late order rate\"); ax[1].set_title(\"Lead time vs delay\")\n",
" ax[2].barh([a[0] for a in alt], [a[2] for a in alt], color=\"tab:blue\"); ax[2].invert_yaxis(); ax[2].set_xlim(0, 1); ax[2].set_title(\"Alternative spec fit (top-6 risk)\")\n",
" plt.tight_layout(); plt.show()\n",
" return heat, alt, bad\n",
"\n",
"\n",
"def safe_dashboard(case, src, catalog):\n",
" \"\"\"๋ชจ๋ ์์ธ๋ฅผ ์ก์ ํฌ๋์ ์์ด ์ฌ์ฉ์ ๋ฉ์์งยท์กฐ์น ์๋ด๋ฅผ ๋๋ ค์ฃผ๊ณ ์๋ฌ ๋ก๊ทธ๋ฅผ ๋จ๊ธด๋ค.\"\"\"\n",
" try:\n",
" heat, alt, bad = dashboard(src, catalog, draw=False)\n",
" for i, why in bad.items():\n",
" log(\"WARN\", \"W01\", f\"{case}: ํ {i} ๊ฒฉ๋ฆฌ - {why}\")\n",
" return {\"case\": case, \"ok\": True, \"msg\": f\"ํ์ ์๋ฃ(๊ฒฉ๋ฆฌ {len(bad)}ํ)\" if bad else \"ํ์ ์๋ฃ\", \"action\": \"-\", \"quarantined\": len(bad)}\n",
" except UserError as e:\n",
" log(\"ERROR\", e.code, f\"{case}: {e.msg}\")\n",
" return {\"case\": case, \"ok\": False, \"msg\": e.msg, \"action\": e.action, \"quarantined\": 0}\n",
" except Exception as e: # ์์ํ์ง ๋ชปํ ์ค๋ฅ๋ ํฌ๋์ ๋์ ์๋ด๋ก ๋ณํ\n",
" log(\"ERROR\", \"E99\", f\"{case}: {type(e).__name__} {e}\")\n",
" return {\"case\": case, \"ok\": False, \"msg\": \"์์ํ์ง ๋ชปํ ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค.\", \"action\": \"์ค๋ฅ ์ฝ๋ E99์ ์
๋ ฅ ํ์ผ์ ๋ด๋น์์๊ฒ ์ ๋ฌํด ์ฃผ์ธ์.\", \"quarantined\": 0}\n",
"\n",
"\n",
"parts, catalog = make_parts(100, seed=7)\n",
"base = parts.copy()\n",
"csv_ok = base.to_csv(index=False)\n",
"\n",
"def mut(**kw):\n",
" d = base.copy()\n",
" for k, v in kw.items():\n",
" d[k] = d[k].astype(object); d.loc[d.index[:3], k] = v\n",
" return d\n",
"\n",
"cases = [\n",
" (\"์ ์ ์
๋ ฅ\", base), (\"์ ์ CSV ๋ฌธ์์ด\", csv_ok), (\"์
๋ ฅ ์์(None)\", None), (\"๋น ๋ฌธ์์ด\", \" \"), (\"๊นจ์ง JSON\", '[{\"part_no\": \"P-0001\", '),\n",
" (\"์ง์ํ์ง ์๋ ํ์(์ ์)\", 12345), (\"๋จธ๋ฆฌ๊ธ๋ง ์๋ CSV\", \",\".join(base.columns) + \"\\n\"), (\"ํ์ ์ด ๋๋ฝ\", base.drop(columns=[\"lead_time_days\", \"desc\"])),\n",
" (\"์ซ์ ์ด์ ๋ฌธ์\", mut(lead_time_days=\"๋ฏธ์ \")), (\"์์ ๋ฆฌ๋ํ์\", mut(lead_time_days=-5)), (\"๊ณต๊ธ์ ์ ๊ฒฐ์ธก\", mut(n_suppliers=np.nan)),\n",
" (\"ํ์ฉ๋์ง ์์ ๊ตญ๊ฐ\", mut(country=\"๋ฏธ์\")), (\"๋ถํ๋ฒํธ ์ค๋ณตยทํ์ ์ค๋ฅ\", mut(part_no=\"P-9\")), (\"์ฐ๊ฐ ์์๋ 0(๋๋์
์ํ)\", mut(annual_demand=0)),\n",
" (\"๊ทน๋จ๊ฐ(1e12)\", mut(stock_qty=1e12)), (\"์ ๋ถ ๋ถ๋ ํ\", base.assign(lead_time_days=-1)),\n",
"]\n",
"res = [safe_dashboard(n, s, catalog) for n, s in cases]\n",
"print(f\"[์๋ฌ ํธ๋ค๋ฌ ์ํ] ์
๋ ฅ {len(res)}์ข
์คํ\")\n",
"for r in res:\n",
" print(f\" {'OK ' if r['ok'] else '์๋ด'} {r['case']:22s} -> {r['msg']}\" + (f\" | ์กฐ์น: {r['action']}\" if r[\"action\"] != \"-\" else \"\"))\n",
"crash = 0 # safe_dashboard ๋ฐ์ผ๋ก ์์ธ๊ฐ ์ ๋๊ฐ ๊ฑด์(์์์ ๋ชจ๋ ํฌ์ฐฉ)\n",
"n_err = sum(not r[\"ok\"] for r in res)\n",
"logged_err = sum(l[\"level\"] == \"ERROR\" for l in LOG)\n",
"print(f\"\\nํฌ๋์ {crash}๊ฑด(๋ชฉํ 0๊ฑด: {'๋ฌ์ฑ' if crash == 0 else '๋ฏธ๋ฌ'}), ์ค๋ฅ {n_err}๊ฑด ์ค ๋ก๊ทธ ๊ธฐ๋ก {logged_err}๊ฑด -> ๊ธฐ๋ก๋ฅ {logged_err / max(n_err, 1):.0%}(๋ชฉํ 100%: {'๋ฌ์ฑ' if logged_err == n_err else '๋ฏธ๋ฌ'})\")\n",
"print(f\"ํ ๋จ์ ๊ฒฉ๋ฆฌ ๊ฒฝ๊ณ {sum(l['level'] == 'WARN' for l in LOG)}๊ฑด ๊ธฐ๋ก, ์๋ด ๋ฌธ๊ตฌ์ ์กฐ์น ๊ฐ์ด๋ ํฌํจ {sum(r['action'] != '-' for r in res if not r['ok'])}/{n_err}๊ฑด\")\n",
"print(\"\\n[์๋ฌ ๋ก๊ทธ ์ 5๊ฑด]\")\n",
"print(pd.DataFrame(LOG).head(5).to_string(index=False))\n",
"\n",
"# ์ ์ ์
๋ ฅ์ผ๋ก ๋์๋ณด๋ ๊ทธ๋ฆฌ๊ธฐ\n",
"heat, alt, bad = dashboard(base, catalog)\n",
"print(\"\\n[๊ณต๊ธ๋ง ์ง๋] ๊ตญ๊ฐ x ๋ฒ์ฃผ ํ๊ท ์ํ ์ ์(์์ ์ํ ๊ตญ๊ฐ ํ)\")\n",
"print(heat.loc[[c for c in COUNTRIES if c in heat.index]].fillna(0).round(0).astype(int).to_string())\n",
"print(\"\\n[๋์ฒด์ ์ถ์ฒ ํจ๋] ์ํ ์์ ํด์ธ ๋ถํ๋ณ ๊ตญ์ฐ ํ๋ณด์ ๊ท๊ฒฉ ์ ํฉ์ฑ\")\n",
"for a in alt:\n",
" print(f\" {a[0]} -> {a[1]} (๊ท๊ฒฉ ์ ํฉ์ฑ {a[2]:.2f})\")\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"๐ก [์ฐธ๊ณ ] ๋ฏธ์
06 ์์ ์คํ ๊ฒฐ๊ณผ (Output)
\n",
"\n",
"```text\n",
"[์๋ฌ ํธ๋ค๋ฌ ์ํ] ์
๋ ฅ 16์ข
์คํ\n",
" OK ์ ์ ์
๋ ฅ -> ํ์ ์๋ฃ\n",
" OK ์ ์ CSV ๋ฌธ์์ด -> ํ์ ์๋ฃ\n",
" ์๋ด ์
๋ ฅ ์์(None) -> ์
๋ ฅ์ด ๋น์ด ์์ต๋๋ค. | ์กฐ์น: ํ์ผ์ ๋ค์ ์ ํํด ์ฃผ์ธ์.\n",
" ์๋ด ๋น ๋ฌธ์์ด -> ๋น ํ์ผ์
๋๋ค. | ์กฐ์น: ๋ด์ฉ์ด ์๋ ํ์ผ์ธ์ง ํ์ธํด ์ฃผ์ธ์.\n",
" ์๋ด ๊นจ์ง JSON -> ํ์ผ ํ์์ ์ฝ์ ์ ์์ต๋๋ค. | ์กฐ์น: CSV(์ผํ ๊ตฌ๋ถ) ๋๋ JSON(๋ชฉ๋ก) ํ์์ผ๋ก ์ ์ฅํ๋์ง ํ์ธํด ์ฃผ์ธ์.\n",
" ์๋ด ์ง์ํ์ง ์๋ ํ์(์ ์) -> ์ง์ํ์ง ์๋ ์
๋ ฅ ํ์์
๋๋ค(int). | ์กฐ์น: CSV ๋๋ JSON ํ์ผ์ ์ฌ์ฉํด ์ฃผ์ธ์.\n",
" ์๋ด ๋จธ๋ฆฌ๊ธ๋ง ์๋ CSV -> ์ฝ์ ํ์ด 0๊ฑด์
๋๋ค. | ์กฐ์น: ๋จธ๋ฆฌ๊ธ๋ง ์๋ ํ์ผ์ด ์๋์ง ํ์ธํด ์ฃผ์ธ์.\n",
" ์๋ด ํ์ ์ด ๋๋ฝ -> ํ์ ์ด์ด ์์ต๋๋ค: lead_time_days, desc | ์กฐ์น: ์ด ์ด๋ฆ์ด ์์(๋ฐ์ดํฐ ๋์
๋๋ฆฌ)๊ณผ ๊ฐ์์ง ํ์ธํด ์ฃผ์ธ์.\n",
" OK ์ซ์ ์ด์ ๋ฌธ์ -> ํ์ ์๋ฃ(๊ฒฉ๋ฆฌ 3ํ)\n",
" OK ์์ ๋ฆฌ๋ํ์ -> ํ์ ์๋ฃ(๊ฒฉ๋ฆฌ 3ํ)\n",
" OK ๊ณต๊ธ์ ์ ๊ฒฐ์ธก -> ํ์ ์๋ฃ(๊ฒฉ๋ฆฌ 3ํ)\n",
" OK ํ์ฉ๋์ง ์์ ๊ตญ๊ฐ -> ํ์ ์๋ฃ(๊ฒฉ๋ฆฌ 3ํ)\n",
" OK ๋ถํ๋ฒํธ ์ค๋ณตยทํ์ ์ค๋ฅ -> ํ์ ์๋ฃ(๊ฒฉ๋ฆฌ 3ํ)\n",
" OK ์ฐ๊ฐ ์์๋ 0(๋๋์
์ํ) -> ํ์ ์๋ฃ(๊ฒฉ๋ฆฌ 3ํ)\n",
" OK ๊ทน๋จ๊ฐ(1e12) -> ํ์ ์๋ฃ(๊ฒฉ๋ฆฌ 3ํ)\n",
" ์๋ด ์ ๋ถ ๋ถ๋ ํ -> ์ฌ์ฉํ ์ ์๋ ํ์ด ์์ต๋๋ค. | ์กฐ์น: ๊ฒฉ๋ฆฌ ์ฌ์ ๋ฅผ ํ์ธํด ๊ฐ์ ๊ณ ์น ๋ค ๋ค์ ์ฌ๋ ค ์ฃผ์ธ์.\n",
"\n",
"ํฌ๋์ 0๊ฑด(๋ชฉํ 0๊ฑด: ๋ฌ์ฑ), ์ค๋ฅ 7๊ฑด ์ค ๋ก๊ทธ ๊ธฐ๋ก 7๊ฑด -> ๊ธฐ๋ก๋ฅ 100%(๋ชฉํ 100%: ๋ฌ์ฑ)\n",
"ํ ๋จ์ ๊ฒฉ๋ฆฌ ๊ฒฝ๊ณ 21๊ฑด ๊ธฐ๋ก, ์๋ด ๋ฌธ๊ตฌ์ ์กฐ์น ๊ฐ์ด๋ ํฌํจ 7/7๊ฑด\n",
"\n",
"[์๋ฌ ๋ก๊ทธ ์ 5๊ฑด]\n",
" seq level code detail\n",
" 1 ERROR E01 ์
๋ ฅ ์์(None): ์
๋ ฅ์ด ๋น์ด ์์ต๋๋ค.\n",
" 2 ERROR E02 ๋น ๋ฌธ์์ด: ๋น ํ์ผ์
๋๋ค.\n",
" 3 ERROR E03 ๊นจ์ง JSON: ํ์ผ ํ์์ ์ฝ์ ์ ์์ต๋๋ค.\n",
" 4 ERROR E04 ์ง์ํ์ง ์๋ ํ์(์ ์): ์ง์ํ์ง ์๋ ์
๋ ฅ ํ์์
๋๋ค(int).\n",
" 5 ERROR E02 ๋จธ๋ฆฌ๊ธ๋ง ์๋ CSV: ์ฝ์ ํ์ด 0๊ฑด์
๋๋ค.\n",
"\n",
"[๊ณต๊ธ๋ง ์ง๋] ๊ตญ๊ฐ x ๋ฒ์ฃผ ํ๊ท ์ํ ์ ์(์์ ์ํ ๊ตญ๊ฐ ํ)\n",
"category RF์ฆํญ๊ธฐ ๊ดํ๋ ์ฆ ๋ฒ ์ด๋ง ์ ์๋ฐธ๋ธ ์์ด๋ก์ผ์ ์ ์๋ชจ๋ ์ ์ด๋ณด๋ ์ปค๋ฅํฐ\n",
"country \n",
"๊ฐ์๊ตญ๊ฐA 39 58 48 41 44 36 46 69\n",
"๊ฐ์๊ตญ๊ฐB 42 49 75 54 51 68 57 30\n",
"๊ฐ์๊ตญ๊ฐC 14 63 39 0 16 0 71 29\n",
"๊ตญ๋ด 0 19 4 38 28 2 28 42\n",
"\n",
"[๋์ฒด์ ์ถ์ฒ ํจ๋] ์ํ ์์ ํด์ธ ๋ถํ๋ณ ๊ตญ์ฐ ํ๋ณด์ ๊ท๊ฒฉ ์ ํฉ์ฑ\n",
" P-0080 -> D-0205 (๊ท๊ฒฉ ์ ํฉ์ฑ 1.00)\n",
" P-0066 -> D-0172 (๊ท๊ฒฉ ์ ํฉ์ฑ 1.00)\n",
"... (์ดํ ์๋ต)\n",
"```\n",
" \n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## ๐ฏ ๋ฏธ์
07. ์ถ์ฒ๋ ๋์ฒด ๋ถํ์ ๊ท๊ฒฉ ์ ํฉ์ฑ ๊ฒํ ๋ฐ ์กฐ๋ฌ ์น์ธ์ ์ํ HITL ์ํฌํ๋ก์ฐ ์ค๊ณ\n",
"**์์์๊ฐ**: ์ฝ 3.0์๊ฐ\n",
"\n",
"### ๐ ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ์ํ ์ง์นจ\n",
"#### ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ๋น์์ฑ\n",
"\n",
"์ถ์ฒ๋ ๋์ฒด ๋ถํ์ ๊ท๊ฒฉ ์ ํฉ์ฑ ๊ฒํ ๋ฐ ์กฐ๋ฌ ์น์ธ HITL ์ํฌํ๋ก์ฐ๋ฅผ ์ค๊ณํ๋ค. ์ค์ ์ ์ ํ์ฅ์์๋ ๋จ์ ํ์ง์ ๊ทธ์น์ง ์๊ณ ์ ์ํ ์ํ ํ๋จ๊ณผ ์ ๋ขฐ๋ ๋์ ์์ฌ๊ฒฐ์ ์ด ํ์์ ์ด๋ฏ๋ก, **์ถ์ฒ๋ ๋์ฒด ๋ถํ์ ๊ท๊ฒฉ ์ ํฉ์ฑ ๊ฒํ ์ ์กฐ๋ฌ ์น์ธ์ ์ํ HITL ์ํฌํ๋ก์ฐ๋ฅผ ์ค๊ณํ๊ณ ์ด๋ ฅ ๋ฌด๊ฒฐ์ฑ๊ณผ ๊ถํ ํต์ ๋ฅผ ๊ฒ์ฆํ๋ค.** ๊ณผ์
์ ์์ํ์ฌ ์คํ์ ๋ฐฉ์งํ๊ณ ์์ ๋์๋ ฅ์ ๊ทน๋ํํด์ผ ํฉ๋๋ค.\n",
"\n",
"์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ณธ ๋ฏธ์
์์๋ **์์คํ
์ ์, ์กฐ๋ฌ๋ด๋น ์์ , ๊ธฐ์ ๊ฒํ ๊ด ๊ท๊ฒฉ ๊ฒํ , ๋ณด์๋ด๋น ๊ฒํ , ์น์ธ๊ถ์ ๊ฒฐ์ฌ์ ์์๋ฅผ ์ํ ๊ธฐ๊ณ๋ก ๊ตฌํํ๊ณ ์ญํ ๋ณ ๊ถํ์ ์ต์๋ก ๋๋ค ๊ณผ์ ์ ์งํํ๋** ์ค์ต์ ์ฒด๊ณ์ ์ผ๋ก ์ํํฉ๋๋ค.\n",
"\n",
"ํ์ต์๋ ์ค์ต๊ฐ์ด๋์ ์ ์๋ ์ด 4๊ฐ ์ธ๋ถ ํ๋(๊ธฐ๋ณธ ๊ธฐ๋ฅ ๊ตฌํ โ ๊ฒฐ๊ณผ ์งํ ๊ฒ์ฆ โ ํ๋ผ๋ฏธํฐ ํ๋ ๋ฐ ๋์ ๊ณผ์ )์ ์์ฐจ์ ์ผ๋ก ์งํํ๋ฉฐ ํต์ฌ ํ์ ๊ธฐ์ค์ ๋ฌ์ฑํฉ๋๋ค.\n",
"\n",
"**์ด๋ฒ ๊ฒฐ๊ณผ๋ฌผ:** `src/workflow/prc07_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": [
"\"\"\"์ํ ์์ ๋ถํ์ ๋์ฒด ๋ถํ ์ถ์ฒ ๊ฒฐ๊ณผ๋ฅผ ์กฐ๋ฌ๋ด๋น ์์ -> ๊ธฐ์ ๊ฒํ ๊ด ๊ท๊ฒฉ ๊ฒํ -> ๋ณด์๋ด๋น ๊ฒํ -> ์น์ธ๊ถ์ ๊ฒฐ์ฌ์ HITL ์ํฌํ๋ก์ฐ๋ก ์ฒ๋ฆฌํ๋, ์์คํ
์ ์๋ ํ์ ๊ณผ ๊ถํ ์ฐํ ์๋๋ฅผ ์ฐจ๋จํ๊ณ ๋ชจ๋ ํ์๋ฅผ ํด์ ์ฒด์ธ ๊ฐ์ฌ ๋ก๊ทธ์ ๋จ๊ฒจ ์ด๋ ฅ ๋ฌด๊ฒฐ์ฑ๊ณผ ์๋ณ์กฐ ํ์ง๋ฅผ ๊ฒ์ฆํ๋ค.\"\"\"\n",
"import hashlib\n",
"import json\n",
"import numpy as np\n",
"import pandas as pd\n",
"import matplotlib.pyplot as plt\n",
"from sklearn.feature_extraction.text import TfidfVectorizer\n",
"from sklearn.metrics.pairwise import cosine_similarity\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# [๊ต์ก์ฉ๊ฐ์์๋ฃ] ๊ฐ์ ๋ฌด๊ธฐ์ฒด๊ณ ๋ถํยท๊ณต๊ธ๋ง ๋ฐ์ดํฐ ์์ฑ๊ธฐ (์ค์ ์ฒด๊ณยท์
์ฒดยท๊ตญ๊ฐยท๋จ๊ฐ ์ ๋ณด ์์)\n",
"# ๋ถํ 100์ข
(ํด์ธ ์กฐ๋ฌ ์ฝ 80%)์ ๊ณต๊ธ๋ง ์์ฑ๊ณผ, ๊ตญ๋ด ๋ถํ ์นดํ๋ก๊ทธ(๊ณผ๊ฑฐ ์น์ธ๋ ๊ตญ์ฐ ๋์ฒดํ +\n",
"# ์ค๋ช
์ ๋น์ทํ์ง๋ง ๊ท๊ฒฉ์ด ๋ง์ง ์๋ ์ ์ฌ ํ๋ณด)๋ฅผ ํจ๊ป ๋ง๋ ๋ค.\n",
"# ---------------------------------------------------------------------------\n",
"COUNTRIES = [\"๊ฐ์๊ตญ๊ฐA\", \"๊ฐ์๊ตญ๊ฐB\", \"๊ฐ์๊ตญ๊ฐC\", \"๊ตญ๋ด\"]\n",
"CATS = {\n",
" \"์ ์๋ชจ๋\": dict(words=[\"DC-DC\", \"์ปจ๋ฒํฐ\", \"์ ์ฐํ\", \"์ ์\", \"์ ์ถ๋ ฅ\"], syn={\"์ปจ๋ฒํฐ\": \"๋ณํ์ฅ์น\", \"์ ์ฐํ\": \"์์ด์๋ ์ด์
\", \"์ ์ถ๋ ฅ\": \"์์ ํ\"}, std=\"MIL-STD-704\", iface=\"๋จ์๋\"),\n",
" \"์ปค๋ฅํฐ\": dict(words=[\"์ํ\", \"์ปค๋ฅํฐ\", \"๋ฐฉ์ํ\", \"๋คํ\", \"๋์ฌ์ฒด๊ฒฐ\"], syn={\"์ปค๋ฅํฐ\": \"์ ์์\", \"๋ฐฉ์ํ\": \"๋ฐํํ\", \"๋์ฌ์ฒด๊ฒฐ\": \"์คํฌ๋ฃจ๊ฒฐํฉ\"}, std=\"MIL-DTL-38999\", iface=\"์ํํ\"),\n",
" \"์ ์๋ฐธ๋ธ\": dict(words=[\"์ ์\", \"์๋ ๋
ธ์ด๋\", \"๋ฐธ๋ธ\", \"๋ฐฉํฅ์ ์ด\", \"4ํฌํธ\"], syn={\"๋ฐธ๋ธ\": \"๊ฐํ์ฅ์น\", \"๋ฐฉํฅ์ ์ด\": \"์ ๋ก์ ํ\", \"์๋ ๋
ธ์ด๋\": \"์ ์์\"}, std=\"SAE-AS5440\", iface=\"ํ๋์ง\"),\n",
" \"๋ฒ ์ด๋ง\": dict(words=[\"๋ณผ\", \"๋ฒ ์ด๋ง\", \"๊ณ ์\", \"๋ฐ๋ดํ\", \"ํ์ ์ถ\"], syn={\"๋ฒ ์ด๋ง\": \"์ถ๋ฐ์ด\", \"๋ฐ๋ดํ\": \"์ค๋ํ\", \"๊ณ ์\": \"๊ณ ํ์ \"}, std=\"ISO-492\", iface=\"์ถ๊ฒฐํฉ\"),\n",
" \"๊ดํ๋ ์ฆ\": dict(words=[\"์ ์ธ์ \", \"๋ ์ฆ\", \"๊ฒ๋ฅด๋ง๋\", \"๋ฐ์ฌ๋ฐฉ์ง\", \"์ง๊ด\"], syn={\"๋ ์ฆ\": \"๊ดํ์ฐฝ\", \"์ง๊ด\": \"์ด์ ํ์ฑ\", \"๋ฐ์ฌ๋ฐฉ์ง\": \"AR์ฝํ
\"}, std=\"MIL-PRF-13830\", iface=\"๋ง์ดํธ\"),\n",
" \"์์ด๋ก์ผ์\": dict(words=[\"๊ด์ฌ์ \", \"์์ด๋ก\", \"๊ฐ์๋\", \"์ผ์\", \"๊ด์ฑ\"], syn={\"์ผ์\": \"๊ฐ์ง๊ธฐ\", \"๊ฐ์๋\": \"ํ์ ์จ\", \"๊ด์ฑ\": \"INS์ฉ\"}, std=\"IEEE-952\", iface=\"์ง๋ ฌํต์ \"),\n",
" \"RF์ฆํญ๊ธฐ\": dict(words=[\"RF\", \"์ ๋ ฅ์ฆํญ๊ธฐ\", \"๊ด๋์ญ\", \"๋ชจ๋\", \"์ก์ \"], syn={\"์ ๋ ฅ์ฆํญ๊ธฐ\": \"ํ์์ฐํ\", \"์ก์ \": \"์ถ๋ ฅ๋จ\", \"๊ด๋์ญ\": \"์์ด๋๋ฐด๋\"}, std=\"MIL-STD-461\", iface=\"SMA\"),\n",
" \"์ ์ด๋ณด๋\": dict(words=[\"FPGA\", \"์ ์ด๋ณด๋\", \"์๋ฒ ๋๋\", \"์ฐ์ฐ\", \"์
์ถ๋ ฅ\"], syn={\"์ ์ด๋ณด๋\": \"์ปจํธ๋กค์นด๋\", \"์ฐ์ฐ\": \"์ ํธ์ฒ๋ฆฌ\", \"์๋ฒ ๋๋\": \"๋ด์ฅํ\"}, std=\"VITA-46\", iface=\"๋ฐฑํ๋ ์ธ\"),\n",
"}\n",
"V_OPTS, W_OPTS = [12, 24, 28], [25, 50, 100]\n",
"VENDORS = [\"๊ฐ์์ ๋ฐ(์ฃผ)\", \"๊ฐ์์ ์(์ฃผ)\", \"๊ฐ์์ ๊ณต์(์ฃผ)\", \"๊ฐ์๊ดํ(์ฃผ)\", \"๊ฐ์ํ
ํฌ(์ฃผ)\", \"๊ฐ์์์คํ
(์ฃผ)\"]\n",
"\n",
"\n",
"def describe(cat, words, s):\n",
" return f\"{cat} {' '.join(words)} {s['v']}V {s['w']}W {s['tmin']}~{s['tmax']}C {s['std']}\"\n",
"\n",
"\n",
"def _sig(x):\n",
" return 1 / (1 + np.exp(-x))\n",
"\n",
"\n",
"def make_parts(n=100, seed=7):\n",
" \"\"\"๋ถํ ๋ชฉ๋ก(๊ณต๊ธ๋ง ์์ฑ ํฌํจ)๊ณผ ๊ตญ๋ด ์นดํ๋ก๊ทธ๋ฅผ ๋ง๋ ๋ค. ๋ฐํ: (parts DataFrame, catalog DataFrame)\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" cats, rows, cat_rows = list(CATS), [], []\n",
" for i in range(n):\n",
" cat = cats[i % len(cats)]; c = CATS[cat]\n",
" spec = {\"v\": int(rng.choice(V_OPTS)), \"w\": int(rng.choice(W_OPTS)), \"tmin\": int(rng.choice([-55, -40])),\n",
" \"tmax\": int(rng.choice([85, 125])), \"size_mm\": float(round(rng.uniform(20, 120), 1)), \"std\": c[\"std\"]}\n",
" words = [str(x) for x in rng.permutation(c[\"words\"])[:4]]\n",
" country = str(rng.choice(COUNTRIES, p=[0.35, 0.3, 0.15, 0.2]))\n",
" overseas = country != \"๊ตญ๋ด\"\n",
" n_sup = int(rng.choice([1, 2, 3], p=[0.45, 0.35, 0.2])) if overseas else int(rng.choice([1, 2, 3]))\n",
" lead = float(round(rng.gamma(3, 45 if overseas else 25) + 20))\n",
" eol = str(rng.choice([\"์ ์\", \"๋จ์ข
์๊ณ \", \"๋จ์ข
\"], p=[0.7, 0.2, 0.1]))\n",
" crit = str(rng.choice([\"A\", \"B\", \"C\"], p=[0.3, 0.4, 0.3]))\n",
" z = (-2.6 + 1.1 * (n_sup == 1) + 0.9 * overseas + 0.006 * (lead - 100) + 1.2 * (eol == \"๋จ์ข
์๊ณ \")\n",
" + 2.0 * (eol == \"๋จ์ข
\") + rng.normal(0, 0.6))\n",
" p_late = float(np.clip(_sig(z) * 0.8 + 0.05, 0, 0.9))\n",
" late = int(rng.binomial(12, p_late))\n",
" demand = int(rng.integers(20, 400))\n",
" rows.append({\"part_no\": f\"P-{i + 1:04d}\", \"category\": cat, \"desc\": describe(cat, words, spec), **spec,\n",
" \"iface\": c[\"iface\"], \"country\": country, \"overseas\": overseas, \"n_suppliers\": n_sup,\n",
" \"lead_time_days\": lead, \"orders_12m\": 12, \"late_orders\": late,\n",
" \"avg_delay_days\": float(round(late and rng.gamma(2, 12 + 20 * p_late), 1)),\n",
" \"eol_status\": eol, \"ltb_months\": int(rng.integers(1, 25)) if eol == \"๋จ์ข
์๊ณ \" else None,\n",
" \"price_change_pct\": float(round(rng.normal(4 + 12 * p_late, 8), 1)), \"criticality\": crit,\n",
" \"annual_demand\": demand, \"stock_qty\": int(rng.integers(0, demand)),\n",
" \"defect_rate\": float(round(abs(rng.normal(0.8 + 2 * p_late, 0.8)), 2)),\n",
" \"export_license\": bool(overseas and rng.random() < 0.35),\n",
" \"disrupted_next12m\": bool(rng.random() < _sig(z + 0.4))})\n",
" if not overseas:\n",
" continue\n",
" # ๊ณผ๊ฑฐ ์น์ธ๋ ๊ตญ์ฐ ๋์ฒดํ(์ ๋ต): ๋์์ด๋ก ์์ , ๊ท๊ฒฉ ํธํ(์ ๋ ฅ ์ฌ์ ํ์ฉ, ์ผ๋ถ๋ ํฌ๊ธฐ ์ฐจ์ด๊ฐ ์ปค ์ฅ์ฐฉ ๊ฒํ ํ์)\n",
" eq_words = [c[\"syn\"].get(w, w) for w in words]\n",
" hard = rng.random() < 0.15 # ์์ ์ด ๋ง์ด ๋ค๋ฅธ ์ฌ๋ก\n",
" if hard:\n",
" eq_words = [c[\"syn\"].get(w, w) for w in c[\"words\"] if w not in words][:1] + eq_words[1:3]\n",
" eq = {**spec, \"w\": spec[\"w\"] if rng.random() < 0.7 else W_OPTS[min(W_OPTS.index(spec[\"w\"]) + 1, 2)],\n",
" \"size_mm\": float(round(spec[\"size_mm\"] * (rng.uniform(1.11, 1.15) if rng.random() < 0.12 else rng.uniform(0.95, 1.05)), 1))}\n",
" cat_rows.append({\"cand_no\": f\"D-{len(cat_rows) + 1:04d}\", \"category\": cat, \"desc\": describe(cat, eq_words, eq), **eq,\n",
" \"iface\": c[\"iface\"], \"vendor\": str(rng.choice(VENDORS)), \"approved_for\": f\"P-{i + 1:04d}\", \"kind\": \"equivalent\"})\n",
" # ์ ์ฌ ์ค๋ต ํ๋ณด: ์ค๋ช
์ ๊ฑฐ์ ๊ฐ์ง๋ง ์ ์์ด ๋ค๋ฅด๊ฑฐ๋(5V/48V) ์์ฉ ์จ๋ ๋ฑ๊ธ(0~70C)\n",
" for kind in (\"volt_mismatch\", \"temp_mismatch\"):\n",
" d = dict(spec)\n",
" if kind == \"volt_mismatch\":\n",
" d[\"v\"] = int(rng.choice([5, 48])) # ์ฒด๊ณ์์ ์ฐ์ง ์๋ ์ ์\n",
" else:\n",
" d[\"tmin\"], d[\"tmax\"] = 0, 70\n",
" d[\"size_mm\"] = float(round(spec[\"size_mm\"] * rng.uniform(0.95, 1.05), 1))\n",
" cat_rows.append({\"cand_no\": f\"D-{len(cat_rows) + 1:04d}\", \"category\": cat, \"desc\": describe(cat, words, d), **d,\n",
" \"iface\": c[\"iface\"], \"vendor\": str(rng.choice(VENDORS)), \"approved_for\": None, \"kind\": kind})\n",
" parts = pd.DataFrame(rows)\n",
" catalog = pd.DataFrame(cat_rows).sample(frac=1, random_state=seed).reset_index(drop=True)\n",
" return parts, catalog\n",
"# ---------------------------------------------------------------------------\n",
"\n",
"# ---- ๋์ฒด ๋ถํ ์ถ์ฒ ๊ณตํต ํจ์: ํ
์คํธ ์ ์ฌ๋ + ๊ท๊ฒฉ ์ ํฉ์ฑ + ์ง์ ๊ทธ๋ํ ์ด์ ์ ์ฌ๋ ----\n",
"WEIGHTS = {\"text\": 0.35, \"spec\": 0.45, \"graph\": 0.20}\n",
"\n",
"\n",
"def spec_fit(p, c):\n",
" \"\"\"๊ท๊ฒฉ ์ ํฉ์ฑ 0~1: 6๊ฐ ์กฐ๊ฑด ์ถฉ์กฑ ๋น์จ. ์ ์ยท์จ๋ ํ๊ณ(ํ์ ์กฐ๊ฑด)๋ฅผ ๋ชป ๋ง์ถ๋ฉด ์ ์๋ฅผ ์ ๋ฐ์ผ๋ก ๊น๋๋ค.\"\"\"\n",
" conds = [p[\"v\"] == c[\"v\"], c[\"w\"] >= p[\"w\"], c[\"tmin\"] <= p[\"tmin\"], c[\"tmax\"] >= p[\"tmax\"],\n",
" abs(c[\"size_mm\"] - p[\"size_mm\"]) / p[\"size_mm\"] <= 0.10, p[\"iface\"] == c[\"iface\"]]\n",
" s = sum(conds) / len(conds)\n",
" return (s * 0.5 if not (conds[0] and conds[2] and conds[3]) else s), conds\n",
"\n",
"\n",
"def graph_nodes(row):\n",
" \"\"\"์ง์ ๊ทธ๋ํ์์ ๋ถํ ๋
ธ๋์ ์ฐ๊ฒฐ๋ ์ด์ ๋
ธ๋ ์งํฉ(๋ฒ์ฃผยทํ์คยท์ธํฐํ์ด์คยท๊ธฐ๋ฅ์ดยท๋ฑ๊ธ).\"\"\"\n",
" words = set(row[\"desc\"].split()[1:-4])\n",
" grade = \"๊ตฐ์ฉ๋ฑ๊ธ\" if row[\"tmin\"] <= -40 else \"์์ฉ๋ฑ๊ธ\"\n",
" return {f\"cat:{row['category']}\", f\"std:{row['std']}\", f\"if:{row['iface']}\", f\"grade:{grade}\", f\"v:{row['v']}\"} | {f\"w:{w}\" for w in words}\n",
"\n",
"\n",
"def build_graph(catalog):\n",
" \"\"\"๋์์ด ์ฌ์ ์ ๊ทธ๋ํ ๊ฐ์ (๊ธฐ๋ฅ์ด = ๋์์ด)์ผ๋ก ๋ฃ์ด ์์ ์ด ๋ฌ๋ผ๋ ๊ฐ์ ๊ธฐ๋ฅ ๋
ธ๋๋ก ์ฐ๊ฒฐ๋๊ฒ ํ๋ค.\"\"\"\n",
" syn = {f\"w:{b}\": f\"w:{a}\" for c in CATS.values() for a, b in c[\"syn\"].items()}\n",
" canon = lambda nodes: {syn.get(x, x) for x in nodes}\n",
" return {\"canon\": canon, \"nodes\": [canon(graph_nodes(r)) for _, r in catalog.iterrows()]}\n",
"\n",
"\n",
"def graph_sim(g, pn, cn):\n",
" \"\"\"๋ ๋ถํ์ด ๊ณต์ ํ๋ ์ด์ ๋
ธ๋ ๋น์จ(Jaccard).\"\"\"\n",
" pn = g[\"canon\"](pn)\n",
" return len(pn & cn) / max(len(pn | cn), 1)\n",
"\n",
"\n",
"def score_table(parts_df, catalog):\n",
" \"\"\"ํด์ธ ๋ถํ x ๊ฐ์ ๋ฒ์ฃผ ๊ตญ๋ด ํ๋ณด ์๋ง๋ค ํ
์คํธยท๊ท๊ฒฉยท๊ทธ๋ํ ์ ์๋ฅผ ๊ณ์ฐํ ํ(๊ฐ์ค์น ์ ์ฉ ์ ).\"\"\"\n",
" vec = TfidfVectorizer(token_pattern=r\"[^ ]+\").fit(list(parts_df[\"desc\"]) + list(catalog[\"desc\"]))\n",
" text = cosine_similarity(vec.transform(parts_df[\"desc\"]), vec.transform(catalog[\"desc\"]))\n",
" g = build_graph(catalog)\n",
" rows = []\n",
" for i, (_, p) in enumerate(parts_df.iterrows()):\n",
" pn = graph_nodes(p)\n",
" for j, (_, c) in enumerate(catalog.iterrows()):\n",
" if c[\"category\"] != p[\"category\"]:\n",
" continue\n",
" sf, conds = spec_fit(p, c)\n",
" rows.append({\"part_no\": p[\"part_no\"], \"cand_no\": c[\"cand_no\"], \"vendor\": c[\"vendor\"], \"text\": text[i, j],\n",
" \"spec\": sf, \"graph\": graph_sim(g, pn, g[\"nodes\"][j]),\n",
" \"unmet\": \",\".join(n for n, ok in zip([\"์ ์\", \"์ ๋ ฅ\", \"์ ์จ\", \"๊ณ ์จ\", \"ํฌ๊ธฐ\", \"์ธํฐํ์ด์ค\"], conds) if not ok) or \"-\",\n",
" \"kind\": c[\"kind\"], \"correct\": c[\"approved_for\"] == p[\"part_no\"]})\n",
" return pd.DataFrame(rows)\n",
"\n",
"\n",
"def recommend(table, weights=None, top=3):\n",
" \"\"\"๊ฐ์คํฉ ์ ์๋ก ๋ถํ๋ณ ์์ top๊ฐ ํ๋ณด๋ฅผ ๊ณ ๋ฅธ๋ค.\"\"\"\n",
" w = weights or WEIGHTS\n",
" t = table.assign(score=w[\"text\"] * table[\"text\"] + w[\"spec\"] * table[\"spec\"] + w[\"graph\"] * table[\"graph\"])\n",
" return t.sort_values([\"part_no\", \"score\"], ascending=[True, False]).groupby(\"part_no\").head(top)\n",
"\n",
"\n",
"def hit_rate(recs, k=1):\n",
" \"\"\"๊ณผ๊ฑฐ ์น์ธ ์ด๋ ฅ(์ ๋ต ๋์ฒดํ)์ด ์์ k๊ฐ ์์ ๋ค์ด๊ฐ ๋ถํ ๋น์จ.\"\"\"\n",
" return recs.groupby(\"part_no\").apply(lambda d: d[\"correct\"].head(k).any(), include_groups=False).mean()\n",
"\n",
"# ---- ์ญํ ๊ณผ ๊ถํ(์ต์ ๊ถํ ์์น): ์์คํ
์ '์ ์'๋ง ํ ์ ์๊ณ ํ์ ๊ถํ์ด ์๋ค ----\n",
"PERM = {\"์์คํ
\": {\"propose\"}, \"์กฐ๋ฌ๋ด๋น\": {\"submit\", \"view\"}, \"๊ธฐ์ ๊ฒํ ๊ด\": {\"review_spec\", \"view\"}, \"๋ณด์๋ด๋น\": {\"review_sec\", \"view\"},\n",
" \"์น์ธ๊ถ์\": {\"approve\", \"view\"}, \"๊ฐ์ฌ๊ด\": {\"view\", \"audit\"},\n",
" \"๋ถ์์ฅ\": {\"submit\", \"approve\", \"view\"}} # ์๊ท๋ชจ ๋ถ์๋ ์์ ๊ณผ ๊ฒฐ์ฌ๋ฅผ ๊ฒธํ ์ ์์ผ๋ ๊ฐ์ ๊ฑด์ ๋ถ๋ฆฌ\n",
"NEXT = {\"์ ์\": (\"submit\", \"์์ \"), \"์์ \": (\"review_spec\", \"๊ท๊ฒฉ๊ฒํ \"), \"๊ท๊ฒฉ๊ฒํ \": (\"review_sec\", \"๋ณด์๊ฒํ \"), \"๋ณด์๊ฒํ \": (\"approve\", \"์น์ธ\")}\n",
"USERS = {\"sys\": \"์์คํ
\", \"kim\": \"์กฐ๋ฌ๋ด๋น\", \"lee\": \"๊ธฐ์ ๊ฒํ ๊ด\", \"park\": \"๋ณด์๋ด๋น\", \"choi\": \"์น์ธ๊ถ์\", \"jung\": \"๊ฐ์ฌ๊ด\", \"oh\": \"๋ถ์์ฅ\"}\n",
"FLAGGED_VENDORS = {\"๊ฐ์์์คํ
(์ฃผ)\"} # [๊ต์ก์ฉ๊ฐ์์๋ฃ] ๋ณด์ ๊ฒํ ์์ ์ถ๊ฐ ํ์ธ์ด ํ์ํ ์
์ฒด ํ๊ธฐ\n",
"\n",
"\n",
"class Chain:\n",
" \"\"\"ํด์ ์ฒด์ธ ๊ฐ์ฌ ๋ก๊ทธ: ๊ฐ ๊ธฐ๋ก์ด ์ง์ ๊ธฐ๋ก์ ํด์๋ฅผ ํฌํจํด ์ค๊ฐ ์์ ์ด ๋๋ฌ๋๋ค.\"\"\"\n",
"\n",
" def __init__(self):\n",
" self.log = []\n",
"\n",
" def add(self, **body):\n",
" prev = self.log[-1][\"hash\"] if self.log else \"GENESIS\"\n",
" body = {\"seq\": len(self.log), **body}\n",
" self.log.append({**body, \"prev\": prev, \"hash\": hashlib.sha256((prev + json.dumps(body, sort_keys=True, ensure_ascii=False)).encode()).hexdigest()})\n",
"\n",
" def verify(self):\n",
" prev = \"GENESIS\"\n",
" for e in self.log:\n",
" b = {k: v for k, v in e.items() if k not in (\"prev\", \"hash\")}\n",
" if e[\"prev\"] != prev or hashlib.sha256((prev + json.dumps(b, sort_keys=True, ensure_ascii=False)).encode()).hexdigest() != e[\"hash\"]:\n",
" return e[\"seq\"]\n",
" prev = e[\"hash\"]\n",
" return None\n",
"\n",
"\n",
"class Workflow:\n",
" def __init__(self):\n",
" self.audit, self.cases, self.blocked = Chain(), {}, []\n",
"\n",
" def _deny(self, who, case, action, why):\n",
" self.audit.add(event=\"DENIED\", who=who, case=case, action=action, reason=why)\n",
" self.blocked.append((who, case, action, why)); return False, why\n",
"\n",
" def act(self, who, case, action, reason=\"\", evidence=\"\"):\n",
" role = USERS.get(who)\n",
" if role is None:\n",
" return self._deny(who, case, action, \"๋ฏธ๋ฑ๋ก ์ฌ์ฉ์\")\n",
" if action == \"delete_log\":\n",
" return self._deny(who, case, action, \"๊ฐ์ฌ ๋ก๊ทธ ์ญ์ ยท์์ ๊ถํ์ ๋๊ตฌ์๊ฒ๋ ์์\")\n",
" if action not in PERM[role]:\n",
" return self._deny(who, case, action, f\"๊ถํ ์์({role} ์ญํ ์ {action} ๊ถํ ์์)\")\n",
" if action == \"propose\":\n",
" self.cases[case] = {\"state\": \"์ ์\", \"submitter\": None}\n",
" self.audit.add(event=\"PROPOSE\", who=who, case=case, reason=reason); return True, \"์ ์ ๋ฑ๋ก\"\n",
" c = self.cases.get(case)\n",
" if c is None:\n",
" return self._deny(who, case, action, \"์กด์ฌํ์ง ์๋ ๊ฑด\")\n",
" if c[\"state\"] in (\"์น์ธ\", \"๋ฐ๋ ค\"):\n",
" return self._deny(who, case, action, f\"์ด๋ฏธ ์ข
๊ฒฐ๋ ๊ฑด({c['state']})\")\n",
" need, nxt = NEXT[c[\"state\"]]\n",
" if action != need:\n",
" return self._deny(who, case, action, f\"๋จ๊ณ ์๋ฐ(ํ์ฌ {c['state']}, ํ์ ํ์ {need})\")\n",
" if not reason.strip() or not evidence.strip():\n",
" return self._deny(who, case, action, \"์ฌ์ ยท๊ทผ๊ฑฐ ์
๋ ฅ ํ์\")\n",
" if action == \"submit\":\n",
" c[\"submitter\"] = who\n",
" if action == \"approve\" and who == c[\"submitter\"]:\n",
" return self._deny(who, case, action, \"์์ ์์ ์น์ธ์ ๋ถ๋ฆฌ ์์น ์๋ฐ\")\n",
" c[\"state\"] = nxt\n",
" self.audit.add(event=action.upper(), who=who, role=role, case=case, to=nxt, reason=reason, evidence=evidence); return True, nxt\n",
"\n",
" def reject(self, who, case, reason, evidence):\n",
" \"\"\"๋ฐ๋ ค: ๊ฒํ ์ยท์น์ธ๊ถ์๊ฐ ์ด๋ ๋จ๊ณ์์๋ ์ฌ์ ยท๊ทผ๊ฑฐ์ ํจ๊ป ํ ์ ์๋ค.\"\"\"\n",
" c = self.cases[case]\n",
" role = USERS.get(who)\n",
" if role not in (\"์น์ธ๊ถ์\", \"๊ธฐ์ ๊ฒํ ๊ด\", \"๋ณด์๋ด๋น\", \"๋ถ์์ฅ\") or c[\"state\"] in (\"์น์ธ\", \"๋ฐ๋ ค\"):\n",
" return self._deny(who, case, \"reject\", \"๋ฐ๋ ค ๊ถํ ์์ ๋๋ ์ข
๊ฒฐ๋ ๊ฑด\")\n",
" if not reason.strip() or not evidence.strip():\n",
" return self._deny(who, case, \"reject\", \"์ฌ์ ยท๊ทผ๊ฑฐ ์
๋ ฅ ํ์\")\n",
" c[\"state\"] = \"๋ฐ๋ ค\"\n",
" self.audit.add(event=\"REJECTED\", who=who, role=role, case=case, reason=reason, evidence=evidence); return True, \"๋ฐ๋ ค\"\n",
"\n",
"\n",
"# ---- ์์ ๋์: ์ํ ์์ ํด์ธ ๋ถํ๊ณผ ์ถ์ฒ 1์์ ํ๋ณด ----\n",
"parts, catalog = make_parts(100, seed=7)\n",
"over = parts[parts[\"overseas\"]].reset_index(drop=True)\n",
"table = score_table(over, catalog)\n",
"top = recommend(table).groupby(\"part_no\").head(1).set_index(\"part_no\")\n",
"risk = (30 * (over[\"n_suppliers\"] == 1) + 25 * (over[\"late_orders\"] / 12) + 20 * (over[\"eol_status\"] != \"์ ์\") + 10 * over[\"criticality\"].eq(\"A\")).to_numpy()\n",
"picked = over.assign(risk=risk).sort_values(\"risk\", ascending=False).head(12)\n",
"rng = np.random.default_rng(4)\n",
"\n",
"wf = Workflow()\n",
"rows = []\n",
"for k, (_, p) in enumerate(picked.iterrows(), 1):\n",
" case, r = f\"C{k:02d}\", top.loc[p[\"part_no\"]]\n",
" wf.act(\"sys\", case, \"propose\", reason=f\"{p['part_no']} ์ํ ์ ์ {p['risk']:.0f}, ์ถ์ฒ {r['cand_no']}\")\n",
" wf.act(\"kim\", case, \"submit\", \"๋จ์ข
ยท๋จ์ผ ๊ณต๊ธ์ ์ํ ๋ถํ ๋์ฒด ๊ฒํ ์์ฒญ\", f\"์ํ ์ ์ {p['risk']:.0f}์ , ํ๋ณด ์ ์ {r['score']:.2f}\")\n",
" hard = [u for u in r[\"unmet\"].split(\",\") if u in (\"์ ์\", \"์ ์จ\", \"๊ณ ์จ\")]\n",
" if rng.random() < 0.15:\n",
" rows.append((case, p[\"part_no\"], r[\"cand_no\"], \"๋ฌด์๋ต -> ๋ณด๋ฅ\", \"์๋ ํ์ ์์: ๊ธฐ์ ๊ฒํ ๊ด ์๋ต ๋๊ธฐ\")); continue\n",
" if hard:\n",
" wf.reject(\"lee\", case, f\"ํ์ ๊ท๊ฒฉ ๋ฏธ์ถฉ์กฑ({','.join(hard)})\", f\"๊ท๊ฒฉ ์ ํฉ์ฑ {r['spec']:.2f}\")\n",
" rows.append((case, p[\"part_no\"], r[\"cand_no\"], wf.cases[case][\"state\"], f\"๊ท๊ฒฉ ๋ฐ๋ ค: {','.join(hard)}\")); continue\n",
" wf.act(\"lee\", case, \"review_spec\", \"ํ์ ๊ท๊ฒฉ(์ ์ยท์จ๋) ์ถฉ์กฑ ํ์ธ\" + (f\", ๋ฏธ์ถฉ์กฑ {r['unmet']}๋ ์ํ ์กฐ๊ฑด๋ถ\" if r[\"unmet\"] != \"-\" else \"\"), f\"๊ท๊ฒฉ ์ ํฉ์ฑ {r['spec']:.2f}\")\n",
" if r[\"vendor\"] in FLAGGED_VENDORS:\n",
" wf.reject(\"park\", case, \"์
์ฒด ๋ณด์ ํ์ธ ๋ฏธ์๋ฃ\", f\"์
์ฒด {r['vendor']} ์ถ๊ฐ ํ์ธ ํ์\")\n",
" rows.append((case, p[\"part_no\"], r[\"cand_no\"], wf.cases[case][\"state\"], \"๋ณด์ ๋ฐ๋ ค: ์
์ฒด ํ์ธ ํ์\")); continue\n",
" wf.act(\"park\", case, \"review_sec\", \"์
์ฒดยท๊ณต๊ธ ๊ฒฝ๋ก ๋ณด์ ํ์ธ\", f\"์
์ฒด {r['vendor']} ํ์ธ ์๋ฃ(๊ฐ์)\")\n",
" ok = r[\"score\"] >= 0.80 and p[\"risk\"] >= 40\n",
" if ok:\n",
" wf.act(\"choi\", case, \"approve\", \"์น์ธ: ์ํ ๋ถํ ๊ตญ์ฐํ ์ํ ์ฐฉ์\", f\"ํ๋ณด ์ ์ {r['score']:.2f}, ์ํ {p['risk']:.0f}\")\n",
" else:\n",
" wf.reject(\"choi\", case, \"๋ฐ๋ ค: ์ถ์ฒ ์ ์ ๋๋ ์ํ๋ ๊ธฐ์ค ๋ฏธ๋ฌ(์ถ๊ฐ ์ํ ํ ์ฌ์์ )\", f\"ํ๋ณด ์ ์ {r['score']:.2f}, ์ํ {p['risk']:.0f}\")\n",
" rows.append((case, p[\"part_no\"], r[\"cand_no\"], wf.cases[case][\"state\"], \"์น์ธ๊ถ์ ๊ฒฐ์ \" if ok else \"์ ์ ๋ฏธ๋ฌ ๋ฐ๋ ค\"))\n",
"print(\"[๊ฒฐ์ฌ ๊ฒฐ๊ณผ] 12๊ฑด\")\n",
"print(pd.DataFrame(rows, columns=[\"๊ฑด\", \"๋ถํ\", \"ํ๋ณด\", \"์ต์ข
์ํ\", \"๋น๊ณ \"]).to_string(index=False))\n",
"st = pd.Series([r[3] for r in rows]).value_counts().to_dict()\n",
"print(\"์ํ ์ง๊ณ:\", st, \"(๋ชจ์ ๊ฒฐ์ฌ์ ๊ธฐ์ค, ์ค์ ๊ฒฐ์ฌ๋ ์ฌ๋์ด ํ๋ค)\")\n",
"\n",
"# ---- ๊ถํ ์ฐํยท์ ์ฐจ ์๋ฐ ์๋ ----\n",
"wf.act(\"sys\", \"T01\", \"propose\", reason=\"์ํ ๊ฑด\")\n",
"wf.act(\"kim\", \"T01\", \"submit\", \"์ํ ์์ \", \"๊ทผ๊ฑฐ\")\n",
"attempts = [\n",
" (\"์์คํ
์๋ ์น์ธ\", lambda: wf.act(\"sys\", \"T01\", \"approve\", \"์๋\", \"์๋\")),\n",
" (\"์กฐ๋ฌ๋ด๋น์ด ๊ท๊ฒฉ ๊ฒํ ๋ํ\", lambda: wf.act(\"kim\", \"T01\", \"review_spec\", \"๋ํ\", \"๊ทผ๊ฑฐ\")),\n",
" (\"๊ฒํ ๋จ๊ณ ๊ฑด๋๋ฐ๊ณ ์น์ธ\", lambda: wf.act(\"choi\", \"T01\", \"approve\", \"๋ฐ๋ก ์น์ธ\", \"๊ทผ๊ฑฐ\")),\n",
" (\"๋ฏธ๋ฑ๋ก ์ฌ์ฉ์ ์ ๊ทผ\", lambda: wf.act(\"guest\", \"T01\", \"review_spec\", \"์ด๋\", \"๊ทผ๊ฑฐ\")),\n",
" (\"์ฌ์ ์๋ ๊ฒํ \", lambda: wf.act(\"lee\", \"T01\", \"review_spec\", \"\", \"\")),\n",
" (\"๊ฐ์ฌ ๋ก๊ทธ ์ญ์ \", lambda: wf.act(\"kim\", \"T01\", \"delete_log\", \"์ ๋ฆฌ\", \"๊ทผ๊ฑฐ\")),\n",
" (\"๊ฐ์ฌ๊ด์ด ์น์ธ ์๋\", lambda: wf.act(\"jung\", \"T01\", \"approve\", \"์น์ธ\", \"๊ทผ๊ฑฐ\")),\n",
" (\"์ข
๊ฒฐ๋ ๊ฑด ์ฌ์น์ธ\", lambda: wf.act(\"choi\", \"C01\", \"approve\", \"์ฌ์น์ธ\", \"๊ทผ๊ฑฐ\")),\n",
"]\n",
"wf.act(\"sys\", \"T02\", \"propose\", reason=\"์ํ\"); wf.act(\"oh\", \"T02\", \"submit\", \"์์ \", \"๊ทผ๊ฑฐ\")\n",
"wf.act(\"lee\", \"T02\", \"review_spec\", \"๊ท๊ฒฉ ํ์ธ\", \"์ ํฉ์ฑ 1.00\"); wf.act(\"park\", \"T02\", \"review_sec\", \"๋ณด์ ํ์ธ\", \"ํ์ธ ์๋ฃ\")\n",
"attempts.append((\"์์ ์๊ฐ ๋ณธ์ธ ๊ฑด ์น์ธ(๋ถ๋ฆฌ ์์น)\", lambda: wf.act(\"oh\", \"T02\", \"approve\", \"๋ณธ์ธ ์น์ธ\", \"๊ทผ๊ฑฐ\")))\n",
"print(\"\\n[๊ถํ ์ฐํยท์ ์ฐจ ์๋ฐ ์๋]\")\n",
"blocked = 0\n",
"for name, fn in attempts:\n",
" ok, why = fn()\n",
" blocked += (not ok)\n",
" print(f\" {name:22s}-> {'์ฐจ๋จ' if not ok else 'ํต๊ณผ(๊ฒฐํจ)'}: {why}\")\n",
"print(f\"์ฐํยท์๋ฐ ์๋ ์ฐจ๋จ {blocked}/{len(attempts)} ({blocked / len(attempts):.0%}) (๋ชฉํ 100%: {'๋ฌ์ฑ' if blocked == len(attempts) else '๋ฏธ๋ฌ'})\")\n",
"\n",
"# ---- ์ด๋ ฅ ๋ฌด๊ฒฐ์ฑ ----\n",
"bad = wf.audit.verify()\n",
"n_cases_logged = sum(1 for e in wf.audit.log if e[\"event\"] == \"PROPOSE\")\n",
"print(f\"\\n๊ฐ์ฌ ๋ก๊ทธ {len(wf.audit.log)}๊ฑด, ํด์ ์ฒด์ธ ๊ฒ์ฆ: {'์ ์' if bad is None else f'seq {bad}์์ ๋ถ์ผ์น'} -> ์ด๋ ฅ ๋ฌด๊ฒฐ์ฑ {'100%' if bad is None else 'ํผ์'}(๋ชฉํ 100%: {'๋ฌ์ฑ' if bad is None else '๋ฏธ๋ฌ'})\")\n",
"saved = json.dumps(wf.audit.log[10], ensure_ascii=False)\n",
"wf.audit.log[10][\"reason\"] = \"์ฌํ ์์ \"\n",
"print(f\"10๋ฒ์งธ ๊ธฐ๋ก ์์ ์์ ํ ๊ฒ์ฆ: {'์๋ณ์กฐ ํ์ง(seq ' + str(wf.audit.verify()) + ')' if wf.audit.verify() is not None else 'ํ์ง ์คํจ'}\")\n",
"wf.audit.log[10] = json.loads(saved)\n",
"print(\"์น์ธ๋ ๊ฑด์ ๋ง์ง๋ง ๊ธฐ๋ก ์์:\", {k: v for k, v in next((e for e in reversed(wf.audit.log) if e[\"event\"] == \"APPROVE\" and e[\"case\"].startswith(\"C\")), {}).items() if k in (\"seq\", \"who\", \"case\", \"reason\", \"evidence\")})\n",
"print(\"โป ๋์ฒด ๋ถํ์ ๊ท๊ฒฉ ์ ํฉ์ฑ ์ต์ข
ํ๋จ๊ณผ ์กฐ๋ฌ ์น์ธ์ ์ฌ๋(๊ธฐ์ ๊ฒํ ๊ดยท๋ณด์๋ด๋นยท์น์ธ๊ถ์)์ด ํ๋ฉฐ, ์์คํ
์ ์ถ์ฒ๊ณผ ๊ทผ๊ฑฐ๋ง ์ ๊ณตํ๋ค.\")\n",
"\n",
"plt.figure(figsize=(6, 3.5)); EN = {\"์น์ธ\": \"approved\", \"๋ฐ๋ ค\": \"rejected\", \"๋ฌด์๋ต -> ๋ณด๋ฅ\": \"on hold\"}; plt.bar([EN.get(k, k) for k in st], list(st.values()), color=\"tab:blue\"); plt.title(\"HITL workflow outcomes (12 cases)\"); plt.ylabel(\"cases\"); plt.tight_layout(); plt.show()\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"๐ก [์ฐธ๊ณ ] ๋ฏธ์
07 ์์ ์คํ ๊ฒฐ๊ณผ (Output)
\n",
"\n",
"```text\n",
"[๊ฒฐ์ฌ ๊ฒฐ๊ณผ] 12๊ฑด\n",
" ๊ฑด ๋ถํ ํ๋ณด ์ต์ข
์ํ ๋น๊ณ \n",
"C01 P-0028 D-0046 ๋ฐ๋ ค ์ ์ ๋ฏธ๋ฌ ๋ฐ๋ ค\n",
"C02 P-0056 D-0058 ์น์ธ ์น์ธ๊ถ์ ๊ฒฐ์ \n",
"C03 P-0087 D-0223 ์น์ธ ์น์ธ๊ถ์ ๊ฒฐ์ \n",
"C04 P-0036 D-0094 ๋ฌด์๋ต -> ๋ณด๋ฅ ์๋ ํ์ ์์: ๊ธฐ์ ๊ฒํ ๊ด ์๋ต ๋๊ธฐ\n",
"C05 P-0066 D-0172 ์น์ธ ์น์ธ๊ถ์ ๊ฒฐ์ \n",
"C06 P-0080 D-0205 ์น์ธ ์น์ธ๊ถ์ ๊ฒฐ์ \n",
"C07 P-0077 D-0049 ๋ฐ๋ ค ์ ์ ๋ฏธ๋ฌ ๋ฐ๋ ค\n",
"C08 P-0045 D-0115 ์น์ธ ์น์ธ๊ถ์ ๊ฒฐ์ \n",
"C09 P-0003 D-0004 ์น์ธ ์น์ธ๊ถ์ ๊ฒฐ์ \n",
"C10 P-0062 D-0160 ๋ฐ๋ ค ๋ณด์ ๋ฐ๋ ค: ์
์ฒด ํ์ธ ํ์\n",
"C11 P-0053 D-0136 ์น์ธ ์น์ธ๊ถ์ ๊ฒฐ์ \n",
"C12 P-0012 D-0025 ์น์ธ ์น์ธ๊ถ์ ๊ฒฐ์ \n",
"์ํ ์ง๊ณ: {'์น์ธ': 8, '๋ฐ๋ ค': 3, '๋ฌด์๋ต -> ๋ณด๋ฅ': 1} (๋ชจ์ ๊ฒฐ์ฌ์ ๊ธฐ์ค, ์ค์ ๊ฒฐ์ฌ๋ ์ฌ๋์ด ํ๋ค)\n",
"\n",
"[๊ถํ ์ฐํยท์ ์ฐจ ์๋ฐ ์๋]\n",
" ์์คํ
์๋ ์น์ธ -> ์ฐจ๋จ: ๊ถํ ์์(์์คํ
์ญํ ์ approve ๊ถํ ์์)\n",
" ์กฐ๋ฌ๋ด๋น์ด ๊ท๊ฒฉ ๊ฒํ ๋ํ -> ์ฐจ๋จ: ๊ถํ ์์(์กฐ๋ฌ๋ด๋น ์ญํ ์ review_spec ๊ถํ ์์)\n",
" ๊ฒํ ๋จ๊ณ ๊ฑด๋๋ฐ๊ณ ์น์ธ -> ์ฐจ๋จ: ๋จ๊ณ ์๋ฐ(ํ์ฌ ์์ , ํ์ ํ์ review_spec)\n",
" ๋ฏธ๋ฑ๋ก ์ฌ์ฉ์ ์ ๊ทผ -> ์ฐจ๋จ: ๋ฏธ๋ฑ๋ก ์ฌ์ฉ์\n",
" ์ฌ์ ์๋ ๊ฒํ -> ์ฐจ๋จ: ์ฌ์ ยท๊ทผ๊ฑฐ ์
๋ ฅ ํ์\n",
" ๊ฐ์ฌ ๋ก๊ทธ ์ญ์ -> ์ฐจ๋จ: ๊ฐ์ฌ ๋ก๊ทธ ์ญ์ ยท์์ ๊ถํ์ ๋๊ตฌ์๊ฒ๋ ์์\n",
" ๊ฐ์ฌ๊ด์ด ์น์ธ ์๋ -> ์ฐจ๋จ: ๊ถํ ์์(๊ฐ์ฌ๊ด ์ญํ ์ approve ๊ถํ ์์)\n",
" ์ข
๊ฒฐ๋ ๊ฑด ์ฌ์น์ธ -> ์ฐจ๋จ: ์ด๋ฏธ ์ข
๊ฒฐ๋ ๊ฑด(๋ฐ๋ ค)\n",
" ์์ ์๊ฐ ๋ณธ์ธ ๊ฑด ์น์ธ(๋ถ๋ฆฌ ์์น) -> ์ฐจ๋จ: ์์ ์์ ์น์ธ์ ๋ถ๋ฆฌ ์์น ์๋ฐ\n",
"์ฐํยท์๋ฐ ์๋ ์ฐจ๋จ 9/9 (100%) (๋ชฉํ 100%: ๋ฌ์ฑ)\n",
"\n",
"๊ฐ์ฌ ๋ก๊ทธ 71๊ฑด, ํด์ ์ฒด์ธ ๊ฒ์ฆ: ์ ์ -> ์ด๋ ฅ ๋ฌด๊ฒฐ์ฑ 100%(๋ชฉํ 100%: ๋ฌ์ฑ)\n",
"10๋ฒ์งธ ๊ธฐ๋ก ์์ ์์ ํ ๊ฒ์ฆ: ์๋ณ์กฐ ํ์ง(seq 10)\n",
"์น์ธ๋ ๊ฑด์ ๋ง์ง๋ง ๊ธฐ๋ก ์์: {'seq': 55, 'who': 'choi', 'case': 'C12', 'reason': '์น์ธ: ์ํ ๋ถํ ๊ตญ์ฐํ ์ํ ์ฐฉ์', 'evidence': 'ํ๋ณด ์ ์ 0.84, ์ํ 62'}\n",
"โป ๋์ฒด ๋ถํ์ ๊ท๊ฒฉ ์ ํฉ์ฑ ์ต์ข
ํ๋จ๊ณผ ์กฐ๋ฌ ์น์ธ์ ์ฌ๋(๊ธฐ์ ๊ฒํ ๊ดยท๋ณด์๋ด๋นยท์น์ธ๊ถ์)์ด ํ๋ฉฐ, ์์คํ
์ ์ถ์ฒ๊ณผ ๊ทผ๊ฑฐ๋ง ์ ๊ณตํ๋ค.\n",
"```\n",
" \n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## ๐ฏ ๋ฏธ์
08. ์ง์ ํ์ ์๊ธฐ ๋ชจ์ ๊ธ๋ก๋ฒ ๊ณต๊ธ๋ง ์ ๋ฉด ์ฐจ๋จ ์คํธ๋ ์ค ์๋ฎฌ๋ ์ด์
\n",
"**์์์๊ฐ**: ์ฝ 3.5์๊ฐ\n",
"\n",
"### ๐ ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ์ํ ์ง์นจ\n",
"#### ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ๋น์์ฑ\n",
"\n",
"๊ธ๋ก๋ฒ ๊ณต๊ธ๋ง ์ฐจ๋จ ๋ชจ์ ์คํธ๋ ์ค ์๋ฎฌ๋ ์ด์
์ ์ํํ๋ค. ์ค์ ์ ์ ํ์ฅ์์๋ ๋จ์ ํ์ง์ ๊ทธ์น์ง ์๊ณ ์ ์ํ ์ํ ํ๋จ๊ณผ ์ ๋ขฐ๋ ๋์ ์์ฌ๊ฒฐ์ ์ด ํ์์ ์ด๋ฏ๋ก, **๊ฐ์ ๊ณต๊ธ๊ตญ ์ ๋ฉด ์ฐจ๋จ์ ๋ชจ์ํ ์คํธ๋ ์ค ์๋ฎฌ๋ ์ด์
์ผ๋ก ๋ฐฐ์น ํ์ดํ๋ผ์ธ์ ์คํ ์๊ฐ, ์์ ์ฑ, ์
๋ฌด ์๊ฐ ์ ๊ฐ ํจ๊ณผ๋ฅผ ๊ฒ์ฆํ๋ค.** ๊ณผ์
์ ์์ํ์ฌ ์คํ์ ๋ฐฉ์งํ๊ณ ์์ ๋์๋ ฅ์ ๊ทน๋ํํด์ผ ํฉ๋๋ค.\n",
"\n",
"์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ณธ ๋ฏธ์
์์๋ **ํ์, ๊ฐ์๊ตญ๊ฐ A ์ฐจ๋จ, A+B ์ฐจ๋จ, ํด์ธ ์ ๋ฉด ์ฐจ๋จ 4๊ฐ ์๋๋ฆฌ์ค๋ฅผ ๋ถํ 2,000์ข
๊ท๋ชจ๋ก ์คํํด ๋จ์ ๋ถํ, ๊ตญ์ฐ ๋์ฒด ํ๋ณด ๋ณด์ , ์
์ฒด ์์ฉ๋์ ๊ณ ๋ คํ ๋ถ๊ธฐ ๋ด ์ฐฉ์ ๊ฐ๋ฅ ๊ฑด์, ํต์ฌ ๋ถํ ์ฐฉ์ ๋๊ธฐ, ์ฌ๊ณ 90์ผ ๋ฏธ๋ง ๋จ์ ์ฐ์ถํ๊ณ ๋ชจ๋ธ์ ์ ์ฉํ๋** ์ค์ต์ ์ฒด๊ณ์ ์ผ๋ก ์ํํฉ๋๋ค.\n",
"\n",
"ํ์ต์๋ ์ค์ต๊ฐ์ด๋์ ์ ์๋ ์ด 5๊ฐ ์ธ๋ถ ํ๋(๊ธฐ๋ณธ ๊ธฐ๋ฅ ๊ตฌํ โ ๊ฒฐ๊ณผ ์งํ ๊ฒ์ฆ โ ํ๋ผ๋ฏธํฐ ํ๋ ๋ฐ ๋์ ๊ณผ์ )์ ์์ฐจ์ ์ผ๋ก ์งํํ๋ฉฐ ํต์ฌ ํ์ ๊ธฐ์ค์ ๋ฌ์ฑํฉ๋๋ค.\n",
"\n",
"**์ด๋ฒ ๊ฒฐ๊ณผ๋ฌผ:** `src/pipeline/prc07_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": [
"\"\"\"๊ฐ์ ๊ณต๊ธ๊ตญ ์ ๋ฉด ์ฐจ๋จ(์ง์ ํ์ ์๊ธฐ ๋ชจ์) ์๋๋ฆฌ์ค 4์ข
์ ๋ถํ 2,000์ข
๊ท๋ชจ ๋ฐฐ์น ํ์ดํ๋ผ์ธ์ผ๋ก ์คํํ๊ณ , ๋ ์ฝ๋ ์์ 2%ยท์ผ์ ์ค๋ฅ ์ฃผ์
์ํฉ์์ ์คํ ์๊ฐ(5๋ถ ์ด๋ด)ยท์๋ํ ์ฑ๊ณต๋ฅ (99% ์ด์)ยท์์์
๋๋น ์๊ฐ ์ ๊ฐ(80% ์ด์)๊ณผ ์๊ฐ ์ด๊ณผ ์ ๊ธฐ๋ฅ ์ถ์(Fail-safe) ๋์์ ๊ฒ์ฆํ๋ค.\"\"\"\n",
"import time\n",
"import numpy as np\n",
"import pandas as pd\n",
"import matplotlib.pyplot as plt\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# [๊ต์ก์ฉ๊ฐ์์๋ฃ] ๊ฐ์ ๋ฌด๊ธฐ์ฒด๊ณ ๋ถํยท๊ณต๊ธ๋ง ๋ฐ์ดํฐ ์์ฑ๊ธฐ (์ค์ ์ฒด๊ณยท์
์ฒดยท๊ตญ๊ฐยท๋จ๊ฐ ์ ๋ณด ์์)\n",
"# ๋ถํ 100์ข
(ํด์ธ ์กฐ๋ฌ ์ฝ 80%)์ ๊ณต๊ธ๋ง ์์ฑ๊ณผ, ๊ตญ๋ด ๋ถํ ์นดํ๋ก๊ทธ(๊ณผ๊ฑฐ ์น์ธ๋ ๊ตญ์ฐ ๋์ฒดํ +\n",
"# ์ค๋ช
์ ๋น์ทํ์ง๋ง ๊ท๊ฒฉ์ด ๋ง์ง ์๋ ์ ์ฌ ํ๋ณด)๋ฅผ ํจ๊ป ๋ง๋ ๋ค.\n",
"# ---------------------------------------------------------------------------\n",
"COUNTRIES = [\"๊ฐ์๊ตญ๊ฐA\", \"๊ฐ์๊ตญ๊ฐB\", \"๊ฐ์๊ตญ๊ฐC\", \"๊ตญ๋ด\"]\n",
"CATS = {\n",
" \"์ ์๋ชจ๋\": dict(words=[\"DC-DC\", \"์ปจ๋ฒํฐ\", \"์ ์ฐํ\", \"์ ์\", \"์ ์ถ๋ ฅ\"], syn={\"์ปจ๋ฒํฐ\": \"๋ณํ์ฅ์น\", \"์ ์ฐํ\": \"์์ด์๋ ์ด์
\", \"์ ์ถ๋ ฅ\": \"์์ ํ\"}, std=\"MIL-STD-704\", iface=\"๋จ์๋\"),\n",
" \"์ปค๋ฅํฐ\": dict(words=[\"์ํ\", \"์ปค๋ฅํฐ\", \"๋ฐฉ์ํ\", \"๋คํ\", \"๋์ฌ์ฒด๊ฒฐ\"], syn={\"์ปค๋ฅํฐ\": \"์ ์์\", \"๋ฐฉ์ํ\": \"๋ฐํํ\", \"๋์ฌ์ฒด๊ฒฐ\": \"์คํฌ๋ฃจ๊ฒฐํฉ\"}, std=\"MIL-DTL-38999\", iface=\"์ํํ\"),\n",
" \"์ ์๋ฐธ๋ธ\": dict(words=[\"์ ์\", \"์๋ ๋
ธ์ด๋\", \"๋ฐธ๋ธ\", \"๋ฐฉํฅ์ ์ด\", \"4ํฌํธ\"], syn={\"๋ฐธ๋ธ\": \"๊ฐํ์ฅ์น\", \"๋ฐฉํฅ์ ์ด\": \"์ ๋ก์ ํ\", \"์๋ ๋
ธ์ด๋\": \"์ ์์\"}, std=\"SAE-AS5440\", iface=\"ํ๋์ง\"),\n",
" \"๋ฒ ์ด๋ง\": dict(words=[\"๋ณผ\", \"๋ฒ ์ด๋ง\", \"๊ณ ์\", \"๋ฐ๋ดํ\", \"ํ์ ์ถ\"], syn={\"๋ฒ ์ด๋ง\": \"์ถ๋ฐ์ด\", \"๋ฐ๋ดํ\": \"์ค๋ํ\", \"๊ณ ์\": \"๊ณ ํ์ \"}, std=\"ISO-492\", iface=\"์ถ๊ฒฐํฉ\"),\n",
" \"๊ดํ๋ ์ฆ\": dict(words=[\"์ ์ธ์ \", \"๋ ์ฆ\", \"๊ฒ๋ฅด๋ง๋\", \"๋ฐ์ฌ๋ฐฉ์ง\", \"์ง๊ด\"], syn={\"๋ ์ฆ\": \"๊ดํ์ฐฝ\", \"์ง๊ด\": \"์ด์ ํ์ฑ\", \"๋ฐ์ฌ๋ฐฉ์ง\": \"AR์ฝํ
\"}, std=\"MIL-PRF-13830\", iface=\"๋ง์ดํธ\"),\n",
" \"์์ด๋ก์ผ์\": dict(words=[\"๊ด์ฌ์ \", \"์์ด๋ก\", \"๊ฐ์๋\", \"์ผ์\", \"๊ด์ฑ\"], syn={\"์ผ์\": \"๊ฐ์ง๊ธฐ\", \"๊ฐ์๋\": \"ํ์ ์จ\", \"๊ด์ฑ\": \"INS์ฉ\"}, std=\"IEEE-952\", iface=\"์ง๋ ฌํต์ \"),\n",
" \"RF์ฆํญ๊ธฐ\": dict(words=[\"RF\", \"์ ๋ ฅ์ฆํญ๊ธฐ\", \"๊ด๋์ญ\", \"๋ชจ๋\", \"์ก์ \"], syn={\"์ ๋ ฅ์ฆํญ๊ธฐ\": \"ํ์์ฐํ\", \"์ก์ \": \"์ถ๋ ฅ๋จ\", \"๊ด๋์ญ\": \"์์ด๋๋ฐด๋\"}, std=\"MIL-STD-461\", iface=\"SMA\"),\n",
" \"์ ์ด๋ณด๋\": dict(words=[\"FPGA\", \"์ ์ด๋ณด๋\", \"์๋ฒ ๋๋\", \"์ฐ์ฐ\", \"์
์ถ๋ ฅ\"], syn={\"์ ์ด๋ณด๋\": \"์ปจํธ๋กค์นด๋\", \"์ฐ์ฐ\": \"์ ํธ์ฒ๋ฆฌ\", \"์๋ฒ ๋๋\": \"๋ด์ฅํ\"}, std=\"VITA-46\", iface=\"๋ฐฑํ๋ ์ธ\"),\n",
"}\n",
"V_OPTS, W_OPTS = [12, 24, 28], [25, 50, 100]\n",
"VENDORS = [\"๊ฐ์์ ๋ฐ(์ฃผ)\", \"๊ฐ์์ ์(์ฃผ)\", \"๊ฐ์์ ๊ณต์(์ฃผ)\", \"๊ฐ์๊ดํ(์ฃผ)\", \"๊ฐ์ํ
ํฌ(์ฃผ)\", \"๊ฐ์์์คํ
(์ฃผ)\"]\n",
"\n",
"\n",
"def describe(cat, words, s):\n",
" return f\"{cat} {' '.join(words)} {s['v']}V {s['w']}W {s['tmin']}~{s['tmax']}C {s['std']}\"\n",
"\n",
"\n",
"def _sig(x):\n",
" return 1 / (1 + np.exp(-x))\n",
"\n",
"\n",
"def make_parts(n=100, seed=7):\n",
" \"\"\"๋ถํ ๋ชฉ๋ก(๊ณต๊ธ๋ง ์์ฑ ํฌํจ)๊ณผ ๊ตญ๋ด ์นดํ๋ก๊ทธ๋ฅผ ๋ง๋ ๋ค. ๋ฐํ: (parts DataFrame, catalog DataFrame)\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" cats, rows, cat_rows = list(CATS), [], []\n",
" for i in range(n):\n",
" cat = cats[i % len(cats)]; c = CATS[cat]\n",
" spec = {\"v\": int(rng.choice(V_OPTS)), \"w\": int(rng.choice(W_OPTS)), \"tmin\": int(rng.choice([-55, -40])),\n",
" \"tmax\": int(rng.choice([85, 125])), \"size_mm\": float(round(rng.uniform(20, 120), 1)), \"std\": c[\"std\"]}\n",
" words = [str(x) for x in rng.permutation(c[\"words\"])[:4]]\n",
" country = str(rng.choice(COUNTRIES, p=[0.35, 0.3, 0.15, 0.2]))\n",
" overseas = country != \"๊ตญ๋ด\"\n",
" n_sup = int(rng.choice([1, 2, 3], p=[0.45, 0.35, 0.2])) if overseas else int(rng.choice([1, 2, 3]))\n",
" lead = float(round(rng.gamma(3, 45 if overseas else 25) + 20))\n",
" eol = str(rng.choice([\"์ ์\", \"๋จ์ข
์๊ณ \", \"๋จ์ข
\"], p=[0.7, 0.2, 0.1]))\n",
" crit = str(rng.choice([\"A\", \"B\", \"C\"], p=[0.3, 0.4, 0.3]))\n",
" z = (-2.6 + 1.1 * (n_sup == 1) + 0.9 * overseas + 0.006 * (lead - 100) + 1.2 * (eol == \"๋จ์ข
์๊ณ \")\n",
" + 2.0 * (eol == \"๋จ์ข
\") + rng.normal(0, 0.6))\n",
" p_late = float(np.clip(_sig(z) * 0.8 + 0.05, 0, 0.9))\n",
" late = int(rng.binomial(12, p_late))\n",
" demand = int(rng.integers(20, 400))\n",
" rows.append({\"part_no\": f\"P-{i + 1:04d}\", \"category\": cat, \"desc\": describe(cat, words, spec), **spec,\n",
" \"iface\": c[\"iface\"], \"country\": country, \"overseas\": overseas, \"n_suppliers\": n_sup,\n",
" \"lead_time_days\": lead, \"orders_12m\": 12, \"late_orders\": late,\n",
" \"avg_delay_days\": float(round(late and rng.gamma(2, 12 + 20 * p_late), 1)),\n",
" \"eol_status\": eol, \"ltb_months\": int(rng.integers(1, 25)) if eol == \"๋จ์ข
์๊ณ \" else None,\n",
" \"price_change_pct\": float(round(rng.normal(4 + 12 * p_late, 8), 1)), \"criticality\": crit,\n",
" \"annual_demand\": demand, \"stock_qty\": int(rng.integers(0, demand)),\n",
" \"defect_rate\": float(round(abs(rng.normal(0.8 + 2 * p_late, 0.8)), 2)),\n",
" \"export_license\": bool(overseas and rng.random() < 0.35),\n",
" \"disrupted_next12m\": bool(rng.random() < _sig(z + 0.4))})\n",
" if not overseas:\n",
" continue\n",
" # ๊ณผ๊ฑฐ ์น์ธ๋ ๊ตญ์ฐ ๋์ฒดํ(์ ๋ต): ๋์์ด๋ก ์์ , ๊ท๊ฒฉ ํธํ(์ ๋ ฅ ์ฌ์ ํ์ฉ, ์ผ๋ถ๋ ํฌ๊ธฐ ์ฐจ์ด๊ฐ ์ปค ์ฅ์ฐฉ ๊ฒํ ํ์)\n",
" eq_words = [c[\"syn\"].get(w, w) for w in words]\n",
" hard = rng.random() < 0.15 # ์์ ์ด ๋ง์ด ๋ค๋ฅธ ์ฌ๋ก\n",
" if hard:\n",
" eq_words = [c[\"syn\"].get(w, w) for w in c[\"words\"] if w not in words][:1] + eq_words[1:3]\n",
" eq = {**spec, \"w\": spec[\"w\"] if rng.random() < 0.7 else W_OPTS[min(W_OPTS.index(spec[\"w\"]) + 1, 2)],\n",
" \"size_mm\": float(round(spec[\"size_mm\"] * (rng.uniform(1.11, 1.15) if rng.random() < 0.12 else rng.uniform(0.95, 1.05)), 1))}\n",
" cat_rows.append({\"cand_no\": f\"D-{len(cat_rows) + 1:04d}\", \"category\": cat, \"desc\": describe(cat, eq_words, eq), **eq,\n",
" \"iface\": c[\"iface\"], \"vendor\": str(rng.choice(VENDORS)), \"approved_for\": f\"P-{i + 1:04d}\", \"kind\": \"equivalent\"})\n",
" # ์ ์ฌ ์ค๋ต ํ๋ณด: ์ค๋ช
์ ๊ฑฐ์ ๊ฐ์ง๋ง ์ ์์ด ๋ค๋ฅด๊ฑฐ๋(5V/48V) ์์ฉ ์จ๋ ๋ฑ๊ธ(0~70C)\n",
" for kind in (\"volt_mismatch\", \"temp_mismatch\"):\n",
" d = dict(spec)\n",
" if kind == \"volt_mismatch\":\n",
" d[\"v\"] = int(rng.choice([5, 48])) # ์ฒด๊ณ์์ ์ฐ์ง ์๋ ์ ์\n",
" else:\n",
" d[\"tmin\"], d[\"tmax\"] = 0, 70\n",
" d[\"size_mm\"] = float(round(spec[\"size_mm\"] * rng.uniform(0.95, 1.05), 1))\n",
" cat_rows.append({\"cand_no\": f\"D-{len(cat_rows) + 1:04d}\", \"category\": cat, \"desc\": describe(cat, words, d), **d,\n",
" \"iface\": c[\"iface\"], \"vendor\": str(rng.choice(VENDORS)), \"approved_for\": None, \"kind\": kind})\n",
" parts = pd.DataFrame(rows)\n",
" catalog = pd.DataFrame(cat_rows).sample(frac=1, random_state=seed).reset_index(drop=True)\n",
" return parts, catalog\n",
"# ---------------------------------------------------------------------------\n",
"\n",
"# ---- ์กฐ๋ฌ ์ํ ์คํฌ๋ฆฌ๋ ๋ฃฐ ์ค์ (YAML ํ์ ๋ฌธ์์ด: ๊ท์ ์ด ๋ฐ๋๋ฉด ์ฝ๋๊ฐ ์๋๋ผ ์ด ์ค์ ๋ง ๊ณ ์น๋ค) ----\n",
"RULES_YAML = \"\"\"\n",
"- id: R01\n",
" name: ๋จ์ผ ๊ณต๊ธ์ \n",
" field: n_suppliers\n",
" op: \"==\"\n",
" value: 1\n",
" weight: 3\n",
"- id: R02\n",
" name: ํด์ธ ์กฐ๋ฌ ์์กด\n",
" field: overseas\n",
" op: \"==\"\n",
" value: true\n",
" weight: 1\n",
"- id: R03\n",
" name: ๋ฆฌ๋ํ์ 180์ผ ์ด๊ณผ\n",
" field: lead_time_days\n",
" op: \">\"\n",
" value: 180\n",
" weight: 2\n",
"- id: R04\n",
" name: ๋ฉ๊ธฐ ์ง์ฐ๋ฅ 30% ์ด์\n",
" field: delay_rate\n",
" op: \">=\"\n",
" value: 0.3\n",
" weight: 3\n",
"- id: R05\n",
" name: ํ๊ท ์ง์ฐ 30์ผ ์ด๊ณผ\n",
" field: avg_delay_days\n",
" op: \">\"\n",
" value: 30\n",
" weight: 2\n",
"- id: R06\n",
" name: ๋จ์ข
์๊ณ \n",
" field: eol_status\n",
" op: \"==\"\n",
" value: ๋จ์ข
์๊ณ \n",
" weight: 3\n",
"- id: R07\n",
" name: ๋จ์ข
ํ์ \n",
" field: eol_status\n",
" op: \"==\"\n",
" value: ๋จ์ข
\n",
" weight: 4\n",
"- id: R08\n",
" name: ์ต์ข
๊ตฌ๋งค 6๊ฐ์ ์ด๋ด\n",
" field: ltb_months\n",
" op: \"<=\"\n",
" value: 6\n",
" weight: 3\n",
"- id: R09\n",
" name: ๋จ๊ฐ 20% ์ด์ ๊ธ๋ฑ\n",
" field: price_change_pct\n",
" op: \">=\"\n",
" value: 20\n",
" weight: 2\n",
"- id: R10\n",
" name: ํต์ฌ(A) ๋ถํ ๋จ์ผ ๊ณต๊ธ\n",
" field: crit_single\n",
" op: \"==\"\n",
" value: true\n",
" weight: 3\n",
"- id: R11\n",
" name: ์ฌ๊ณ ๊ฐ์ฉ๊ธฐ๊ฐ < ๋ฆฌ๋ํ์\n",
" field: cover_gap_days\n",
" op: \"<\"\n",
" value: 0\n",
" weight: 3\n",
"- id: R12\n",
" name: ์
๊ณ ๋ถ๋๋ฅ 2% ์ด๊ณผ\n",
" field: defect_rate\n",
" op: \">\"\n",
" value: 2.0\n",
" weight: 2\n",
"- id: R13\n",
" name: ์์ถ ํ๊ฐ ํ์\n",
" field: export_license\n",
" op: \"==\"\n",
" value: true\n",
" weight: 2\n",
"- id: R14\n",
" name: ์ฌ๊ณ ์์\n",
" field: stock_qty\n",
" op: \"==\"\n",
" value: 0\n",
" weight: 2\n",
"- id: R15\n",
" name: ๊ณต๊ธ๊ตญ ํธ์ค(30% ์ด์)\n",
" field: country_share\n",
" op: \">=\"\n",
" value: 0.3\n",
" weight: 1\n",
"- id: R16\n",
" name: ์ง์ฐ ์ด๋ ฅ ์๋ ๋จ์ผ ๊ณต๊ธ์ \n",
" field: single_late\n",
" op: \"==\"\n",
" value: true\n",
" weight: 2\n",
"\"\"\"\n",
"GRADE_CUT = {\"๊ณ ์ํ\": 16, \"์ฃผ์\": 10} # ๊ฐ์ค์น ํฉ ๊ธฐ์ค 1์ฐจ ์คํฌ๋ฆฌ๋ ๋ฑ๊ธ\n",
"\n",
"\n",
"def parse_rules(text):\n",
" \"\"\"๊ฐ์ด YAML ํ์(๋ชฉ๋ก + key: value). ์ซ์ยทtrue/falseยท๋ฐ์ดํ ๋ฌธ์์ด์ ํด์ํ๋ค.\"\"\"\n",
" rules, cur = [], None\n",
" for line in text.strip().splitlines():\n",
" if not line.strip():\n",
" continue\n",
" s = line.strip()\n",
" if s.startswith(\"- \"):\n",
" cur = {}; rules.append(cur); s = s[2:]\n",
" k, v = [x.strip() for x in s.split(\":\", 1)]\n",
" v = v.strip('\"')\n",
" if v in (\"true\", \"false\"):\n",
" v = v == \"true\"\n",
" else:\n",
" try:\n",
" v = float(v) if \".\" in v else int(v)\n",
" except ValueError:\n",
" pass\n",
" cur[k] = v\n",
" return rules\n",
"\n",
"\n",
"OPS = {\"==\": lambda a, b: a == b, \"!=\": lambda a, b: a != b, \">\": lambda a, b: a > b, \">=\": lambda a, b: a >= b,\n",
" \"<\": lambda a, b: a < b, \"<=\": lambda a, b: a <= b}\n",
"\n",
"\n",
"def derive(df):\n",
" \"\"\"๋ฃฐ์ ์ฐ๋ ํ์ ํ๋(์ง์ฐ๋ฅ , ํต์ฌ ๋จ์ผ๊ณต๊ธ, ์ฌ๊ณ ๊ฐ์ฉ๊ธฐ๊ฐ ์ฐจ์ด, ๊ณต๊ธ๊ตญ ๋น์ค ๋ฑ)๋ฅผ ๋ง๋ ๋ค.\"\"\"\n",
" d = df.copy()\n",
" d[\"delay_rate\"] = d[\"late_orders\"] / d[\"orders_12m\"].clip(lower=1)\n",
" d[\"crit_single\"] = (d[\"criticality\"] == \"A\") & (d[\"n_suppliers\"] == 1)\n",
" d[\"cover_gap_days\"] = d[\"stock_qty\"] / d[\"annual_demand\"].clip(lower=1) * 365 - d[\"lead_time_days\"]\n",
" d[\"country_share\"] = d[\"country\"].map(d[\"country\"].value_counts(normalize=True)).where(d[\"overseas\"], 0.0)\n",
" d[\"single_late\"] = (d[\"n_suppliers\"] == 1) & (d[\"late_orders\"] >= 3)\n",
" return d\n",
"\n",
"\n",
"def apply_rules(rec, rules):\n",
" \"\"\"๋ ์ฝ๋ 1๊ฑด(dict)์ ๋ฃฐ์ ์ ์ฉํด (์ํ ์ ์, ๊ฑธ๋ฆฐ ๋ฃฐ ๋ชฉ๋ก)์ ๋๋ ค์ค๋ค. ๊ฐ์ด ์์ผ๋ฉด ํด๋น ๋ฃฐ์ ๊ฑด๋๋ด๋ค.\"\"\"\n",
" hits = []\n",
" for r in rules:\n",
" v = rec.get(r[\"field\"])\n",
" if v is None or (isinstance(v, float) and np.isnan(v)):\n",
" continue\n",
" if OPS[r[\"op\"]](v, r[\"value\"]):\n",
" hits.append(r)\n",
" score = sum(r[\"weight\"] for r in hits)\n",
" grade = \"๊ณ ์ํ\" if score >= GRADE_CUT[\"๊ณ ์ํ\"] else (\"์ฃผ์\" if score >= GRADE_CUT[\"์ฃผ์\"] else \"๊ด์ฌ\")\n",
" return score, grade, [r[\"id\"] for r in hits]\n",
"\n",
"rules = parse_rules(RULES_YAML)\n",
"SCEN = {\"ํ์\": [], \"๊ฐ์๊ตญ๊ฐA ์ฐจ๋จ\": [\"๊ฐ์๊ตญ๊ฐA\"], \"๊ฐ์๊ตญ๊ฐA+B ์ฐจ๋จ\": [\"๊ฐ์๊ตญ๊ฐA\", \"๊ฐ์๊ตญ๊ฐB\"], \"ํด์ธ ์ ๋ฉด ์ฐจ๋จ\": [\"๊ฐ์๊ตญ๊ฐA\", \"๊ฐ์๊ตญ๊ฐB\", \"๊ฐ์๊ตญ๊ฐC\"]}\n",
"MANUAL_MIN = 32.0 # [๊ฐ์ ] ์์์
1๊ฑด: ์ํ ํ์ธ 12๋ถ + ๊ตญ๋ด ํ๋ณด ํ์ยท๊ท๊ฒฉ ๋์กฐ 20๋ถ\n",
"VENDOR_CAP = 70 # [๊ฐ์ ] ๊ตญ๋ด ์
์ฒด 1๊ณณ์ด ํ ๋ถ๊ธฐ์ ์ฐฉ์ํ ์ ์๋ ์ ๊ท ๊ตญ์ฐํ ๊ฐ๋ฐ ๊ฑด์\n",
"REVIEW_MIN = 4.0 # [๊ฐ์ ] ์๋ ์ฒ๋ฆฌ ํ ๋ด๋น์๊ฐ ํ์ธํ๋ ๊ฑด๋น ์๊ฐ(๊ณ ์ํยท๋จ์ ๊ฑด๋ง)\n",
"\n",
"\n",
"def fit_vec(P, C):\n",
" \"\"\"๊ท๊ฒฉ ์ ํฉ์ฑ(0~1)์ ๋ถํ x ํ๋ณด ํ๋ ฌ๋ก ํ ๋ฒ์ ๊ณ์ฐ: ์ ์ยท์ ๋ ฅยท์ ์จยท๊ณ ์จยทํฌ๊ธฐยท์ธํฐํ์ด์ค 6์กฐ๊ฑด.\"\"\"\n",
" cond = np.stack([P[\"v\"].to_numpy()[:, None] == C[\"v\"].to_numpy()[None], C[\"w\"].to_numpy()[None] >= P[\"w\"].to_numpy()[:, None],\n",
" C[\"tmin\"].to_numpy()[None] <= P[\"tmin\"].to_numpy()[:, None], C[\"tmax\"].to_numpy()[None] >= P[\"tmax\"].to_numpy()[:, None],\n",
" np.abs(C[\"size_mm\"].to_numpy()[None] - P[\"size_mm\"].to_numpy()[:, None]) / P[\"size_mm\"].to_numpy()[:, None] <= 0.10,\n",
" P[\"iface\"].to_numpy()[:, None] == C[\"iface\"].to_numpy()[None]])\n",
" s = cond.mean(axis=0)\n",
" return np.where(cond[0] & cond[2] & cond[3], s, s * 0.5)\n",
"\n",
"\n",
"def pipeline(parts, catalog, blocked, mode=\"full\", fail_p=0.0, rng=None, budget_s=None):\n",
" \"\"\"๊ฒ์ฆ -> ๋ฃฐ ์คํฌ๋ฆฌ๋ -> ์ฐจ๋จ ์ํฅ -> (full ๋ชจ๋) ๊ตญ์ฐ ๋์ฒด ํ๋ณด ํ์. ์ค๋ฅ๋ ์ฌ์๋ 3ํ, ์คํจ ๊ฑด์ ๊ฒฉ๋ฆฌํ๊ณ ๊ณ์ํ๋ค.\"\"\"\n",
" t0 = time.perf_counter()\n",
" stats = {\"ok\": 0, \"quarantine\": 0, \"retry\": 0, \"failed\": 0, \"degraded\": False}\n",
" d = derive(parts)\n",
" hit = np.zeros(len(d), bool); score = np.zeros(len(d))\n",
" recs = d.to_dict(\"records\")\n",
" for i, r in enumerate(recs):\n",
" if budget_s and time.perf_counter() - t0 > budget_s: # ์๊ฐ ์์ฐ ์ด๊ณผ: ํ๋ณด ํ์์ ์๋ตํ๊ณ ๋ฃฐ ์คํฌ๋ฆฌ๋๋ง ๊ณ์(๊ธฐ๋ฅ ์ถ์)\n",
" stats[\"degraded\"] = True; mode = \"rules_only\"\n",
" if r.get(\"lead_time_days\") is None or pd.isna(r.get(\"lead_time_days\")):\n",
" stats[\"quarantine\"] += 1; continue # ์์ ๋ ์ฝ๋๋ ๊ฒฉ๋ฆฌํ๊ณ ๊ณ์ ์งํ\n",
" for attempt in range(4):\n",
" if rng is not None and rng.random() < fail_p: # ์ผ์ ์ค๋ฅ(๋ชจ์): ์ต๋ 3ํ ์ฌ์๋\n",
" stats[\"retry\"] += 1; continue\n",
" score[i] = apply_rules(r, rules)[0]; hit[i] = r[\"country\"] in blocked; stats[\"ok\"] += 1; break\n",
" else:\n",
" stats[\"failed\"] += 1\n",
" d = d.assign(score=score, blocked=hit)\n",
" cover = pd.Series(False, index=d.index); vend = pd.Series(\"-\", index=d.index)\n",
" if mode == \"full\" and hit.any():\n",
" for cat, g in d[d[\"blocked\"]].groupby(\"category\"):\n",
" C = catalog[catalog[\"category\"] == cat]\n",
" if len(C):\n",
" f = fit_vec(g, C)\n",
" cover.loc[g.index] = (f.max(axis=1) >= 0.83) # ๊ท๊ฒฉ 5/6 ์ด์ ์ถฉ์กฑ ํ๋ณด๊ฐ ์์ผ๋ฉด '๋์ฒด ๊ฒํ ๊ฐ๋ฅ'\n",
" vend.loc[g.index] = C[\"vendor\"].to_numpy()[f.argmax(axis=1)]\n",
" d[\"alt\"], d[\"vendor\"] = cover, vend\n",
" return d, stats, time.perf_counter() - t0\n",
"\n",
"\n",
"rng = np.random.default_rng(8)\n",
"parts, catalog = make_parts(2000, seed=7)\n",
"parts.loc[rng.choice(len(parts), 40, replace=False), \"lead_time_days\"] = np.nan # ๋ ์ฝ๋ ์์ 2%\n",
"print(f\"[๊ต์ก์ฉ๊ฐ์์๋ฃ] ๊ตญ๋ด ์
์ฒด ์์ฉ๋ ๊ฐ์ : ์
์ฒด๋น ๋ถ๊ธฐ {VENDOR_CAP}๊ฑด\")\n",
"print(f\"[๊ต์ก์ฉ๊ฐ์์๋ฃ] ๋ถํ {len(parts)}์ข
, ๊ตญ๋ด ํ๋ณด {len(catalog)}๊ฑด, ์์ ๋ ์ฝ๋ {int(parts['lead_time_days'].isna().sum())}๊ฑด(2%) ์ฃผ์
, ์ผ์ ์ค๋ฅ ํ๋ฅ 3%/์๋\")\n",
"\n",
"res = []\n",
"for name, blk in SCEN.items():\n",
" d, st, sec = pipeline(parts, catalog, blk, fail_p=0.03, rng=rng)\n",
" b = d[d[\"blocked\"]].sort_values([\"criticality\", \"score\"], ascending=[True, False]) # ํต์ฌ(A) ๋จผ์ , ์ํ ์ ์ ๋์ ์์ผ๋ก ์
์ฒด ์์ฉ๋ ๋ฐฐ์ \n",
" b = b.assign(start=b[b[\"alt\"]].groupby(\"vendor\").cumcount() < VENDOR_CAP)\n",
" b[\"start\"] = b[\"start\"].fillna(False).astype(bool)\n",
" crit = b[b[\"criticality\"] == \"A\"]\n",
" days = (b[\"stock_qty\"] / b[\"annual_demand\"].clip(lower=1) * 365)\n",
" res.append({\"์๋๋ฆฌ์ค\": name, \"๋จ์ ๋ถํ\": len(b), \"๋์ฒด ํ๋ณด ์์\": int(b[\"alt\"].sum()), \"๋ถ๊ธฐ ๋ด ์ฐฉ์ ๊ฐ๋ฅ\": int(b[\"start\"].sum()), \"ํต์ฌ(A) ์ฐฉ์ ๋๊ธฐ\": int((~crit[\"start\"]).sum()),\n",
" \"์ฌ๊ณ 90์ผ ๋ฏธ๋ง ๋จ์ \": int((days < 90).sum()), \"์คํ(์ด)\": sec, \"์ฑ๊ณต\": st[\"ok\"], \"์ฌ์๋\": st[\"retry\"], \"๊ฒฉ๋ฆฌ\": st[\"quarantine\"], \"์คํจ\": st[\"failed\"]})\n",
"R = pd.DataFrame(res)\n",
"print(\"\\n[์ฐจ๋จ ์๋๋ฆฌ์ค๋ณ ์ํฅ๊ณผ ์คํ ๊ฒฐ๊ณผ]\")\n",
"print(R.round(2).to_string(index=False))\n",
"tot_ok, tot_all = R[\"์ฑ๊ณต\"].sum(), R[\"์ฑ๊ณต\"].sum() + R[\"์คํจ\"].sum()\n",
"succ = tot_ok / tot_all\n",
"total_s = R[\"์คํ(์ด)\"].sum()\n",
"print(f\"\\n์ ์ฒด ๋ฐฐ์น ์คํ ์๊ฐ {total_s:.1f}์ด (๋ชฉํ 5๋ถ ์ด๋ด: {'๋ฌ์ฑ' if total_s <= 300 else '๋ฏธ๋ฌ'})\")\n",
"print(f\"์๋ํ ์ฑ๊ณต๋ฅ {succ:.2%} = ์ฒ๋ฆฌ ์ฑ๊ณต {tot_ok}๊ฑด / ์ฒ๋ฆฌ ๋์ {tot_all}๊ฑด(์์ ๊ฒฉ๋ฆฌ {R['๊ฒฉ๋ฆฌ'].sum()}๊ฑด์ ๋ณ๋ ๋ณด๊ณ , ์ผ์ ์ค๋ฅ {R['์ฌ์๋'].sum()}ํ๋ ์ฌ์๋๋ก ๋ณต๊ตฌ) (๋ชฉํ 99% ์ด์: {'๋ฌ์ฑ' if succ >= 0.99 else '๋ฏธ๋ฌ'})\")\n",
"\n",
"# ์์์
๋๋น ์๊ฐ ์ ๊ฐ(๊ฐ์ ๋จ๊ฐ ์ฌ์ฉ)\n",
"last = R.iloc[-1]\n",
"n_proc = len(parts)\n",
"manual = n_proc * MANUAL_MIN\n",
"auto = last[\"์คํ(์ด)\"] / 60 + (last[\"๋จ์ ๋ถํ\"] + R[\"๊ฒฉ๋ฆฌ\"].iloc[-1]) * REVIEW_MIN\n",
"print(f\"์์์
๋๋น(ํด์ธ ์ ๋ฉด ์ฐจ๋จ, {n_proc}์ข
): ์์์
{manual / 60:.0f}์๊ฐ vs ์๋ {auto / 60:.0f}์๊ฐ(๋ด๋น์ ํ์ธ ํฌํจ) -> {1 - auto / manual:.0%} ์ ๊ฐ (๋ชฉํ 80% ์ด์: {'๋ฌ์ฑ' if 1 - auto / manual >= 0.8 else '๋ฏธ๋ฌ'})\")\n",
"print(f\" ๊ฐ์ : ์์์
๊ฑด๋น {MANUAL_MIN:.0f}๋ถ, ์๋ ์ฒ๋ฆฌ ํ ํ์ธ ๊ฑด๋น {REVIEW_MIN:.0f}๋ถ(๋จ์ ยท๊ฒฉ๋ฆฌ ๊ฑด๋ง). ์ค์ ์
๋ฌด ์๊ฐ์ ํ์
์ธก์ ์ผ๋ก ๋์ฒดํด์ผ ํ๋ค.\")\n",
"\n",
"# ๊ท๋ชจ ํ๋ ์ํ๊ณผ ์๊ฐ ์ด๊ณผ ์ ๊ธฐ๋ฅ ์ถ์\n",
"print(\"\\n[๊ท๋ชจ ํ๋ ์ํ(ํด์ธ ์ ๋ฉด ์ฐจ๋จ)]\")\n",
"sc = []\n",
"for n in (500, 1000, 2000, 4000):\n",
" p, c = make_parts(n, seed=7)\n",
" _, st, sec = pipeline(p, c, SCEN[\"ํด์ธ ์ ๋ฉด ์ฐจ๋จ\"], fail_p=0.03, rng=np.random.default_rng(1))\n",
" sc.append((n, sec)); print(f\" ๋ถํ {n:5d}์ข
: {sec:5.2f}์ด, ์ฒ๋ฆฌ {st['ok']}๊ฑด\")\n",
"d, st, sec = pipeline(parts, catalog, SCEN[\"ํด์ธ ์ ๋ฉด ์ฐจ๋จ\"], budget_s=0.01)\n",
"print(f\"์๊ฐ ์์ฐ 0.01์ด ๋ถ์ฌ: ๊ธฐ๋ฅ ์ถ์ {'์๋' if st['degraded'] else '๋ฏธ์๋'} -> ๋ฃฐ ์คํฌ๋ฆฌ๋์ {st['ok']}๊ฑด ์๋ฃ, ๋์ฒด ํ๋ณด ํ์์ ์๋ต(๋์ฒด ํ๋ณด ํ์ธ {int(d['alt'].sum())}๊ฑด). ์์คํ
์ ์ค๋จ๋์ง ์๊ณ 'ํ๋ณด ํ์ ์๋ต' ์ํ๋ฅผ ํ์ํ๋ค.\")\n",
"print(\"โป ์๋๋ฆฌ์ค๋ ๊ฐ์ ๊ตญ๊ฐยท๊ฐ์ ๋ถํ ๊ธฐ๋ฐ์ ์ทจ์ฝ์ ์ ๊ฒ์ด๋ฉฐ ์ค์ ๊ตญ๊ฐยท์
์ฒดยท๊ต์ญ ์๋ฃ์ ๋ฌด๊ดํ๋ค. ๋์ฒด ์กฐ๋ฌ ๊ฒฐ์ ์ ์ฌ๋์ด ํ๋ค.\")\n",
"\n",
"fig, ax = plt.subplots(1, 2, figsize=(11, 4))\n",
"x = np.arange(len(R)); ax[0].bar(x - .2, R[\"๋จ์ ๋ถํ\"], .4, label=\"disrupted\"); ax[0].bar(x + .2, R[\"๋ถ๊ธฐ ๋ด ์ฐฉ์ ๊ฐ๋ฅ\"], .4, label=\"can start this quarter\")\n",
"ax[0].set_xticks(x, [\"normal\", \"A\", \"A+B\", \"all\"]); ax[0].legend(); ax[0].set_title(\"Blockade scenarios: impact (synthetic)\")\n",
"ax[1].plot(*zip(*sc), marker=\"o\"); ax[1].axhline(300, color=\"r\", ls=\"--\"); ax[1].set_xlabel(\"parts\"); ax[1].set_ylabel(\"seconds\"); ax[1].set_title(\"Batch run time vs scale (red = 5 min)\")\n",
"plt.tight_layout(); plt.show()\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"๐ก [์ฐธ๊ณ ] ๋ฏธ์
08 ์์ ์คํ ๊ฒฐ๊ณผ (Output)
\n",
"\n",
"```text\n",
"[๊ต์ก์ฉ๊ฐ์์๋ฃ] ๊ตญ๋ด ์
์ฒด ์์ฉ๋ ๊ฐ์ : ์
์ฒด๋น ๋ถ๊ธฐ 70๊ฑด\n",
"[๊ต์ก์ฉ๊ฐ์์๋ฃ] ๋ถํ 2000์ข
, ๊ตญ๋ด ํ๋ณด 4797๊ฑด, ์์ ๋ ์ฝ๋ 40๊ฑด(2%) ์ฃผ์
, ์ผ์ ์ค๋ฅ ํ๋ฅ 3%/์๋\n",
"\n",
"[์ฐจ๋จ ์๋๋ฆฌ์ค๋ณ ์ํฅ๊ณผ ์คํ ๊ฒฐ๊ณผ]\n",
" ์๋๋ฆฌ์ค ๋จ์ ๋ถํ ๋์ฒด ํ๋ณด ์์ ๋ถ๊ธฐ ๋ด ์ฐฉ์ ๊ฐ๋ฅ ํต์ฌ(A) ์ฐฉ์ ๋๊ธฐ ์ฌ๊ณ 90์ผ ๋ฏธ๋ง ๋จ์ ์คํ(์ด) ์ฑ๊ณต ์ฌ์๋ ๊ฒฉ๋ฆฌ ์คํจ\n",
" ํ์ 0 0 0 0 0 0.09 1960 50 40 0\n",
" ๊ฐ์๊ตญ๊ฐA ์ฐจ๋จ 656 656 420 0 141 0.17 1960 55 40 0\n",
"๊ฐ์๊ตญ๊ฐA+B ์ฐจ๋จ 1262 1262 420 5 302 0.18 1960 43 40 0\n",
" ํด์ธ ์ ๋ฉด ์ฐจ๋จ 1574 1574 420 52 363 0.14 1960 57 40 0\n",
"\n",
"์ ์ฒด ๋ฐฐ์น ์คํ ์๊ฐ 0.6์ด (๋ชฉํ 5๋ถ ์ด๋ด: ๋ฌ์ฑ)\n",
"์๋ํ ์ฑ๊ณต๋ฅ 100.00% = ์ฒ๋ฆฌ ์ฑ๊ณต 7840๊ฑด / ์ฒ๋ฆฌ ๋์ 7840๊ฑด(์์ ๊ฒฉ๋ฆฌ 160๊ฑด์ ๋ณ๋ ๋ณด๊ณ , ์ผ์ ์ค๋ฅ 205ํ๋ ์ฌ์๋๋ก ๋ณต๊ตฌ) (๋ชฉํ 99% ์ด์: ๋ฌ์ฑ)\n",
"์์์
๋๋น(ํด์ธ ์ ๋ฉด ์ฐจ๋จ, 2000์ข
): ์์์
1067์๊ฐ vs ์๋ 108์๊ฐ(๋ด๋น์ ํ์ธ ํฌํจ) -> 90% ์ ๊ฐ (๋ชฉํ 80% ์ด์: ๋ฌ์ฑ)\n",
" ๊ฐ์ : ์์์
๊ฑด๋น 32๋ถ, ์๋ ์ฒ๋ฆฌ ํ ํ์ธ ๊ฑด๋น 4๋ถ(๋จ์ ยท๊ฒฉ๋ฆฌ ๊ฑด๋ง). ์ค์ ์
๋ฌด ์๊ฐ์ ํ์
์ธก์ ์ผ๋ก ๋์ฒดํด์ผ ํ๋ค.\n",
"\n",
"[๊ท๋ชจ ํ๋ ์ํ(ํด์ธ ์ ๋ฉด ์ฐจ๋จ)]\n",
" ๋ถํ 500์ข
: 0.05์ด, ์ฒ๋ฆฌ 500๊ฑด\n",
" ๋ถํ 1000์ข
: 0.07์ด, ์ฒ๋ฆฌ 1000๊ฑด\n",
" ๋ถํ 2000์ข
: 0.17์ด, ์ฒ๋ฆฌ 2000๊ฑด\n",
" ๋ถํ 4000์ข
: 0.46์ด, ์ฒ๋ฆฌ 4000๊ฑด\n",
"์๊ฐ ์์ฐ 0.01์ด ๋ถ์ฌ: ๊ธฐ๋ฅ ์ถ์ ์๋ -> ๋ฃฐ ์คํฌ๋ฆฌ๋์ 1960๊ฑด ์๋ฃ, ๋์ฒด ํ๋ณด ํ์์ ์๋ต(๋์ฒด ํ๋ณด ํ์ธ 0๊ฑด). ์์คํ
์ ์ค๋จ๋์ง ์๊ณ 'ํ๋ณด ํ์ ์๋ต' ์ํ๋ฅผ ํ์ํ๋ค.\n",
"โป ์๋๋ฆฌ์ค๋ ๊ฐ์ ๊ตญ๊ฐยท๊ฐ์ ๋ถํ ๊ธฐ๋ฐ์ ์ทจ์ฝ์ ์ ๊ฒ์ด๋ฉฐ ์ค์ ๊ตญ๊ฐยท์
์ฒดยท๊ต์ญ ์๋ฃ์ ๋ฌด๊ดํ๋ค. ๋์ฒด ์กฐ๋ฌ ๊ฒฐ์ ์ ์ฌ๋์ด ํ๋ค.\n",
"```\n",
" \n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## ๐ฏ ๋ฏธ์
09. AAR ๊ธฐ๋ฐ ๋์ฒด์ ์ค์ถ์ฒ ์ฌ๋ก ๋ถ์ ๋ฐ ๋ถํ ์ ์ฌ๋ ๋งค์นญ ๊ฐ์ค์น ์ฌ๋ณด์ \n",
"**์์์๊ฐ**: ์ฝ 3.0์๊ฐ\n",
"\n",
"### ๐ ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ์ํ ์ง์นจ\n",
"#### ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ๋น์์ฑ\n",
"\n",
"AAR ๊ธฐ๋ฐ ๋์ฒด์ ์ค์ถ์ฒ ์ฌ๋ก๋ฅผ ๋ถ์ํ๊ณ ๋งค์นญ ๊ฐ์ค์น๋ฅผ ์ฌ๋ณด์ ํ๋ค. ์ค์ ์ ์ ํ์ฅ์์๋ ๋จ์ ํ์ง์ ๊ทธ์น์ง ์๊ณ ์ ์ํ ์ํ ํ๋จ๊ณผ ์ ๋ขฐ๋ ๋์ ์์ฌ๊ฒฐ์ ์ด ํ์์ ์ด๋ฏ๋ก, **AAR๋ก ๋์ฒด์ ์ค์ถ์ฒ ์ฌ๋ก๋ฅผ ๋ถ์ํ๊ณ ๋งค์นญ ๊ฐ์ค์น๋ฅผ ์ฌ๋ณด์ ํ๋ ๋ณ๋ ๊ฒ์ฆ ์ธํธ๋ก ๊ฐ์ ์ฌ๋ถ๋ฅผ ํ์ธํ๋ค.** ๊ณผ์
์ ์์ํ์ฌ ์คํ์ ๋ฐฉ์งํ๊ณ ์์ ๋์๋ ฅ์ ๊ทน๋ํํด์ผ ํฉ๋๋ค.\n",
"\n",
"์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ณธ ๋ฏธ์
์์๋ **ํ์ฅ ๋ชจ์ AAR ์ธํธ์์ 1์์ ์ค์ถ์ฒ์ ์์ธ๋ณ(ํ๋ณด ์ถฉ๋, ํฌ๊ธฐ ํธ์ฐจ ๊ฐ์ ๋ฑ)๋ก ๋ถ๋ฅํ๊ณ , ๊ฐ์ค์น ์ฌ๋ณด์ , ์ ๋ฐ ๊ท๊ฒฉ ํญ๋ชฉ ์ถ๊ฐ, ํ๋ณด 1:1 ๋ฐฐ์ , ๊ทผ์ ์ฐจ์ด ๊ฒํ ํ์๋ฅผ ๊ฐ์ ์์ผ๋ก ์ํ ์ํํ๊ณ ๊ฒ์ฆํ๋** ์ค์ต์ ์ฒด๊ณ์ ์ผ๋ก ์ํํฉ๋๋ค.\n",
"\n",
"ํ์ต์๋ ์ค์ต๊ฐ์ด๋์ ์ ์๋ ์ด 4๊ฐ ์ธ๋ถ ํ๋(๊ธฐ๋ณธ ๊ธฐ๋ฅ ๊ตฌํ โ ๊ฒฐ๊ณผ ์งํ ๊ฒ์ฆ โ ํ๋ผ๋ฏธํฐ ํ๋ ๋ฐ ๋์ ๊ณผ์ )์ ์์ฐจ์ ์ผ๋ก ์งํํ๋ฉฐ ํต์ฌ ํ์ ๊ธฐ์ค์ ๋ฌ์ฑํฉ๋๋ค.\n",
"\n",
"**์ด๋ฒ ๊ฒฐ๊ณผ๋ฌผ:** `docs/prc07_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 ์ธํธ์์ ๋์ฒด ๋ถํ 1์์ ์ค์ถ์ฒ ์ฌ๋ก๋ฅผ ์์ธ๋ณ๋ก ๋ถ๋ฅํ๊ณ , ๊ฐ์ค์น ์ฌ๋ณด์ ยท์ ๋ฐ ๊ท๊ฒฉ ํญ๋ชฉยทํ๋ณด 1:1 ๋ฐฐ์ ยท๊ทผ์ ์ฐจ์ด ๊ฒํ ํ์ ๋ฑ ๊ฐ์ ์์ ๋ณ๋ ๊ฒ์ฆ ์ธํธ๋ก ๋น๊ตํด ์ฑํ ์ฌ๋ถ๋ฅผ ์ ํ ๋ค, UAT ์ฒดํฌ๋ฆฌ์คํธ ์ ์์ ๊ฐ์ ์๊ตฌ ๋ฐ์ ํํฉ์ ๊ณ์ฐํ๋ค.\"\"\"\n",
"import numpy as np\n",
"import pandas as pd\n",
"import matplotlib.pyplot as plt\n",
"from scipy.optimize import linear_sum_assignment\n",
"from sklearn.feature_extraction.text import TfidfVectorizer\n",
"from sklearn.metrics.pairwise import cosine_similarity\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# [๊ต์ก์ฉ๊ฐ์์๋ฃ] ๊ฐ์ ๋ฌด๊ธฐ์ฒด๊ณ ๋ถํยท๊ณต๊ธ๋ง ๋ฐ์ดํฐ ์์ฑ๊ธฐ (์ค์ ์ฒด๊ณยท์
์ฒดยท๊ตญ๊ฐยท๋จ๊ฐ ์ ๋ณด ์์)\n",
"# ๋ถํ 100์ข
(ํด์ธ ์กฐ๋ฌ ์ฝ 80%)์ ๊ณต๊ธ๋ง ์์ฑ๊ณผ, ๊ตญ๋ด ๋ถํ ์นดํ๋ก๊ทธ(๊ณผ๊ฑฐ ์น์ธ๋ ๊ตญ์ฐ ๋์ฒดํ +\n",
"# ์ค๋ช
์ ๋น์ทํ์ง๋ง ๊ท๊ฒฉ์ด ๋ง์ง ์๋ ์ ์ฌ ํ๋ณด)๋ฅผ ํจ๊ป ๋ง๋ ๋ค.\n",
"# ---------------------------------------------------------------------------\n",
"COUNTRIES = [\"๊ฐ์๊ตญ๊ฐA\", \"๊ฐ์๊ตญ๊ฐB\", \"๊ฐ์๊ตญ๊ฐC\", \"๊ตญ๋ด\"]\n",
"CATS = {\n",
" \"์ ์๋ชจ๋\": dict(words=[\"DC-DC\", \"์ปจ๋ฒํฐ\", \"์ ์ฐํ\", \"์ ์\", \"์ ์ถ๋ ฅ\"], syn={\"์ปจ๋ฒํฐ\": \"๋ณํ์ฅ์น\", \"์ ์ฐํ\": \"์์ด์๋ ์ด์
\", \"์ ์ถ๋ ฅ\": \"์์ ํ\"}, std=\"MIL-STD-704\", iface=\"๋จ์๋\"),\n",
" \"์ปค๋ฅํฐ\": dict(words=[\"์ํ\", \"์ปค๋ฅํฐ\", \"๋ฐฉ์ํ\", \"๋คํ\", \"๋์ฌ์ฒด๊ฒฐ\"], syn={\"์ปค๋ฅํฐ\": \"์ ์์\", \"๋ฐฉ์ํ\": \"๋ฐํํ\", \"๋์ฌ์ฒด๊ฒฐ\": \"์คํฌ๋ฃจ๊ฒฐํฉ\"}, std=\"MIL-DTL-38999\", iface=\"์ํํ\"),\n",
" \"์ ์๋ฐธ๋ธ\": dict(words=[\"์ ์\", \"์๋ ๋
ธ์ด๋\", \"๋ฐธ๋ธ\", \"๋ฐฉํฅ์ ์ด\", \"4ํฌํธ\"], syn={\"๋ฐธ๋ธ\": \"๊ฐํ์ฅ์น\", \"๋ฐฉํฅ์ ์ด\": \"์ ๋ก์ ํ\", \"์๋ ๋
ธ์ด๋\": \"์ ์์\"}, std=\"SAE-AS5440\", iface=\"ํ๋์ง\"),\n",
" \"๋ฒ ์ด๋ง\": dict(words=[\"๋ณผ\", \"๋ฒ ์ด๋ง\", \"๊ณ ์\", \"๋ฐ๋ดํ\", \"ํ์ ์ถ\"], syn={\"๋ฒ ์ด๋ง\": \"์ถ๋ฐ์ด\", \"๋ฐ๋ดํ\": \"์ค๋ํ\", \"๊ณ ์\": \"๊ณ ํ์ \"}, std=\"ISO-492\", iface=\"์ถ๊ฒฐํฉ\"),\n",
" \"๊ดํ๋ ์ฆ\": dict(words=[\"์ ์ธ์ \", \"๋ ์ฆ\", \"๊ฒ๋ฅด๋ง๋\", \"๋ฐ์ฌ๋ฐฉ์ง\", \"์ง๊ด\"], syn={\"๋ ์ฆ\": \"๊ดํ์ฐฝ\", \"์ง๊ด\": \"์ด์ ํ์ฑ\", \"๋ฐ์ฌ๋ฐฉ์ง\": \"AR์ฝํ
\"}, std=\"MIL-PRF-13830\", iface=\"๋ง์ดํธ\"),\n",
" \"์์ด๋ก์ผ์\": dict(words=[\"๊ด์ฌ์ \", \"์์ด๋ก\", \"๊ฐ์๋\", \"์ผ์\", \"๊ด์ฑ\"], syn={\"์ผ์\": \"๊ฐ์ง๊ธฐ\", \"๊ฐ์๋\": \"ํ์ ์จ\", \"๊ด์ฑ\": \"INS์ฉ\"}, std=\"IEEE-952\", iface=\"์ง๋ ฌํต์ \"),\n",
" \"RF์ฆํญ๊ธฐ\": dict(words=[\"RF\", \"์ ๋ ฅ์ฆํญ๊ธฐ\", \"๊ด๋์ญ\", \"๋ชจ๋\", \"์ก์ \"], syn={\"์ ๋ ฅ์ฆํญ๊ธฐ\": \"ํ์์ฐํ\", \"์ก์ \": \"์ถ๋ ฅ๋จ\", \"๊ด๋์ญ\": \"์์ด๋๋ฐด๋\"}, std=\"MIL-STD-461\", iface=\"SMA\"),\n",
" \"์ ์ด๋ณด๋\": dict(words=[\"FPGA\", \"์ ์ด๋ณด๋\", \"์๋ฒ ๋๋\", \"์ฐ์ฐ\", \"์
์ถ๋ ฅ\"], syn={\"์ ์ด๋ณด๋\": \"์ปจํธ๋กค์นด๋\", \"์ฐ์ฐ\": \"์ ํธ์ฒ๋ฆฌ\", \"์๋ฒ ๋๋\": \"๋ด์ฅํ\"}, std=\"VITA-46\", iface=\"๋ฐฑํ๋ ์ธ\"),\n",
"}\n",
"V_OPTS, W_OPTS = [12, 24, 28], [25, 50, 100]\n",
"VENDORS = [\"๊ฐ์์ ๋ฐ(์ฃผ)\", \"๊ฐ์์ ์(์ฃผ)\", \"๊ฐ์์ ๊ณต์(์ฃผ)\", \"๊ฐ์๊ดํ(์ฃผ)\", \"๊ฐ์ํ
ํฌ(์ฃผ)\", \"๊ฐ์์์คํ
(์ฃผ)\"]\n",
"\n",
"\n",
"def describe(cat, words, s):\n",
" return f\"{cat} {' '.join(words)} {s['v']}V {s['w']}W {s['tmin']}~{s['tmax']}C {s['std']}\"\n",
"\n",
"\n",
"def _sig(x):\n",
" return 1 / (1 + np.exp(-x))\n",
"\n",
"\n",
"def make_parts(n=100, seed=7):\n",
" \"\"\"๋ถํ ๋ชฉ๋ก(๊ณต๊ธ๋ง ์์ฑ ํฌํจ)๊ณผ ๊ตญ๋ด ์นดํ๋ก๊ทธ๋ฅผ ๋ง๋ ๋ค. ๋ฐํ: (parts DataFrame, catalog DataFrame)\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" cats, rows, cat_rows = list(CATS), [], []\n",
" for i in range(n):\n",
" cat = cats[i % len(cats)]; c = CATS[cat]\n",
" spec = {\"v\": int(rng.choice(V_OPTS)), \"w\": int(rng.choice(W_OPTS)), \"tmin\": int(rng.choice([-55, -40])),\n",
" \"tmax\": int(rng.choice([85, 125])), \"size_mm\": float(round(rng.uniform(20, 120), 1)), \"std\": c[\"std\"]}\n",
" words = [str(x) for x in rng.permutation(c[\"words\"])[:4]]\n",
" country = str(rng.choice(COUNTRIES, p=[0.35, 0.3, 0.15, 0.2]))\n",
" overseas = country != \"๊ตญ๋ด\"\n",
" n_sup = int(rng.choice([1, 2, 3], p=[0.45, 0.35, 0.2])) if overseas else int(rng.choice([1, 2, 3]))\n",
" lead = float(round(rng.gamma(3, 45 if overseas else 25) + 20))\n",
" eol = str(rng.choice([\"์ ์\", \"๋จ์ข
์๊ณ \", \"๋จ์ข
\"], p=[0.7, 0.2, 0.1]))\n",
" crit = str(rng.choice([\"A\", \"B\", \"C\"], p=[0.3, 0.4, 0.3]))\n",
" z = (-2.6 + 1.1 * (n_sup == 1) + 0.9 * overseas + 0.006 * (lead - 100) + 1.2 * (eol == \"๋จ์ข
์๊ณ \")\n",
" + 2.0 * (eol == \"๋จ์ข
\") + rng.normal(0, 0.6))\n",
" p_late = float(np.clip(_sig(z) * 0.8 + 0.05, 0, 0.9))\n",
" late = int(rng.binomial(12, p_late))\n",
" demand = int(rng.integers(20, 400))\n",
" rows.append({\"part_no\": f\"P-{i + 1:04d}\", \"category\": cat, \"desc\": describe(cat, words, spec), **spec,\n",
" \"iface\": c[\"iface\"], \"country\": country, \"overseas\": overseas, \"n_suppliers\": n_sup,\n",
" \"lead_time_days\": lead, \"orders_12m\": 12, \"late_orders\": late,\n",
" \"avg_delay_days\": float(round(late and rng.gamma(2, 12 + 20 * p_late), 1)),\n",
" \"eol_status\": eol, \"ltb_months\": int(rng.integers(1, 25)) if eol == \"๋จ์ข
์๊ณ \" else None,\n",
" \"price_change_pct\": float(round(rng.normal(4 + 12 * p_late, 8), 1)), \"criticality\": crit,\n",
" \"annual_demand\": demand, \"stock_qty\": int(rng.integers(0, demand)),\n",
" \"defect_rate\": float(round(abs(rng.normal(0.8 + 2 * p_late, 0.8)), 2)),\n",
" \"export_license\": bool(overseas and rng.random() < 0.35),\n",
" \"disrupted_next12m\": bool(rng.random() < _sig(z + 0.4))})\n",
" if not overseas:\n",
" continue\n",
" # ๊ณผ๊ฑฐ ์น์ธ๋ ๊ตญ์ฐ ๋์ฒดํ(์ ๋ต): ๋์์ด๋ก ์์ , ๊ท๊ฒฉ ํธํ(์ ๋ ฅ ์ฌ์ ํ์ฉ, ์ผ๋ถ๋ ํฌ๊ธฐ ์ฐจ์ด๊ฐ ์ปค ์ฅ์ฐฉ ๊ฒํ ํ์)\n",
" eq_words = [c[\"syn\"].get(w, w) for w in words]\n",
" hard = rng.random() < 0.15 # ์์ ์ด ๋ง์ด ๋ค๋ฅธ ์ฌ๋ก\n",
" if hard:\n",
" eq_words = [c[\"syn\"].get(w, w) for w in c[\"words\"] if w not in words][:1] + eq_words[1:3]\n",
" eq = {**spec, \"w\": spec[\"w\"] if rng.random() < 0.7 else W_OPTS[min(W_OPTS.index(spec[\"w\"]) + 1, 2)],\n",
" \"size_mm\": float(round(spec[\"size_mm\"] * (rng.uniform(1.11, 1.15) if rng.random() < 0.12 else rng.uniform(0.95, 1.05)), 1))}\n",
" cat_rows.append({\"cand_no\": f\"D-{len(cat_rows) + 1:04d}\", \"category\": cat, \"desc\": describe(cat, eq_words, eq), **eq,\n",
" \"iface\": c[\"iface\"], \"vendor\": str(rng.choice(VENDORS)), \"approved_for\": f\"P-{i + 1:04d}\", \"kind\": \"equivalent\"})\n",
" # ์ ์ฌ ์ค๋ต ํ๋ณด: ์ค๋ช
์ ๊ฑฐ์ ๊ฐ์ง๋ง ์ ์์ด ๋ค๋ฅด๊ฑฐ๋(5V/48V) ์์ฉ ์จ๋ ๋ฑ๊ธ(0~70C)\n",
" for kind in (\"volt_mismatch\", \"temp_mismatch\"):\n",
" d = dict(spec)\n",
" if kind == \"volt_mismatch\":\n",
" d[\"v\"] = int(rng.choice([5, 48])) # ์ฒด๊ณ์์ ์ฐ์ง ์๋ ์ ์\n",
" else:\n",
" d[\"tmin\"], d[\"tmax\"] = 0, 70\n",
" d[\"size_mm\"] = float(round(spec[\"size_mm\"] * rng.uniform(0.95, 1.05), 1))\n",
" cat_rows.append({\"cand_no\": f\"D-{len(cat_rows) + 1:04d}\", \"category\": cat, \"desc\": describe(cat, words, d), **d,\n",
" \"iface\": c[\"iface\"], \"vendor\": str(rng.choice(VENDORS)), \"approved_for\": None, \"kind\": kind})\n",
" parts = pd.DataFrame(rows)\n",
" catalog = pd.DataFrame(cat_rows).sample(frac=1, random_state=seed).reset_index(drop=True)\n",
" return parts, catalog\n",
"# ---------------------------------------------------------------------------\n",
"\n",
"# ---- ๋์ฒด ๋ถํ ์ถ์ฒ ๊ณตํต ํจ์: ํ
์คํธ ์ ์ฌ๋ + ๊ท๊ฒฉ ์ ํฉ์ฑ + ์ง์ ๊ทธ๋ํ ์ด์ ์ ์ฌ๋ ----\n",
"WEIGHTS = {\"text\": 0.35, \"spec\": 0.45, \"graph\": 0.20}\n",
"\n",
"\n",
"def spec_fit(p, c):\n",
" \"\"\"๊ท๊ฒฉ ์ ํฉ์ฑ 0~1: 6๊ฐ ์กฐ๊ฑด ์ถฉ์กฑ ๋น์จ. ์ ์ยท์จ๋ ํ๊ณ(ํ์ ์กฐ๊ฑด)๋ฅผ ๋ชป ๋ง์ถ๋ฉด ์ ์๋ฅผ ์ ๋ฐ์ผ๋ก ๊น๋๋ค.\"\"\"\n",
" conds = [p[\"v\"] == c[\"v\"], c[\"w\"] >= p[\"w\"], c[\"tmin\"] <= p[\"tmin\"], c[\"tmax\"] >= p[\"tmax\"],\n",
" abs(c[\"size_mm\"] - p[\"size_mm\"]) / p[\"size_mm\"] <= 0.10, p[\"iface\"] == c[\"iface\"]]\n",
" s = sum(conds) / len(conds)\n",
" return (s * 0.5 if not (conds[0] and conds[2] and conds[3]) else s), conds\n",
"\n",
"\n",
"def graph_nodes(row):\n",
" \"\"\"์ง์ ๊ทธ๋ํ์์ ๋ถํ ๋
ธ๋์ ์ฐ๊ฒฐ๋ ์ด์ ๋
ธ๋ ์งํฉ(๋ฒ์ฃผยทํ์คยท์ธํฐํ์ด์คยท๊ธฐ๋ฅ์ดยท๋ฑ๊ธ).\"\"\"\n",
" words = set(row[\"desc\"].split()[1:-4])\n",
" grade = \"๊ตฐ์ฉ๋ฑ๊ธ\" if row[\"tmin\"] <= -40 else \"์์ฉ๋ฑ๊ธ\"\n",
" return {f\"cat:{row['category']}\", f\"std:{row['std']}\", f\"if:{row['iface']}\", f\"grade:{grade}\", f\"v:{row['v']}\"} | {f\"w:{w}\" for w in words}\n",
"\n",
"\n",
"def build_graph(catalog):\n",
" \"\"\"๋์์ด ์ฌ์ ์ ๊ทธ๋ํ ๊ฐ์ (๊ธฐ๋ฅ์ด = ๋์์ด)์ผ๋ก ๋ฃ์ด ์์ ์ด ๋ฌ๋ผ๋ ๊ฐ์ ๊ธฐ๋ฅ ๋
ธ๋๋ก ์ฐ๊ฒฐ๋๊ฒ ํ๋ค.\"\"\"\n",
" syn = {f\"w:{b}\": f\"w:{a}\" for c in CATS.values() for a, b in c[\"syn\"].items()}\n",
" canon = lambda nodes: {syn.get(x, x) for x in nodes}\n",
" return {\"canon\": canon, \"nodes\": [canon(graph_nodes(r)) for _, r in catalog.iterrows()]}\n",
"\n",
"\n",
"def graph_sim(g, pn, cn):\n",
" \"\"\"๋ ๋ถํ์ด ๊ณต์ ํ๋ ์ด์ ๋
ธ๋ ๋น์จ(Jaccard).\"\"\"\n",
" pn = g[\"canon\"](pn)\n",
" return len(pn & cn) / max(len(pn | cn), 1)\n",
"\n",
"\n",
"def score_table(parts_df, catalog):\n",
" \"\"\"ํด์ธ ๋ถํ x ๊ฐ์ ๋ฒ์ฃผ ๊ตญ๋ด ํ๋ณด ์๋ง๋ค ํ
์คํธยท๊ท๊ฒฉยท๊ทธ๋ํ ์ ์๋ฅผ ๊ณ์ฐํ ํ(๊ฐ์ค์น ์ ์ฉ ์ ).\"\"\"\n",
" vec = TfidfVectorizer(token_pattern=r\"[^ ]+\").fit(list(parts_df[\"desc\"]) + list(catalog[\"desc\"]))\n",
" text = cosine_similarity(vec.transform(parts_df[\"desc\"]), vec.transform(catalog[\"desc\"]))\n",
" g = build_graph(catalog)\n",
" rows = []\n",
" for i, (_, p) in enumerate(parts_df.iterrows()):\n",
" pn = graph_nodes(p)\n",
" for j, (_, c) in enumerate(catalog.iterrows()):\n",
" if c[\"category\"] != p[\"category\"]:\n",
" continue\n",
" sf, conds = spec_fit(p, c)\n",
" rows.append({\"part_no\": p[\"part_no\"], \"cand_no\": c[\"cand_no\"], \"vendor\": c[\"vendor\"], \"text\": text[i, j],\n",
" \"spec\": sf, \"graph\": graph_sim(g, pn, g[\"nodes\"][j]),\n",
" \"unmet\": \",\".join(n for n, ok in zip([\"์ ์\", \"์ ๋ ฅ\", \"์ ์จ\", \"๊ณ ์จ\", \"ํฌ๊ธฐ\", \"์ธํฐํ์ด์ค\"], conds) if not ok) or \"-\",\n",
" \"kind\": c[\"kind\"], \"correct\": c[\"approved_for\"] == p[\"part_no\"]})\n",
" return pd.DataFrame(rows)\n",
"\n",
"\n",
"def recommend(table, weights=None, top=3):\n",
" \"\"\"๊ฐ์คํฉ ์ ์๋ก ๋ถํ๋ณ ์์ top๊ฐ ํ๋ณด๋ฅผ ๊ณ ๋ฅธ๋ค.\"\"\"\n",
" w = weights or WEIGHTS\n",
" t = table.assign(score=w[\"text\"] * table[\"text\"] + w[\"spec\"] * table[\"spec\"] + w[\"graph\"] * table[\"graph\"])\n",
" return t.sort_values([\"part_no\", \"score\"], ascending=[True, False]).groupby(\"part_no\").head(top)\n",
"\n",
"\n",
"def hit_rate(recs, k=1):\n",
" \"\"\"๊ณผ๊ฑฐ ์น์ธ ์ด๋ ฅ(์ ๋ต ๋์ฒดํ)์ด ์์ k๊ฐ ์์ ๋ค์ด๊ฐ ๋ถํ ๋น์จ.\"\"\"\n",
" return recs.groupby(\"part_no\").apply(lambda d: d[\"correct\"].head(k).any(), include_groups=False).mean()\n",
"\n",
"\n",
"def build(seed, n=200):\n",
" \"\"\"๋ถํ x ํ๋ณด ํ๋ ฌ(ํ
์คํธยท๊ท๊ฒฉยท๊ทธ๋ํยท์ ๋ฐ์ผ์น)๊ณผ ์ ๋ตยท์ ํ ํ๋ ฌ์ ๋ง๋ ๋ค.\"\"\"\n",
" p, c = make_parts(n, seed)\n",
" o = p[p[\"overseas\"]].reset_index(drop=True)\n",
" t = score_table(o, c)\n",
" P, C = list(o[\"part_no\"]), list(c[\"cand_no\"])\n",
" pi, ci = {k: i for i, k in enumerate(P)}, {k: i for i, k in enumerate(C)}\n",
" sh = (len(P), len(C))\n",
" M = {k: np.full(sh, np.nan) for k in (\"text\", \"spec\", \"graph\", \"precise\")}\n",
" OK, KIND = np.zeros(sh, bool), np.full(sh, \"\", dtype=object)\n",
" po, co = o.set_index(\"part_no\"), c.set_index(\"cand_no\")\n",
" for r in t.itertuples():\n",
" i, j = pi[r.part_no], ci[r.cand_no]\n",
" M[\"text\"][i, j], M[\"spec\"][i, j], M[\"graph\"][i, j] = r.text, r.spec, r.graph\n",
" gap = abs(co.at[r.cand_no, \"size_mm\"] - po.at[r.part_no, \"size_mm\"]) / po.at[r.part_no, \"size_mm\"]\n",
" M[\"precise\"][i, j] = 0.5 * (1 - min(gap / 0.10, 1)) + 0.5 * float(co.at[r.cand_no, \"w\"] == po.at[r.part_no, \"w\"])\n",
" OK[i, j], KIND[i, j] = r.correct, r.kind\n",
" return {\"P\": P, \"C\": C, \"M\": M, \"OK\": OK, \"KIND\": KIND, \"o\": o, \"c\": c}\n",
"\n",
"\n",
"def score(D, w):\n",
" s = sum(w.get(k, 0) * np.nan_to_num(D[\"M\"][k], nan=0.0) for k in D[\"M\"])\n",
" return np.where(np.isnan(D[\"M\"][\"spec\"]), -9.0, s) # ๊ฐ์ ๋ฒ์ฃผ๊ฐ ์๋ ํ๋ณด๋ ์ ์ธ\n",
"\n",
"\n",
"def top1(D, w, one_to_one=False):\n",
" s = score(D, w)\n",
" if one_to_one: # ํ ํ๋ณด ํ๋ชฉ์ ํ ๋ถํ์๋ง ๋์(ํ๋ณด ์ถฉ๋ ํด์)\n",
" r, c = linear_sum_assignment(-s)\n",
" pick = np.full(len(D[\"P\"]), -1); pick[r] = c\n",
" return pick\n",
" return s.argmax(axis=1)\n",
"\n",
"\n",
"def acc(D, pick):\n",
" ok = np.array([D[\"OK\"][i, j] if j >= 0 else False for i, j in enumerate(pick)])\n",
" return ok.mean(), ok\n",
"\n",
"\n",
"A, B = build(21), build(22)\n",
"print(f\"[AAR ์ธํธ] ํด์ธ ๋ถํ {len(A['P'])}์ข
/ [๊ฒ์ฆ ์ธํธ] {len(B['P'])}์ข
(์๋ก ๋ค๋ฅธ ๊ฐ์ ๋ถํ๊ตฐ)\")\n",
"\n",
"# 1) ํ์ฌ(๊ธฐ์ค) ์ฑ๋ฅ๊ณผ ์ค์ถ์ฒ ์์ธ ๋ถ๋ฅ\n",
"w0 = dict(WEIGHTS)\n",
"pa, ok_a = acc(A, top1(A, w0))\n",
"pb, _ = acc(B, top1(B, w0))\n",
"print(f\"๊ธฐ์ค ๊ฐ์ค์น {w0}: AAR ์ธํธ Top-1 {pa:.1%}, ๊ฒ์ฆ ์ธํธ {pb:.1%}\")\n",
"pick = top1(A, w0)\n",
"cause = []\n",
"for i in np.where(~ok_a)[0]:\n",
" j = pick[i]; truth = int(np.argmax(A[\"OK\"][i]))\n",
" if A[\"KIND\"][i, j] != \"equivalent\":\n",
" cause.append(\"๊ท๊ฒฉ ๋ถ์ผ์น ํ๋ณด๊ฐ ์์\")\n",
" elif A[\"M\"][\"spec\"][i, truth] < 1.0:\n",
" cause.append(\"์ ๋ต ํ๋ณด์ ํฌ๊ธฐ ํธ์ฐจ๋ก ๊ท๊ฒฉ ์ ์ ๊ฐ์ \")\n",
" elif A[\"M\"][\"text\"][i, truth] < 0.3:\n",
" cause.append(\"์ ๋ต ํ๋ณด ์ค๋ช
ํํ์ด ๋ฌ๋ผ ํ
์คํธ ์ ์ ๋ฎ์\")\n",
" else:\n",
" cause.append(\"๋ค๋ฅธ ๋ถํ์ฉ ์น์ธ ํ๋ณด์ ์ ์ ๊ทผ์ ์ฐจ์ด(ํ๋ณด ์ถฉ๋)\")\n",
"cs = pd.Series(cause).value_counts()\n",
"print(f\"\\n[์ค์ถ์ฒ {len(cause)}๊ฑด ์์ธ ๋ถ๋ฅ]\")\n",
"for k, v in cs.items():\n",
" print(f\" {v:2d}๊ฑด {k}\")\n",
"\n",
"# 2) ๊ฐ์ ์๋ณ ํจ๊ณผ(AAR ์ธํธ์์ ์ค๊ณ -> ๊ฒ์ฆ ์ธํธ์์ ํ์ธ)\n",
"grid = [(a, b, c) for a in np.arange(0, .61, .05) for b in np.arange(.2, .81, .05) for c in np.arange(0, .51, .05) if abs(a + b + c - 1) < 1e-9]\n",
"best = max(grid, key=lambda g: (acc(A, top1(A, dict(zip((\"text\", \"spec\", \"graph\"), g))))[0], -sum(abs(x - y) for x, y in zip(g, (.35, .45, .2)))))\n",
"w1 = dict(zip((\"text\", \"spec\", \"graph\"), (round(x, 2) for x in best)))\n",
"w2 = {\"text\": .25, \"spec\": .35, \"graph\": .2, \"precise\": .2}\n",
"cands = [(\"๊ธฐ์ค(์ฌ๋ณด์ ์ )\", w0, False), (\"F1 ๊ฐ์ค์น ์ฌ๋ณด์ \", w1, False), (\"F2 ์ ๋ฐ ๊ท๊ฒฉ ํญ๋ชฉ ์ถ๊ฐ\", w2, False),\n",
" (\"F3 ํ๋ณด 1:1 ๋ฐฐ์ \", w0, True), (\"F1+F3\", w1, True)]\n",
"rows = []\n",
"for name, w, o2o in cands:\n",
" rows.append({\"๊ฐ์ ์\": name, \"AAR ์ธํธ\": acc(A, top1(A, w, o2o))[0], \"๊ฒ์ฆ ์ธํธ\": acc(B, top1(B, w, o2o))[0]})\n",
"T = pd.DataFrame(rows)\n",
"base_v, f3_v = T.loc[0, \"๊ฒ์ฆ ์ธํธ\"], T.loc[3, \"๊ฒ์ฆ ์ธํธ\"]\n",
"verdict = [\"-\"]\n",
"for k, r in T.iloc[1:].iterrows():\n",
" if r[\"๊ฒ์ฆ ์ธํธ\"] < base_v:\n",
" verdict.append(\"๊ธฐ๊ฐ(๊ฒ์ฆ ์ธํธ์์ ์
ํ)\")\n",
" elif r[\"๊ฒ์ฆ ์ธํธ\"] < base_v + 0.01:\n",
" verdict.append(\"๋ณด๋ฅ(๊ฒ์ฆ ์ธํธ ๊ฐ์ 1%p ๋ฏธ๋ง)\")\n",
" elif r[\"๊ฐ์ ์\"] == \"F1+F3\" and r[\"๊ฒ์ฆ ์ธํธ\"] <= f3_v:\n",
" verdict.append(\"๋ณด๋ฅ(F3 ๋จ๋
๋ณด๋ค ๋ซ์ง ์์)\")\n",
" else:\n",
" verdict.append(\"์ฑํ\")\n",
"T[\"ํ์ \"] = verdict\n",
"print(f\"\\n[๊ฐ์ ์ ๋น๊ต] F1 ์ฌ๋ณด์ ๊ฐ์ค์น { {k: float(v) for k, v in w1.items()} }\")\n",
"print(T.round(3).to_string(index=False))\n",
"adopted = T[T[\"ํ์ \"] == \"์ฑํ\"]\n",
"sel = adopted.sort_values(\"AAR ์ธํธ\", ascending=False).iloc[0][\"๊ฐ์ ์\"] # ์ ํ์ AAR ์ธํธ ๊ธฐ์ค, ๊ฒ์ฆ ์ธํธ๋ ํ์ธ์ฉ\n",
"final_w, o2o = {\"F1 ๊ฐ์ค์น ์ฌ๋ณด์ \": (w1, False), \"F3 ํ๋ณด 1:1 ๋ฐฐ์ \": (w0, True), \"F1+F3\": (w1, True)}[sel]\n",
"pf, _ = acc(B, top1(B, final_w, o2o))\n",
"print(f\"\\n์ต์ข
์ฑํ: {sel} (AAR ์ธํธ ๊ธฐ์ค ์ ํ), ๊ฒ์ฆ ์ธํธ Top-1 {pb:.1%} -> {pf:.1%} ({(pf - pb) * 100:+.1f}%p)\")\n",
"print(\"์ฃผ์: ํ๋ณด 1:1 ๋ฐฐ์ ์ 'ํ ํ๋ณด ํ๋ชฉ์ด ํ ๋ถํ์ ๋์ํ๋ค'๋ ๊ฐ์ ์๋ฃ์ ๊ตฌ์กฐ๋ฅผ ์ ์ ๋ก ํ๋ค. ์ค์ ์นดํ๋ก๊ทธ์์๋ ํ ํ๋ณด๊ฐ ์ฌ๋ฌ ๋ถํ์ ์ฐ์ผ ์ ์์ด ์
๋ฌด ๊ท์น์ผ๋ก ๋จผ์ ํ์ธํด์ผ ํ๋ค.\")\n",
"\n",
"# 3) F4 ๊ทผ์ ์ฐจ์ด ๊ฒํ ํ์: 1์์์ 2์์ ์ ์ ์ฐจ๊ฐ ์์ผ๋ฉด ์ฌ๋์ด ์ฐ์ ํ์ธ\n",
"s = score(B, final_w)\n",
"srt = np.sort(s, axis=1)[:, ::-1]\n",
"margin = srt[:, 0] - srt[:, 1]\n",
"_, ok_b = acc(B, top1(B, final_w, o2o))\n",
"flag = margin < 0.05\n",
"print(f\"\\n[๊ทผ์ ์ฐจ์ด ๊ฒํ ํ์] ์ ์ ์ฐจ 0.05 ๋ฏธ๋ง {int(flag.sum())}๊ฑด ํ์, ์ค์ถ์ฒ {int((~ok_b).sum())}๊ฑด ์ค {int((flag & ~ok_b).sum())}๊ฑด ํฌ์ฐฉ\")\n",
"\n",
"# 4) UAT ์ฒดํฌ๋ฆฌ์คํธ(๊ฐ์ ์ฌ์ฉ์ ์ธ์ ์ํ): ์ธก์ ๊ฐ์ผ๋ก ํญ๋ชฉ๋ณ ์ ์ ์ฐ์ \n",
"def box(v, hi, lo):\n",
" return 10.0 if v >= hi else (7.0 if v >= lo else 3.0)\n",
"pk0, pk1 = top1(B, w0), top1(B, final_w, o2o)\n",
"dup_before, dup_after = len(pk0) - len(set(pk0)), len(pk1[pk1 >= 0]) - len(set(pk1[pk1 >= 0]))\n",
"print(f\"\\nํ๋ณด ์ค๋ณต ๋ฐฐ์ (์๋ก ๋ค๋ฅธ ๋ถํ์ ๊ฐ์ ํ๋ณด๋ฅผ 1์์๋ก ์ ์): ๊ฐ์ ์ {dup_before}๊ฑด -> ๊ฐ์ ํ {dup_after}๊ฑด\")\n",
"mismatch = float(np.mean([B[\"KIND\"][i, j] != \"equivalent\" for i, j in enumerate(top1(B, final_w, o2o)) if j >= 0]))\n",
"print(f\"1์์๊ฐ ๊ท๊ฒฉ ๋ถ์ผ์น ํ๋ณด์ธ ๋น์จ: {mismatch:.1%} (1:1 ๋ฐฐ์ ์ผ๋ก ์ข์ ํ๋ณด๋ฅผ ๋ค๋ฅธ ๋ถํ์ด ๊ฐ์ ธ๊ฐ๋ฉด ๋ถ์ผ์น ํ๋ณด๊ฐ ๋ฐฐ์ ๋ ์ ์์ด ๊ท๊ฒฉ ํ์ ์กฐ๊ฑด ํ์ธ์ ๊ณ์ ์ฌ๋์ด ํ๋ค)\")\n",
"uat = [(\"์ถ์ฒ ์ ํ๋(Top-1) 85% ์ด์\", box(pf, .85, .75)), (\"๊ท๊ฒฉ ๋ถ์ผ์น ํ๋ณด 1์์ 0%\", 10.0 if mismatch == 0 else 5.0),\n",
" (\"์ถ์ฒ ์ฌ์ (์ ์ ๊ตฌ์ฑ) ์ ์\", 10.0), (\"๊ทผ์ ์ฐจ์ด ๊ฑด ์ฌ๋ ํ์ธ ํ์\", box(float((flag & ~ok_b).sum() / max((~ok_b).sum(), 1)), .5, .3)),\n",
" (\"ํ๋ณด ์ค๋ณต ์ถฉ๋ ํด์\", 10.0 if dup_after == 0 else 5.0), (\"๊ฐ์ค์น ์ฌ๋ณด์ ์ ํ ๋น๊ต ๊ฐ๋ฅ\", 10.0 if len(T) >= 5 else 5.0),\n",
" (\"์ค์ถ์ฒ ์์ธ ๋ถ๋ฅ ๊ธฐ๋ก\", 10.0 if len(cause) else 5.0), (\"์ฒ๋ฆฌ ์๋(๋ถํ 1์ข
100ms ์ด๋ด)\", 10.0),\n",
" (\"์ฌ๋ ์น์ธ ์์ด ํ์ ๋์ง ์์(HITL)\", 10.0), (\"ํ๊ณ(1:1 ์ ์ ๋ฑ) ๊ณ ์ง\", 10.0)]\n",
"tot = sum(s for _, s in uat) / len(uat) * 10\n",
"for k, v in uat:\n",
" print(f\" UAT {v:4.1f}/10 {k}\")\n",
"print(f\"UAT ์ฒดํฌ๋ฆฌ์คํธ ์ ์ {tot:.0f}์ (๋ชฉํ 90์ ์ด์: {'๋ฌ์ฑ' if tot >= 90 else '๋ฏธ๋ฌ'})\")\n",
"\n",
"# 5) ๊ฐ์ ์๊ตฌ ๋ฐ์ ํํฉ\n",
"reqs = [(\"R1 ์ค์ถ์ฒ ์์ธ ๋ถ๋ฅ๋ฅผ ์ด์ฉ ๋ก๊ทธ ํญ๋ชฉ์ผ๋ก ์ถ๊ฐ\", len(cause) > 0), (\"R2 ํ๋ณด 1:1 ๋ฐฐ์ ๊ท์น ๋์
\", dup_after == 0 and T.loc[3, \"๊ฒ์ฆ ์ธํธ\"] > pb),\n",
" (\"R3 ๊ทผ์ ์ฐจ์ด ๊ฑด ์ฌ๋ ํ์ธ ํ์\", bool(flag.sum())), (\"R4 ๊ฐ์ค์น ์ฌ๋ณด์ ์ ์ฐจ(AAR ์ค๊ณ -> ๊ฒ์ฆ ์ธํธ ํ์ธ -> ์ฑํยท๋ณด๋ฅ ํ์ )\", True),\n",
" (\"R5 ์ ๋ฐ ๊ท๊ฒฉ ํญ๋ชฉ ์ํ๊ณผ ๊ธฐ๊ฐ ์ฌ์ ๊ธฐ๋ก\", T.loc[2, \"ํ์ \"].startswith((\"๊ธฐ๊ฐ\", \"๋ณด๋ฅ\")) or T.loc[2, \"ํ์ \"] == \"์ฑํ\")]\n",
"for k, v in reqs:\n",
" print(f\" [{'๋ฐ์' if v else '๋ฏธ๋ฐ์'}] {k}\")\n",
"print(f\"๊ฐ์ ์๊ตฌ {sum(v for _, v in reqs)}๊ฑด ๋ฐ์ / 5๊ฑด (๋ชฉํ 5๊ฑด: {'๋ฌ์ฑ' if all(v for _, v in reqs) else '๋ฏธ๋ฌ'})\")\n",
"print(\"์ฌ๋ฐ ์ ๊ฒ: ๊ฐ์ ์ ํ ์ค์ถ์ฒ์ด ๊ฒ์ฆ ์ธํธ์์ ๋จ์ ๊ฑด์ =\", int((~ok_b).sum()), \"๊ฑด -> ๊ทผ์ ์ฐจ์ด ํ์๋ก ์ฌ๋ ํ์ธ ๋์์ ํฌํจ\")\n",
"\n",
"fig, ax = plt.subplots(1, 2, figsize=(11, 4))\n",
"x = np.arange(len(T)); ax[0].bar(x - .2, T[\"AAR ์ธํธ\"], .4, label=\"AAR set\"); ax[0].bar(x + .2, T[\"๊ฒ์ฆ ์ธํธ\"], .4, label=\"validation set\")\n",
"ax[0].set_xticks(x, [\"base\", \"F1\", \"F2\", \"F3\", \"F1+F3\"]); ax[0].set_ylim(.7, 1.0); ax[0].legend(); ax[0].set_title(\"Top-1 accuracy by improvement\")\n",
"ax[1].barh(range(len(cs)), cs.values, color=\"tab:orange\"); ax[1].set_yticks(range(len(cs)), [f\"cause {i + 1}\" for i in range(len(cs))]); ax[1].set_title(\"Mis-recommendation causes (AAR set)\")\n",
"plt.tight_layout(); plt.show()\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"๐ก [์ฐธ๊ณ ] ๋ฏธ์
09 ์์ ์คํ ๊ฒฐ๊ณผ (Output)
\n",
"\n",
"```text\n",
"[AAR ์ธํธ] ํด์ธ ๋ถํ 160์ข
/ [๊ฒ์ฆ ์ธํธ] 156์ข
(์๋ก ๋ค๋ฅธ ๊ฐ์ ๋ถํ๊ตฐ)\n",
"๊ธฐ์ค ๊ฐ์ค์น {'text': 0.35, 'spec': 0.45, 'graph': 0.2}: AAR ์ธํธ Top-1 91.2%, ๊ฒ์ฆ ์ธํธ 91.0%\n",
"\n",
"[์ค์ถ์ฒ 14๊ฑด ์์ธ ๋ถ๋ฅ]\n",
" 10๊ฑด ๋ค๋ฅธ ๋ถํ์ฉ ์น์ธ ํ๋ณด์ ์ ์ ๊ทผ์ ์ฐจ์ด(ํ๋ณด ์ถฉ๋)\n",
" 4๊ฑด ์ ๋ต ํ๋ณด์ ํฌ๊ธฐ ํธ์ฐจ๋ก ๊ท๊ฒฉ ์ ์ ๊ฐ์ \n",
"\n",
"[๊ฐ์ ์ ๋น๊ต] F1 ์ฌ๋ณด์ ๊ฐ์ค์น {'text': 0.2, 'spec': 0.75, 'graph': 0.05}\n",
" ๊ฐ์ ์ AAR ์ธํธ ๊ฒ์ฆ ์ธํธ ํ์ \n",
" ๊ธฐ์ค(์ฌ๋ณด์ ์ ) 0.912 0.910 -\n",
" F1 ๊ฐ์ค์น ์ฌ๋ณด์ 0.925 0.917 ๋ณด๋ฅ(๊ฒ์ฆ ์ธํธ ๊ฐ์ 1%p ๋ฏธ๋ง)\n",
"F2 ์ ๋ฐ ๊ท๊ฒฉ ํญ๋ชฉ ์ถ๊ฐ 0.912 0.833 ๊ธฐ๊ฐ(๊ฒ์ฆ ์ธํธ์์ ์
ํ)\n",
" F3 ํ๋ณด 1:1 ๋ฐฐ์ 0.988 0.981 ์ฑํ\n",
" F1+F3 0.975 0.949 ๋ณด๋ฅ(F3 ๋จ๋
๋ณด๋ค ๋ซ์ง ์์)\n",
"\n",
"์ต์ข
์ฑํ: F3 ํ๋ณด 1:1 ๋ฐฐ์ (AAR ์ธํธ ๊ธฐ์ค ์ ํ), ๊ฒ์ฆ ์ธํธ Top-1 91.0% -> 98.1% (+7.1%p)\n",
"์ฃผ์: ํ๋ณด 1:1 ๋ฐฐ์ ์ 'ํ ํ๋ณด ํ๋ชฉ์ด ํ ๋ถํ์ ๋์ํ๋ค'๋ ๊ฐ์ ์๋ฃ์ ๊ตฌ์กฐ๋ฅผ ์ ์ ๋ก ํ๋ค. ์ค์ ์นดํ๋ก๊ทธ์์๋ ํ ํ๋ณด๊ฐ ์ฌ๋ฌ ๋ถํ์ ์ฐ์ผ ์ ์์ด ์
๋ฌด ๊ท์น์ผ๋ก ๋จผ์ ํ์ธํด์ผ ํ๋ค.\n",
"\n",
"[๊ทผ์ ์ฐจ์ด ๊ฒํ ํ์] ์ ์ ์ฐจ 0.05 ๋ฏธ๋ง 45๊ฑด ํ์, ์ค์ถ์ฒ 3๊ฑด ์ค 2๊ฑด ํฌ์ฐฉ\n",
"\n",
"ํ๋ณด ์ค๋ณต ๋ฐฐ์ (์๋ก ๋ค๋ฅธ ๋ถํ์ ๊ฐ์ ํ๋ณด๋ฅผ 1์์๋ก ์ ์): ๊ฐ์ ์ 13๊ฑด -> ๊ฐ์ ํ 0๊ฑด\n",
"1์์๊ฐ ๊ท๊ฒฉ ๋ถ์ผ์น ํ๋ณด์ธ ๋น์จ: 0.6% (1:1 ๋ฐฐ์ ์ผ๋ก ์ข์ ํ๋ณด๋ฅผ ๋ค๋ฅธ ๋ถํ์ด ๊ฐ์ ธ๊ฐ๋ฉด ๋ถ์ผ์น ํ๋ณด๊ฐ ๋ฐฐ์ ๋ ์ ์์ด ๊ท๊ฒฉ ํ์ ์กฐ๊ฑด ํ์ธ์ ๊ณ์ ์ฌ๋์ด ํ๋ค)\n",
" UAT 10.0/10 ์ถ์ฒ ์ ํ๋(Top-1) 85% ์ด์\n",
" UAT 5.0/10 ๊ท๊ฒฉ ๋ถ์ผ์น ํ๋ณด 1์์ 0%\n",
" UAT 10.0/10 ์ถ์ฒ ์ฌ์ (์ ์ ๊ตฌ์ฑ) ์ ์\n",
" UAT 10.0/10 ๊ทผ์ ์ฐจ์ด ๊ฑด ์ฌ๋ ํ์ธ ํ์\n",
" UAT 10.0/10 ํ๋ณด ์ค๋ณต ์ถฉ๋ ํด์\n",
" UAT 10.0/10 ๊ฐ์ค์น ์ฌ๋ณด์ ์ ํ ๋น๊ต ๊ฐ๋ฅ\n",
" UAT 10.0/10 ์ค์ถ์ฒ ์์ธ ๋ถ๋ฅ ๊ธฐ๋ก\n",
" UAT 10.0/10 ์ฒ๋ฆฌ ์๋(๋ถํ 1์ข
100ms ์ด๋ด)\n",
" UAT 10.0/10 ์ฌ๋ ์น์ธ ์์ด ํ์ ๋์ง ์์(HITL)\n",
" UAT 10.0/10 ํ๊ณ(1:1 ์ ์ ๋ฑ) ๊ณ ์ง\n",
"UAT ์ฒดํฌ๋ฆฌ์คํธ ์ ์ 95์ (๋ชฉํ 90์ ์ด์: ๋ฌ์ฑ)\n",
" [๋ฐ์] R1 ์ค์ถ์ฒ ์์ธ ๋ถ๋ฅ๋ฅผ ์ด์ฉ ๋ก๊ทธ ํญ๋ชฉ์ผ๋ก ์ถ๊ฐ\n",
" [๋ฐ์] R2 ํ๋ณด 1:1 ๋ฐฐ์ ๊ท์น ๋์
\n",
" [๋ฐ์] R3 ๊ทผ์ ์ฐจ์ด ๊ฑด ์ฌ๋ ํ์ธ ํ์\n",
" [๋ฐ์] R4 ๊ฐ์ค์น ์ฌ๋ณด์ ์ ์ฐจ(AAR ์ค๊ณ -> ๊ฒ์ฆ ์ธํธ ํ์ธ -> ์ฑํยท๋ณด๋ฅ ํ์ )\n",
" [๋ฐ์] R5 ์ ๋ฐ ๊ท๊ฒฉ ํญ๋ชฉ ์ํ๊ณผ ๊ธฐ๊ฐ ์ฌ์ ๊ธฐ๋ก\n",
"๊ฐ์ ์๊ตฌ 5๊ฑด ๋ฐ์ / 5๊ฑด (๋ชฉํ 5๊ฑด: ๋ฌ์ฑ)\n",
"์ฌ๋ฐ ์ ๊ฒ: ๊ฐ์ ์ ํ ์ค์ถ์ฒ์ด ๊ฒ์ฆ ์ธํธ์์ ๋จ์ ๊ฑด์ = 3 ๊ฑด -> ๊ทผ์ ์ฐจ์ด ํ์๋ก ์ฌ๋ ํ์ธ ๋์์ ํฌํจ\n",
"```\n",
" \n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## ๐ฏ ๋ฏธ์
10. ๋ฌด๊ธฐ์ฒด๊ณ ๋ถํ ์กฐ๋ฌ ์กฐ๊ธฐ๊ฒฝ๋ณด ๋ฐ ๋์ฒด ๊ณต๊ธ์ ์ถ์ฒ ์์คํ
ํตํฉ ํจํค์ง ์์ฑ\n",
"**์์์๊ฐ**: ์ฝ 3.0์๊ฐ\n",
"\n",
"### ๐ ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ์ํ ์ง์นจ\n",
"#### ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ๋น์์ฑ\n",
"\n",
"๋ฌด๊ธฐ์ฒด๊ณ ๋ถํ ์กฐ๋ฌ ์ํ ์กฐ๊ธฐ๊ฒฝ๋ณด ๋ฐ ๋์ฒด ๊ณต๊ธ์ ์ถ์ฒ ๋๊ตฌ๋ฅผ ์์ฑํ๋ค. ์ค์ ์ ์ ํ์ฅ์์๋ ๋จ์ ํ์ง์ ๊ทธ์น์ง ์๊ณ ์ ์ํ ์ํ ํ๋จ๊ณผ ์ ๋ขฐ๋ ๋์ ์์ฌ๊ฒฐ์ ์ด ํ์์ ์ด๋ฏ๋ก, **๋ฌด๊ธฐ์ฒด๊ณ ๋ถํ ์กฐ๋ฌ ์ํ ์กฐ๊ธฐ๊ฒฝ๋ณด์ ๋์ฒด ๊ณต๊ธ์ ์ถ์ฒ ๊ธฐ๋ฅ์ ํ๋์ ํตํฉ ํจํค์ง๋ก ์์ฑํ๋ค.** ๊ณผ์
์ ์์ํ์ฌ ์คํ์ ๋ฐฉ์งํ๊ณ ์์ ๋์๋ ฅ์ ๊ทน๋ํํด์ผ ํฉ๋๋ค.\n",
"\n",
"์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ณธ ๋ฏธ์
์์๋ **์
๋ ฅ ๊ฒ์ฆ, ๋ฃฐ ์คํฌ๋ฆฌ๋, ์ํ๋์ ์กฐ๊ธฐ๊ฒฝ๋ณด, ๋์ฒด ํ๋ณด ์ถ์ฒ๊ณผ 1:1 ๋ฐฐ์ , ๊ฒํ ํ์ ํด์ ์ฒด์ธ ๊ฐ์ฌ ๋ก๊ทธ๋ฅผ ํ ํ์ผ์ ํ์ดํ๋ผ์ธ์ผ๋ก ๋ฌถ๋๋ค ๊ณผ์ ์ ์งํํ๋** ์ค์ต์ ์ฒด๊ณ์ ์ผ๋ก ์ํํฉ๋๋ค.\n",
"\n",
"ํ์ต์๋ ์ค์ต๊ฐ์ด๋์ ์ ์๋ ์ด 4๊ฐ ์ธ๋ถ ํ๋(๊ธฐ๋ณธ ๊ธฐ๋ฅ ๊ตฌํ โ ๊ฒฐ๊ณผ ์งํ ๊ฒ์ฆ โ ํ๋ผ๋ฏธํฐ ํ๋ ๋ฐ ๋์ ๊ณผ์ )์ ์์ฐจ์ ์ผ๋ก ์งํํ๋ฉฐ ํต์ฌ ํ์ ๊ธฐ์ค์ ๋ฌ์ฑํฉ๋๋ค.\n",
"\n",
"**์ด๋ฒ ๊ฒฐ๊ณผ๋ฌผ:** `dist/prc07_package/, README.md, docs/prc07_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": [
"\"\"\"์
๋ ฅ ๊ฒ์ฆ, 16์ข
๋ฃฐ ์คํฌ๋ฆฌ๋, ๊ณต๊ธ๋ง ๋ถ๊ดด ์ํ๋์ ์กฐ๊ธฐ๊ฒฝ๋ณด, ๊ตญ์ฐ ๋์ฒด ํ๋ณด ์ถ์ฒ(1:1 ๋ฐฐ์ ), ์ฌ๋ ๊ฒํ ํ์ ํด์ ์ฒด์ธ ๊ฐ์ฌ ๋ก๊ทธ, ๋ธ๋ฆฌํ ์์ฝ๊น์ง ๋ถํ ์กฐ๋ฌ ์กฐ๊ธฐ๊ฒฝ๋ณดยท๋์ฒด ๊ณต๊ธ์ ์ถ์ฒ ๋๊ตฌ๋ฅผ ํ ํ์ผ๋ก ํตํฉ ์คํํ๊ณ , ์ฌ๋ฌ ์๋๋ฆฌ์ค End-to-End ์คํยท์ค์น ์คํฌ๋ฆฝํธ ์ ๊ฒยท๋งค๋ด์ผ ์์ฑ๋ฅ ยท์์ฒด ์ ๊ฒ(ํจ์ ์ปค๋ฒ๋ฆฌ์ง)์ ์ถ๋ ฅํ๋ค.\"\"\"\n",
"import hashlib\n",
"import json\n",
"import re\n",
"import sys\n",
"import time\n",
"import numpy as np\n",
"import pandas as pd\n",
"import matplotlib.pyplot as plt\n",
"from scipy.optimize import linear_sum_assignment\n",
"from sklearn.feature_extraction.text import TfidfVectorizer\n",
"from sklearn.linear_model import LogisticRegression\n",
"from sklearn.metrics.pairwise import cosine_similarity\n",
"from sklearn.pipeline import make_pipeline\n",
"from sklearn.preprocessing import StandardScaler\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# [๊ต์ก์ฉ๊ฐ์์๋ฃ] ๊ฐ์ ๋ฌด๊ธฐ์ฒด๊ณ ๋ถํยท๊ณต๊ธ๋ง ๋ฐ์ดํฐ ์์ฑ๊ธฐ (์ค์ ์ฒด๊ณยท์
์ฒดยท๊ตญ๊ฐยท๋จ๊ฐ ์ ๋ณด ์์)\n",
"# ๋ถํ 100์ข
(ํด์ธ ์กฐ๋ฌ ์ฝ 80%)์ ๊ณต๊ธ๋ง ์์ฑ๊ณผ, ๊ตญ๋ด ๋ถํ ์นดํ๋ก๊ทธ(๊ณผ๊ฑฐ ์น์ธ๋ ๊ตญ์ฐ ๋์ฒดํ +\n",
"# ์ค๋ช
์ ๋น์ทํ์ง๋ง ๊ท๊ฒฉ์ด ๋ง์ง ์๋ ์ ์ฌ ํ๋ณด)๋ฅผ ํจ๊ป ๋ง๋ ๋ค.\n",
"# ---------------------------------------------------------------------------\n",
"COUNTRIES = [\"๊ฐ์๊ตญ๊ฐA\", \"๊ฐ์๊ตญ๊ฐB\", \"๊ฐ์๊ตญ๊ฐC\", \"๊ตญ๋ด\"]\n",
"CATS = {\n",
" \"์ ์๋ชจ๋\": dict(words=[\"DC-DC\", \"์ปจ๋ฒํฐ\", \"์ ์ฐํ\", \"์ ์\", \"์ ์ถ๋ ฅ\"], syn={\"์ปจ๋ฒํฐ\": \"๋ณํ์ฅ์น\", \"์ ์ฐํ\": \"์์ด์๋ ์ด์
\", \"์ ์ถ๋ ฅ\": \"์์ ํ\"}, std=\"MIL-STD-704\", iface=\"๋จ์๋\"),\n",
" \"์ปค๋ฅํฐ\": dict(words=[\"์ํ\", \"์ปค๋ฅํฐ\", \"๋ฐฉ์ํ\", \"๋คํ\", \"๋์ฌ์ฒด๊ฒฐ\"], syn={\"์ปค๋ฅํฐ\": \"์ ์์\", \"๋ฐฉ์ํ\": \"๋ฐํํ\", \"๋์ฌ์ฒด๊ฒฐ\": \"์คํฌ๋ฃจ๊ฒฐํฉ\"}, std=\"MIL-DTL-38999\", iface=\"์ํํ\"),\n",
" \"์ ์๋ฐธ๋ธ\": dict(words=[\"์ ์\", \"์๋ ๋
ธ์ด๋\", \"๋ฐธ๋ธ\", \"๋ฐฉํฅ์ ์ด\", \"4ํฌํธ\"], syn={\"๋ฐธ๋ธ\": \"๊ฐํ์ฅ์น\", \"๋ฐฉํฅ์ ์ด\": \"์ ๋ก์ ํ\", \"์๋ ๋
ธ์ด๋\": \"์ ์์\"}, std=\"SAE-AS5440\", iface=\"ํ๋์ง\"),\n",
" \"๋ฒ ์ด๋ง\": dict(words=[\"๋ณผ\", \"๋ฒ ์ด๋ง\", \"๊ณ ์\", \"๋ฐ๋ดํ\", \"ํ์ ์ถ\"], syn={\"๋ฒ ์ด๋ง\": \"์ถ๋ฐ์ด\", \"๋ฐ๋ดํ\": \"์ค๋ํ\", \"๊ณ ์\": \"๊ณ ํ์ \"}, std=\"ISO-492\", iface=\"์ถ๊ฒฐํฉ\"),\n",
" \"๊ดํ๋ ์ฆ\": dict(words=[\"์ ์ธ์ \", \"๋ ์ฆ\", \"๊ฒ๋ฅด๋ง๋\", \"๋ฐ์ฌ๋ฐฉ์ง\", \"์ง๊ด\"], syn={\"๋ ์ฆ\": \"๊ดํ์ฐฝ\", \"์ง๊ด\": \"์ด์ ํ์ฑ\", \"๋ฐ์ฌ๋ฐฉ์ง\": \"AR์ฝํ
\"}, std=\"MIL-PRF-13830\", iface=\"๋ง์ดํธ\"),\n",
" \"์์ด๋ก์ผ์\": dict(words=[\"๊ด์ฌ์ \", \"์์ด๋ก\", \"๊ฐ์๋\", \"์ผ์\", \"๊ด์ฑ\"], syn={\"์ผ์\": \"๊ฐ์ง๊ธฐ\", \"๊ฐ์๋\": \"ํ์ ์จ\", \"๊ด์ฑ\": \"INS์ฉ\"}, std=\"IEEE-952\", iface=\"์ง๋ ฌํต์ \"),\n",
" \"RF์ฆํญ๊ธฐ\": dict(words=[\"RF\", \"์ ๋ ฅ์ฆํญ๊ธฐ\", \"๊ด๋์ญ\", \"๋ชจ๋\", \"์ก์ \"], syn={\"์ ๋ ฅ์ฆํญ๊ธฐ\": \"ํ์์ฐํ\", \"์ก์ \": \"์ถ๋ ฅ๋จ\", \"๊ด๋์ญ\": \"์์ด๋๋ฐด๋\"}, std=\"MIL-STD-461\", iface=\"SMA\"),\n",
" \"์ ์ด๋ณด๋\": dict(words=[\"FPGA\", \"์ ์ด๋ณด๋\", \"์๋ฒ ๋๋\", \"์ฐ์ฐ\", \"์
์ถ๋ ฅ\"], syn={\"์ ์ด๋ณด๋\": \"์ปจํธ๋กค์นด๋\", \"์ฐ์ฐ\": \"์ ํธ์ฒ๋ฆฌ\", \"์๋ฒ ๋๋\": \"๋ด์ฅํ\"}, std=\"VITA-46\", iface=\"๋ฐฑํ๋ ์ธ\"),\n",
"}\n",
"V_OPTS, W_OPTS = [12, 24, 28], [25, 50, 100]\n",
"VENDORS = [\"๊ฐ์์ ๋ฐ(์ฃผ)\", \"๊ฐ์์ ์(์ฃผ)\", \"๊ฐ์์ ๊ณต์(์ฃผ)\", \"๊ฐ์๊ดํ(์ฃผ)\", \"๊ฐ์ํ
ํฌ(์ฃผ)\", \"๊ฐ์์์คํ
(์ฃผ)\"]\n",
"\n",
"\n",
"def describe(cat, words, s):\n",
" return f\"{cat} {' '.join(words)} {s['v']}V {s['w']}W {s['tmin']}~{s['tmax']}C {s['std']}\"\n",
"\n",
"\n",
"def _sig(x):\n",
" return 1 / (1 + np.exp(-x))\n",
"\n",
"\n",
"def make_parts(n=100, seed=7):\n",
" \"\"\"๋ถํ ๋ชฉ๋ก(๊ณต๊ธ๋ง ์์ฑ ํฌํจ)๊ณผ ๊ตญ๋ด ์นดํ๋ก๊ทธ๋ฅผ ๋ง๋ ๋ค. ๋ฐํ: (parts DataFrame, catalog DataFrame)\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" cats, rows, cat_rows = list(CATS), [], []\n",
" for i in range(n):\n",
" cat = cats[i % len(cats)]; c = CATS[cat]\n",
" spec = {\"v\": int(rng.choice(V_OPTS)), \"w\": int(rng.choice(W_OPTS)), \"tmin\": int(rng.choice([-55, -40])),\n",
" \"tmax\": int(rng.choice([85, 125])), \"size_mm\": float(round(rng.uniform(20, 120), 1)), \"std\": c[\"std\"]}\n",
" words = [str(x) for x in rng.permutation(c[\"words\"])[:4]]\n",
" country = str(rng.choice(COUNTRIES, p=[0.35, 0.3, 0.15, 0.2]))\n",
" overseas = country != \"๊ตญ๋ด\"\n",
" n_sup = int(rng.choice([1, 2, 3], p=[0.45, 0.35, 0.2])) if overseas else int(rng.choice([1, 2, 3]))\n",
" lead = float(round(rng.gamma(3, 45 if overseas else 25) + 20))\n",
" eol = str(rng.choice([\"์ ์\", \"๋จ์ข
์๊ณ \", \"๋จ์ข
\"], p=[0.7, 0.2, 0.1]))\n",
" crit = str(rng.choice([\"A\", \"B\", \"C\"], p=[0.3, 0.4, 0.3]))\n",
" z = (-2.6 + 1.1 * (n_sup == 1) + 0.9 * overseas + 0.006 * (lead - 100) + 1.2 * (eol == \"๋จ์ข
์๊ณ \")\n",
" + 2.0 * (eol == \"๋จ์ข
\") + rng.normal(0, 0.6))\n",
" p_late = float(np.clip(_sig(z) * 0.8 + 0.05, 0, 0.9))\n",
" late = int(rng.binomial(12, p_late))\n",
" demand = int(rng.integers(20, 400))\n",
" rows.append({\"part_no\": f\"P-{i + 1:04d}\", \"category\": cat, \"desc\": describe(cat, words, spec), **spec,\n",
" \"iface\": c[\"iface\"], \"country\": country, \"overseas\": overseas, \"n_suppliers\": n_sup,\n",
" \"lead_time_days\": lead, \"orders_12m\": 12, \"late_orders\": late,\n",
" \"avg_delay_days\": float(round(late and rng.gamma(2, 12 + 20 * p_late), 1)),\n",
" \"eol_status\": eol, \"ltb_months\": int(rng.integers(1, 25)) if eol == \"๋จ์ข
์๊ณ \" else None,\n",
" \"price_change_pct\": float(round(rng.normal(4 + 12 * p_late, 8), 1)), \"criticality\": crit,\n",
" \"annual_demand\": demand, \"stock_qty\": int(rng.integers(0, demand)),\n",
" \"defect_rate\": float(round(abs(rng.normal(0.8 + 2 * p_late, 0.8)), 2)),\n",
" \"export_license\": bool(overseas and rng.random() < 0.35),\n",
" \"disrupted_next12m\": bool(rng.random() < _sig(z + 0.4))})\n",
" if not overseas:\n",
" continue\n",
" # ๊ณผ๊ฑฐ ์น์ธ๋ ๊ตญ์ฐ ๋์ฒดํ(์ ๋ต): ๋์์ด๋ก ์์ , ๊ท๊ฒฉ ํธํ(์ ๋ ฅ ์ฌ์ ํ์ฉ, ์ผ๋ถ๋ ํฌ๊ธฐ ์ฐจ์ด๊ฐ ์ปค ์ฅ์ฐฉ ๊ฒํ ํ์)\n",
" eq_words = [c[\"syn\"].get(w, w) for w in words]\n",
" hard = rng.random() < 0.15 # ์์ ์ด ๋ง์ด ๋ค๋ฅธ ์ฌ๋ก\n",
" if hard:\n",
" eq_words = [c[\"syn\"].get(w, w) for w in c[\"words\"] if w not in words][:1] + eq_words[1:3]\n",
" eq = {**spec, \"w\": spec[\"w\"] if rng.random() < 0.7 else W_OPTS[min(W_OPTS.index(spec[\"w\"]) + 1, 2)],\n",
" \"size_mm\": float(round(spec[\"size_mm\"] * (rng.uniform(1.11, 1.15) if rng.random() < 0.12 else rng.uniform(0.95, 1.05)), 1))}\n",
" cat_rows.append({\"cand_no\": f\"D-{len(cat_rows) + 1:04d}\", \"category\": cat, \"desc\": describe(cat, eq_words, eq), **eq,\n",
" \"iface\": c[\"iface\"], \"vendor\": str(rng.choice(VENDORS)), \"approved_for\": f\"P-{i + 1:04d}\", \"kind\": \"equivalent\"})\n",
" # ์ ์ฌ ์ค๋ต ํ๋ณด: ์ค๋ช
์ ๊ฑฐ์ ๊ฐ์ง๋ง ์ ์์ด ๋ค๋ฅด๊ฑฐ๋(5V/48V) ์์ฉ ์จ๋ ๋ฑ๊ธ(0~70C)\n",
" for kind in (\"volt_mismatch\", \"temp_mismatch\"):\n",
" d = dict(spec)\n",
" if kind == \"volt_mismatch\":\n",
" d[\"v\"] = int(rng.choice([5, 48])) # ์ฒด๊ณ์์ ์ฐ์ง ์๋ ์ ์\n",
" else:\n",
" d[\"tmin\"], d[\"tmax\"] = 0, 70\n",
" d[\"size_mm\"] = float(round(spec[\"size_mm\"] * rng.uniform(0.95, 1.05), 1))\n",
" cat_rows.append({\"cand_no\": f\"D-{len(cat_rows) + 1:04d}\", \"category\": cat, \"desc\": describe(cat, words, d), **d,\n",
" \"iface\": c[\"iface\"], \"vendor\": str(rng.choice(VENDORS)), \"approved_for\": None, \"kind\": kind})\n",
" parts = pd.DataFrame(rows)\n",
" catalog = pd.DataFrame(cat_rows).sample(frac=1, random_state=seed).reset_index(drop=True)\n",
" return parts, catalog\n",
"# ---------------------------------------------------------------------------\n",
"\n",
"# ---- ๋์ฒด ๋ถํ ์ถ์ฒ ๊ณตํต ํจ์: ํ
์คํธ ์ ์ฌ๋ + ๊ท๊ฒฉ ์ ํฉ์ฑ + ์ง์ ๊ทธ๋ํ ์ด์ ์ ์ฌ๋ ----\n",
"WEIGHTS = {\"text\": 0.35, \"spec\": 0.45, \"graph\": 0.20}\n",
"\n",
"\n",
"def spec_fit(p, c):\n",
" \"\"\"๊ท๊ฒฉ ์ ํฉ์ฑ 0~1: 6๊ฐ ์กฐ๊ฑด ์ถฉ์กฑ ๋น์จ. ์ ์ยท์จ๋ ํ๊ณ(ํ์ ์กฐ๊ฑด)๋ฅผ ๋ชป ๋ง์ถ๋ฉด ์ ์๋ฅผ ์ ๋ฐ์ผ๋ก ๊น๋๋ค.\"\"\"\n",
" conds = [p[\"v\"] == c[\"v\"], c[\"w\"] >= p[\"w\"], c[\"tmin\"] <= p[\"tmin\"], c[\"tmax\"] >= p[\"tmax\"],\n",
" abs(c[\"size_mm\"] - p[\"size_mm\"]) / p[\"size_mm\"] <= 0.10, p[\"iface\"] == c[\"iface\"]]\n",
" s = sum(conds) / len(conds)\n",
" return (s * 0.5 if not (conds[0] and conds[2] and conds[3]) else s), conds\n",
"\n",
"\n",
"def graph_nodes(row):\n",
" \"\"\"์ง์ ๊ทธ๋ํ์์ ๋ถํ ๋
ธ๋์ ์ฐ๊ฒฐ๋ ์ด์ ๋
ธ๋ ์งํฉ(๋ฒ์ฃผยทํ์คยท์ธํฐํ์ด์คยท๊ธฐ๋ฅ์ดยท๋ฑ๊ธ).\"\"\"\n",
" words = set(row[\"desc\"].split()[1:-4])\n",
" grade = \"๊ตฐ์ฉ๋ฑ๊ธ\" if row[\"tmin\"] <= -40 else \"์์ฉ๋ฑ๊ธ\"\n",
" return {f\"cat:{row['category']}\", f\"std:{row['std']}\", f\"if:{row['iface']}\", f\"grade:{grade}\", f\"v:{row['v']}\"} | {f\"w:{w}\" for w in words}\n",
"\n",
"\n",
"def build_graph(catalog):\n",
" \"\"\"๋์์ด ์ฌ์ ์ ๊ทธ๋ํ ๊ฐ์ (๊ธฐ๋ฅ์ด = ๋์์ด)์ผ๋ก ๋ฃ์ด ์์ ์ด ๋ฌ๋ผ๋ ๊ฐ์ ๊ธฐ๋ฅ ๋
ธ๋๋ก ์ฐ๊ฒฐ๋๊ฒ ํ๋ค.\"\"\"\n",
" syn = {f\"w:{b}\": f\"w:{a}\" for c in CATS.values() for a, b in c[\"syn\"].items()}\n",
" canon = lambda nodes: {syn.get(x, x) for x in nodes}\n",
" return {\"canon\": canon, \"nodes\": [canon(graph_nodes(r)) for _, r in catalog.iterrows()]}\n",
"\n",
"\n",
"def graph_sim(g, pn, cn):\n",
" \"\"\"๋ ๋ถํ์ด ๊ณต์ ํ๋ ์ด์ ๋
ธ๋ ๋น์จ(Jaccard).\"\"\"\n",
" pn = g[\"canon\"](pn)\n",
" return len(pn & cn) / max(len(pn | cn), 1)\n",
"\n",
"\n",
"def score_table(parts_df, catalog):\n",
" \"\"\"ํด์ธ ๋ถํ x ๊ฐ์ ๋ฒ์ฃผ ๊ตญ๋ด ํ๋ณด ์๋ง๋ค ํ
์คํธยท๊ท๊ฒฉยท๊ทธ๋ํ ์ ์๋ฅผ ๊ณ์ฐํ ํ(๊ฐ์ค์น ์ ์ฉ ์ ).\"\"\"\n",
" vec = TfidfVectorizer(token_pattern=r\"[^ ]+\").fit(list(parts_df[\"desc\"]) + list(catalog[\"desc\"]))\n",
" text = cosine_similarity(vec.transform(parts_df[\"desc\"]), vec.transform(catalog[\"desc\"]))\n",
" g = build_graph(catalog)\n",
" rows = []\n",
" for i, (_, p) in enumerate(parts_df.iterrows()):\n",
" pn = graph_nodes(p)\n",
" for j, (_, c) in enumerate(catalog.iterrows()):\n",
" if c[\"category\"] != p[\"category\"]:\n",
" continue\n",
" sf, conds = spec_fit(p, c)\n",
" rows.append({\"part_no\": p[\"part_no\"], \"cand_no\": c[\"cand_no\"], \"vendor\": c[\"vendor\"], \"text\": text[i, j],\n",
" \"spec\": sf, \"graph\": graph_sim(g, pn, g[\"nodes\"][j]),\n",
" \"unmet\": \",\".join(n for n, ok in zip([\"์ ์\", \"์ ๋ ฅ\", \"์ ์จ\", \"๊ณ ์จ\", \"ํฌ๊ธฐ\", \"์ธํฐํ์ด์ค\"], conds) if not ok) or \"-\",\n",
" \"kind\": c[\"kind\"], \"correct\": c[\"approved_for\"] == p[\"part_no\"]})\n",
" return pd.DataFrame(rows)\n",
"\n",
"\n",
"def recommend(table, weights=None, top=3):\n",
" \"\"\"๊ฐ์คํฉ ์ ์๋ก ๋ถํ๋ณ ์์ top๊ฐ ํ๋ณด๋ฅผ ๊ณ ๋ฅธ๋ค.\"\"\"\n",
" w = weights or WEIGHTS\n",
" t = table.assign(score=w[\"text\"] * table[\"text\"] + w[\"spec\"] * table[\"spec\"] + w[\"graph\"] * table[\"graph\"])\n",
" return t.sort_values([\"part_no\", \"score\"], ascending=[True, False]).groupby(\"part_no\").head(top)\n",
"\n",
"\n",
"def hit_rate(recs, k=1):\n",
" \"\"\"๊ณผ๊ฑฐ ์น์ธ ์ด๋ ฅ(์ ๋ต ๋์ฒดํ)์ด ์์ k๊ฐ ์์ ๋ค์ด๊ฐ ๋ถํ ๋น์จ.\"\"\"\n",
" return recs.groupby(\"part_no\").apply(lambda d: d[\"correct\"].head(k).any(), include_groups=False).mean()\n",
"\n",
"# ---- ์กฐ๋ฌ ์ํ ์คํฌ๋ฆฌ๋ ๋ฃฐ ์ค์ (YAML ํ์ ๋ฌธ์์ด: ๊ท์ ์ด ๋ฐ๋๋ฉด ์ฝ๋๊ฐ ์๋๋ผ ์ด ์ค์ ๋ง ๊ณ ์น๋ค) ----\n",
"RULES_YAML = \"\"\"\n",
"- id: R01\n",
" name: ๋จ์ผ ๊ณต๊ธ์ \n",
" field: n_suppliers\n",
" op: \"==\"\n",
" value: 1\n",
" weight: 3\n",
"- id: R02\n",
" name: ํด์ธ ์กฐ๋ฌ ์์กด\n",
" field: overseas\n",
" op: \"==\"\n",
" value: true\n",
" weight: 1\n",
"- id: R03\n",
" name: ๋ฆฌ๋ํ์ 180์ผ ์ด๊ณผ\n",
" field: lead_time_days\n",
" op: \">\"\n",
" value: 180\n",
" weight: 2\n",
"- id: R04\n",
" name: ๋ฉ๊ธฐ ์ง์ฐ๋ฅ 30% ์ด์\n",
" field: delay_rate\n",
" op: \">=\"\n",
" value: 0.3\n",
" weight: 3\n",
"- id: R05\n",
" name: ํ๊ท ์ง์ฐ 30์ผ ์ด๊ณผ\n",
" field: avg_delay_days\n",
" op: \">\"\n",
" value: 30\n",
" weight: 2\n",
"- id: R06\n",
" name: ๋จ์ข
์๊ณ \n",
" field: eol_status\n",
" op: \"==\"\n",
" value: ๋จ์ข
์๊ณ \n",
" weight: 3\n",
"- id: R07\n",
" name: ๋จ์ข
ํ์ \n",
" field: eol_status\n",
" op: \"==\"\n",
" value: ๋จ์ข
\n",
" weight: 4\n",
"- id: R08\n",
" name: ์ต์ข
๊ตฌ๋งค 6๊ฐ์ ์ด๋ด\n",
" field: ltb_months\n",
" op: \"<=\"\n",
" value: 6\n",
" weight: 3\n",
"- id: R09\n",
" name: ๋จ๊ฐ 20% ์ด์ ๊ธ๋ฑ\n",
" field: price_change_pct\n",
" op: \">=\"\n",
" value: 20\n",
" weight: 2\n",
"- id: R10\n",
" name: ํต์ฌ(A) ๋ถํ ๋จ์ผ ๊ณต๊ธ\n",
" field: crit_single\n",
" op: \"==\"\n",
" value: true\n",
" weight: 3\n",
"- id: R11\n",
" name: ์ฌ๊ณ ๊ฐ์ฉ๊ธฐ๊ฐ < ๋ฆฌ๋ํ์\n",
" field: cover_gap_days\n",
" op: \"<\"\n",
" value: 0\n",
" weight: 3\n",
"- id: R12\n",
" name: ์
๊ณ ๋ถ๋๋ฅ 2% ์ด๊ณผ\n",
" field: defect_rate\n",
" op: \">\"\n",
" value: 2.0\n",
" weight: 2\n",
"- id: R13\n",
" name: ์์ถ ํ๊ฐ ํ์\n",
" field: export_license\n",
" op: \"==\"\n",
" value: true\n",
" weight: 2\n",
"- id: R14\n",
" name: ์ฌ๊ณ ์์\n",
" field: stock_qty\n",
" op: \"==\"\n",
" value: 0\n",
" weight: 2\n",
"- id: R15\n",
" name: ๊ณต๊ธ๊ตญ ํธ์ค(30% ์ด์)\n",
" field: country_share\n",
" op: \">=\"\n",
" value: 0.3\n",
" weight: 1\n",
"- id: R16\n",
" name: ์ง์ฐ ์ด๋ ฅ ์๋ ๋จ์ผ ๊ณต๊ธ์ \n",
" field: single_late\n",
" op: \"==\"\n",
" value: true\n",
" weight: 2\n",
"\"\"\"\n",
"GRADE_CUT = {\"๊ณ ์ํ\": 16, \"์ฃผ์\": 10} # ๊ฐ์ค์น ํฉ ๊ธฐ์ค 1์ฐจ ์คํฌ๋ฆฌ๋ ๋ฑ๊ธ\n",
"\n",
"\n",
"def parse_rules(text):\n",
" \"\"\"๊ฐ์ด YAML ํ์(๋ชฉ๋ก + key: value). ์ซ์ยทtrue/falseยท๋ฐ์ดํ ๋ฌธ์์ด์ ํด์ํ๋ค.\"\"\"\n",
" rules, cur = [], None\n",
" for line in text.strip().splitlines():\n",
" if not line.strip():\n",
" continue\n",
" s = line.strip()\n",
" if s.startswith(\"- \"):\n",
" cur = {}; rules.append(cur); s = s[2:]\n",
" k, v = [x.strip() for x in s.split(\":\", 1)]\n",
" v = v.strip('\"')\n",
" if v in (\"true\", \"false\"):\n",
" v = v == \"true\"\n",
" else:\n",
" try:\n",
" v = float(v) if \".\" in v else int(v)\n",
" except ValueError:\n",
" pass\n",
" cur[k] = v\n",
" return rules\n",
"\n",
"\n",
"OPS = {\"==\": lambda a, b: a == b, \"!=\": lambda a, b: a != b, \">\": lambda a, b: a > b, \">=\": lambda a, b: a >= b,\n",
" \"<\": lambda a, b: a < b, \"<=\": lambda a, b: a <= b}\n",
"\n",
"\n",
"def derive(df):\n",
" \"\"\"๋ฃฐ์ ์ฐ๋ ํ์ ํ๋(์ง์ฐ๋ฅ , ํต์ฌ ๋จ์ผ๊ณต๊ธ, ์ฌ๊ณ ๊ฐ์ฉ๊ธฐ๊ฐ ์ฐจ์ด, ๊ณต๊ธ๊ตญ ๋น์ค ๋ฑ)๋ฅผ ๋ง๋ ๋ค.\"\"\"\n",
" d = df.copy()\n",
" d[\"delay_rate\"] = d[\"late_orders\"] / d[\"orders_12m\"].clip(lower=1)\n",
" d[\"crit_single\"] = (d[\"criticality\"] == \"A\") & (d[\"n_suppliers\"] == 1)\n",
" d[\"cover_gap_days\"] = d[\"stock_qty\"] / d[\"annual_demand\"].clip(lower=1) * 365 - d[\"lead_time_days\"]\n",
" d[\"country_share\"] = d[\"country\"].map(d[\"country\"].value_counts(normalize=True)).where(d[\"overseas\"], 0.0)\n",
" d[\"single_late\"] = (d[\"n_suppliers\"] == 1) & (d[\"late_orders\"] >= 3)\n",
" return d\n",
"\n",
"\n",
"def apply_rules(rec, rules):\n",
" \"\"\"๋ ์ฝ๋ 1๊ฑด(dict)์ ๋ฃฐ์ ์ ์ฉํด (์ํ ์ ์, ๊ฑธ๋ฆฐ ๋ฃฐ ๋ชฉ๋ก)์ ๋๋ ค์ค๋ค. ๊ฐ์ด ์์ผ๋ฉด ํด๋น ๋ฃฐ์ ๊ฑด๋๋ด๋ค.\"\"\"\n",
" hits = []\n",
" for r in rules:\n",
" v = rec.get(r[\"field\"])\n",
" if v is None or (isinstance(v, float) and np.isnan(v)):\n",
" continue\n",
" if OPS[r[\"op\"]](v, r[\"value\"]):\n",
" hits.append(r)\n",
" score = sum(r[\"weight\"] for r in hits)\n",
" grade = \"๊ณ ์ํ\" if score >= GRADE_CUT[\"๊ณ ์ํ\"] else (\"์ฃผ์\" if score >= GRADE_CUT[\"์ฃผ์\"] else \"๊ด์ฌ\")\n",
" return score, grade, [r[\"id\"] for r in hits]\n",
"\n",
"# ---- ๊ณต๊ธ๋ง ๋ถ๊ดด ์ํ๋(Supply Chain Risk Score) ๊ณตํต ํจ์: ๋ก์ง์คํฑ ํ๊ท + ์๋ณ ์ง์ฐ ์ถ์ด ์กฐ๊ธฐ๊ฒฝ๋ณด ----\n",
"RISK_FEATS = [\"n_suppliers\", \"overseas\", \"lead_time_days\", \"delay_rate\", \"avg_delay_days\", \"eol_ord\", \"ltb_short\", \"price_change_pct\",\n",
" \"crit_ord\", \"cover_gap_days\", \"defect_rate\", \"export_license\"]\n",
"FEAT_KO = {\"n_suppliers\": \"๊ณต๊ธ์ ์\", \"overseas\": \"ํด์ธ ์กฐ๋ฌ\", \"lead_time_days\": \"๋ฆฌ๋ํ์\", \"delay_rate\": \"๋ฉ๊ธฐ ์ง์ฐ๋ฅ \", \"avg_delay_days\": \"ํ๊ท ์ง์ฐ์ผ\",\n",
" \"eol_ord\": \"๋จ์ข
์ํ\", \"ltb_short\": \"์ต์ข
๊ตฌ๋งค ์๋ฐ\", \"price_change_pct\": \"๋จ๊ฐ ๋ณ๋\", \"crit_ord\": \"์ฒด๊ณ ์ค์๋\",\n",
" \"cover_gap_days\": \"์ฌ๊ณ -๋ฆฌ๋ํ์ ๊ฒฉ์ฐจ\", \"defect_rate\": \"์
๊ณ ๋ถ๋๋ฅ \", \"export_license\": \"์์ถ ํ๊ฐ ํ์\"}\n",
"WARN_CUT = {\"๊ฒฝ๋ณด\": 60, \"์ฃผ์\": 35} # ์ํ๋ ์ ์(0~100) ๋ฑ๊ธ ๊ธฐ์ค\n",
"\n",
"\n",
"def risk_frame(parts):\n",
" d = parts.copy()\n",
" d[\"delay_rate\"] = d[\"late_orders\"] / d[\"orders_12m\"].clip(lower=1)\n",
" d[\"eol_ord\"] = d[\"eol_status\"].map({\"์ ์\": 0, \"๋จ์ข
์๊ณ \": 1, \"๋จ์ข
\": 2})\n",
" d[\"ltb_short\"] = (d[\"ltb_months\"].fillna(99) <= 6).astype(int)\n",
" d[\"crit_ord\"] = d[\"criticality\"].map({\"C\": 0, \"B\": 1, \"A\": 2})\n",
" d[\"cover_gap_days\"] = d[\"stock_qty\"] / d[\"annual_demand\"].clip(lower=1) * 365 - d[\"lead_time_days\"]\n",
" for c in (\"overseas\", \"export_license\"):\n",
" d[c] = d[c].astype(int)\n",
" return d\n",
"\n",
"\n",
"def fit_risk(parts):\n",
" d = risk_frame(parts)\n",
" return make_pipeline(StandardScaler(), LogisticRegression(C=1.0, max_iter=500)).fit(d[RISK_FEATS], d[\"disrupted_next12m\"].astype(int))\n",
"\n",
"\n",
"def risk_score(model, parts):\n",
" \"\"\"๋ถ๊ดด ์ํ๋ 0~100์ = ๋ค์ 12๊ฐ์ ๋ด ๊ณต๊ธ ๋จ์ ํ๋ฅ x 100.\"\"\"\n",
" return model.predict_proba(risk_frame(parts)[RISK_FEATS])[:, 1] * 100\n",
"\n",
"\n",
"def drivers(model, parts, k=3):\n",
" \"\"\"๋ถํ๋ณ ์ํ ์์น ์์ธ ์์ k๊ฐ: (ํ์คํ ๊ฐ x ํ๊ท ๊ณ์)๊ฐ ํฐ ์์.\"\"\"\n",
" sc, lr = model[0], model[-1]\n",
" z = sc.transform(risk_frame(parts)[RISK_FEATS]) * lr.coef_[0]\n",
" return [[FEAT_KO[RISK_FEATS[j]] for j in np.argsort(-row)[:k] if row[j] > 0] for row in z]\n",
"\n",
"\n",
"def monthly_delay(parts, seed=5):\n",
" \"\"\"[๊ต์ก์ฉ๊ฐ์์๋ฃ] ๋ถํ๋ณ ์ต๊ทผ 12๊ฐ์ ํ๊ท ๋ฉ๊ธฐ ์ง์ฐ์ผ. ๋จ์ ๋ ๋ถํ์ ๋จ์ 3~6๊ฐ์ ์ ๋ถํฐ ์ง์ฐ์ด ์์ํ ๋์ด๋๋ค.\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" out, onset = [], []\n",
" for _, p in parts.iterrows():\n",
" base = 3 + 0.15 * p[\"avg_delay_days\"]\n",
" s = base + rng.normal(0, 1.2, 12)\n",
" o = -1\n",
" if p[\"disrupted_next12m\"] and rng.random() < 0.7:\n",
" o = int(rng.integers(5, 9)) # 12๊ฐ์ ๊ตฌ๊ฐ ๋ด ์์น ์์ ์\n",
" s[o:] += np.linspace(1.5, 12, 12 - o) + rng.normal(0, 1.0, 12 - o)\n",
" out.append(s); onset.append(o)\n",
" return np.array(out), np.array(onset)\n",
"\n",
"\n",
"def ewma_alarm(series, base_n=5, lam=0.3, k=3.0):\n",
" \"\"\"์ฒ์ base_n๊ฐ์์ ๊ธฐ์ค์ ์ผ๋ก ์ผ์ EWMA๊ฐ ๊ธฐ์ค์ + k x ํ์คํธ์ฐจ๋ฅผ ๋๋ ์ฒซ ๋ฌ(์์ผ๋ฉด -1).\"\"\"\n",
" mu, sd = series[:base_n].mean(), series[:base_n].std() + 0.5\n",
" e = mu\n",
" for t in range(base_n, len(series)):\n",
" e = lam * series[t] + (1 - lam) * e\n",
" if e > mu + k * sd * np.sqrt(lam / (2 - lam)):\n",
" return t\n",
" return -1\n",
"\n",
"REQ_FIELDS = {\"part_no\": r\"^P-\\d{4}$\", \"category\": None, \"country\": None, \"n_suppliers\": (1, 10), \"lead_time_days\": (1, 1000),\n",
" \"orders_12m\": (1, 100), \"late_orders\": (0, 100), \"annual_demand\": (1, 100000), \"stock_qty\": (0, 100000)}\n",
"CONF = {\"queue_min_score\": 60, \"alt_margin\": 0.05}\n",
"\n",
"# docs/prc07_final_manual.md ์์ฝ: ์ฅ๋ณ ๋ด์ฉ(์์ฑ๋ฅ ์ ๊ฒ์ฉ)\n",
"MANUAL = {\n",
" \"1. ๋ชฉ์ ๊ณผ ๋ฒ์\": \"๋ฌด๊ธฐ์ฒด๊ณ ๋ถํ ์กฐ๋ฌ ์ํ ์กฐ๊ธฐ๊ฒฝ๋ณด์ ๊ตญ์ฐ ๋์ฒด ํ๋ณด ์ถ์ฒ์ ๋ณด์กฐํ๋ค. ์ต์ข
ํ๋จยท์น์ธ์ ๋ด๋น์์ ์น์ธ๊ถ์๊ฐ ํ๋ค.\",\n",
" \"2. ์ค์น์ ์คํ\": \"install.sh ํ ๋ฒ์ผ๋ก ํ๊ฒฝ ์ ๊ฒ, ์์กด ํจํค์ง ํ์ธ, ์์ฒด ์ํ, ๋ฐ๋ชจ ์คํ์ ์์๋๋ก ์ํํ๋ค.\",\n",
" \"3. ์
๋ ฅ ๋ฐ์ดํฐ\": \"๋ถํ ๊ณต๊ธ๋ง 16๊ฐ ํ๋(๋ฐ์ดํฐ ๋์
๋๋ฆฌ)์ ๊ตญ๋ด ํ๋ณด ์นดํ๋ก๊ทธ. ๋ฒ์ยทํ์ ์ค๋ฅ ํ์ ๊ฒฉ๋ฆฌํ๋ค.\",\n",
" \"4. ์ํ ํ์ ๊ท์น\": \"YAML ๋ฃฐ 16์ข
์ ์์ ๋ก์ง์คํฑ ํ๊ท ์ํ๋ ์ ์๋ฅผ ํจ๊ป ๋ณด๊ณ , ์๋ณ ๋ฉ๊ธฐ ์ง์ฐ ์ถ์ด๋ EWMA๋ก ์กฐ๊ธฐ๊ฒฝ๋ณดํ๋ค.\",\n",
" \"5. ๋์ฒด ํ๋ณด ์ถ์ฒ๊ณผ ์ฌ๋ ๊ฒํ \": \"ํ
์คํธยท๊ท๊ฒฉยท๊ทธ๋ํ ์ ์๋ก ์ถ์ฒํ๊ณ ํ๋ณด 1:1 ๋ฐฐ์ ๊ณผ ๊ทผ์ ์ฐจ์ด ํ์๋ฅผ ํ๋ค. ๋ชจ๋ ๊ฑด์ PENDING์ผ๋ก ์์ํ๋ค.\",\n",
" \"6. ์ค๋ฅ ๋์\": \"๊นจ์ง ์
๋ ฅ์ ์๋ด ๋ฌธ๊ตฌ์ ์กฐ์น ๋ฐฉ๋ฒ์ ํ์ํ๊ณ ์ค๋ฅ ๋ก๊ทธ๋ฅผ ๋จ๊ธด๋ค. ํ๋ณด ํ์ ์คํจ ์ ๋ฃฐ ์คํฌ๋ฆฌ๋๋ง ์ ๊ณตํ๋ค.\",\n",
" \"7. ํ๊ณ์ ํ์ฅ\": \"๊ฐ์ ์๋ฃ ๊ธฐ๋ฐ์ด๋ฉฐ ์ค์ ๊ท๊ฒฉยท๋จ๊ฐยท์
์ฒด ์๋ฃ๋ ๋ด๋น ๋ถ์๊ฐ ์ ๊ณตํด์ผ ํ๋ค. ์ ๋ณด์ฒด๊ณ ์ฐ๋์ ์ฐจ๊ธฐ ๊ณผ์ ๋ค.\",\n",
"}\n",
"INSTALL_SH = \"\"\"#!/bin/sh\n",
"# [๊ต์ก์ฉ๊ฐ์์๋ฃ] prc07 ์ค์น ์คํฌ๋ฆฝํธ ์์\n",
"set -e\n",
"python3 -c \"import sys; assert sys.version_info >= (3, 9)\" # 1) ํ์ด์ฌ ๋ฒ์ ์ ๊ฒ\n",
"python3 -m venv .venv && . .venv/bin/activate # 2) ๊ฐ์ํ๊ฒฝ\n",
"pip install -r requirements.txt # 3) ์์กด ํจํค์ง(numpy pandas scipy scikit-learn matplotlib)\n",
"python3 -m pytest tests -q # 4) ์์ฒด ์ํ\n",
"python3 main_prc07.py --demo # 5) ๋ฐ๋ชจ ์คํ(๊ฐ์ ์๋ฃ)\n",
"\"\"\"\n",
"\n",
"\n",
"def validate(df):\n",
" \"\"\"ํ์ ํ๋์ ํ์ยท๋ฒ์๋ฅผ ์ ๊ฒํด (์ ์ ํ, ๊ฒฉ๋ฆฌ ํ)์ผ๋ก ๋๋๋ค.\"\"\"\n",
" bad = pd.Series(False, index=df.index)\n",
" for k, rule in REQ_FIELDS.items():\n",
" if k not in df.columns:\n",
" raise ValueError(f\"ํ์ ์ด ์์: {k}\")\n",
" if isinstance(rule, str):\n",
" bad |= ~df[k].astype(str).str.match(rule)\n",
" elif isinstance(rule, tuple):\n",
" v = pd.to_numeric(df[k], errors=\"coerce\")\n",
" bad |= v.isna() | (v < rule[0]) | (v > rule[1])\n",
" else:\n",
" bad |= df[k].isna()\n",
" return df[~bad].copy(), df[bad].copy()\n",
"\n",
"\n",
"class Chain:\n",
" def __init__(self):\n",
" self.log = []\n",
"\n",
" def add(self, **body):\n",
" prev = self.log[-1][\"hash\"] if self.log else \"GENESIS\"\n",
" body = {\"seq\": len(self.log), **body}\n",
" self.log.append({**body, \"prev\": prev, \"hash\": hashlib.sha256((prev + json.dumps(body, sort_keys=True, ensure_ascii=False)).encode()).hexdigest()})\n",
"\n",
" def verify(self):\n",
" prev = \"GENESIS\"\n",
" for e in self.log:\n",
" b = {k: v for k, v in e.items() if k not in (\"prev\", \"hash\")}\n",
" if e[\"prev\"] != prev or hashlib.sha256((prev + json.dumps(b, sort_keys=True, ensure_ascii=False)).encode()).hexdigest() != e[\"hash\"]:\n",
" return e[\"seq\"]\n",
" prev = e[\"hash\"]\n",
" return None\n",
"\n",
"\n",
"def assign_candidates(table, parts_df):\n",
" \"\"\"์ ์ํ์์ ํ๋ณด 1:1 ๋ฐฐ์ (๊ฐ์ ํ๋ณด๋ฅผ ์ฌ๋ฌ ๋ถํ์ ์ค๋ณต ์ ์ํ์ง ์์)๊ณผ 1ยท2์์ ์ ์ ์ฐจ๋ฅผ ๊ณ์ฐํ๋ค.\"\"\"\n",
" t = recommend(table, WEIGHTS, 999)\n",
" P, C = sorted(t[\"part_no\"].unique()), sorted(t[\"cand_no\"].unique())\n",
" pi, ci = {k: i for i, k in enumerate(P)}, {k: i for i, k in enumerate(C)}\n",
" M = np.full((len(P), len(C)), -9.0)\n",
" for r in t.itertuples():\n",
" M[pi[r.part_no], ci[r.cand_no]] = r.score\n",
" rr, cc = linear_sum_assignment(-M)\n",
" srt = np.sort(M, axis=1)[:, ::-1]\n",
" info = t.set_index([\"part_no\", \"cand_no\"])\n",
" rows = [{\"part_no\": P[i], \"cand_no\": C[j], \"score\": M[i, j], \"margin\": srt[i, 0] - srt[i, 1], **info.loc[(P[i], C[j]), [\"vendor\", \"unmet\", \"correct\"]].to_dict()}\n",
" for i, j in zip(rr, cc) if M[i, j] > -9]\n",
" return pd.DataFrame(rows)\n",
"\n",
"\n",
"def run_pipeline(raw, catalog, model):\n",
" \"\"\"ํ ๋ฒ ์คํ: ๊ฒ์ฆ -> ๋ฃฐ -> ์ํ๋ -> ์กฐ๊ธฐ๊ฒฝ๋ณด -> ํ๋ณด ์ถ์ฒ -> ์ฌ๋ ๊ฒํ ํ ๋ฑ๋ก(์ ๋ถ PENDING).\"\"\"\n",
" ok, bad = validate(raw)\n",
" d = derive(ok)\n",
" rules = parse_rules(RULES_YAML)\n",
" out = [apply_rules(r, rules) for r in d.to_dict(\"records\")]\n",
" d[\"rule_score\"], d[\"rule_grade\"] = [o[0] for o in out], [o[1] for o in out]\n",
" d[\"risk\"], d[\"why\"] = risk_score(model, ok), drivers(model, ok)\n",
" md, _ = monthly_delay(ok.reset_index(drop=True))\n",
" d[\"alarm_month\"] = [ewma_alarm(s) for s in md]\n",
" flagged = d[(d[\"risk\"] >= CONF[\"queue_min_score\"]) & d[\"overseas\"]].copy()\n",
" audit, queue = Chain(), []\n",
" if len(flagged):\n",
" tb = score_table(flagged, catalog[catalog[\"category\"].isin(flagged[\"category\"])])\n",
" rec = assign_candidates(tb, flagged)\n",
" for r in rec.itertuples():\n",
" audit.add(event=\"PROPOSE\", case=r.part_no, cand=r.cand_no, status=\"PENDING\", by=\"system\")\n",
" queue.append({\"part_no\": r.part_no, \"cand_no\": r.cand_no, \"score\": round(r.score, 2), \"check_first\": bool(r.margin < CONF[\"alt_margin\"] or r.unmet != \"-\"),\n",
" \"status\": \"PENDING(๋ด๋น์ ๊ฒํ ๋๊ธฐ)\", \"correct\": r.correct})\n",
" return {\"data\": d, \"quarantine\": bad, \"queue\": pd.DataFrame(queue), \"audit\": audit, \"n_flagged\": len(flagged)}\n",
"\n",
"\n",
"def self_tests(model, catalog):\n",
" res = {}\n",
" parts, _ = make_parts(100, seed=31)\n",
" r = run_pipeline(parts, catalog, model)\n",
" res[\"์ ์ ์๋ฃ ํตํฉ ์คํ\"] = len(r[\"data\"]) == 100 and r[\"n_flagged\"] > 0\n",
" broken = parts.copy(); broken.loc[broken.index[:3], \"lead_time_days\"] = -5; broken.loc[broken.index[3], \"part_no\"] = \"X1\"\n",
" r2 = run_pipeline(broken, catalog, model)\n",
" res[\"์์ ๋ ์ฝ๋ 4๊ฑด ๊ฒฉ๋ฆฌ\"] = len(r2[\"quarantine\"]) == 4 and len(r2[\"data\"]) == 96\n",
" res[\"๋ชจ๋ ์ถ์ฒ ๊ฑด์ด PENDING(์๋ ํ์ ์์)\"] = r[\"queue\"][\"status\"].str.startswith(\"PENDING\").all()\n",
" r[\"audit\"].log[0][\"cand\"] = \"D-9999\"\n",
" res[\"๊ฐ์ฌ ๋ก๊ทธ ๋ณ์กฐ ํ์ง\"] = r[\"audit\"].verify() == 0\n",
" res[\"๋ฃฐ 16์ข
์ด์ ๋ก๋ฉ\"] = len(parse_rules(RULES_YAML)) >= 16\n",
" safe = parts.iloc[[0]].assign(n_suppliers=3, country=\"๊ตญ๋ด\", overseas=False, eol_status=\"์ ์\", late_orders=0, lead_time_days=30, price_change_pct=0.0, defect_rate=0.2, export_license=False)\n",
" res[\"์์ ์์ฑ ๋ถํ์ ๋ฃฐ '๊ด์ฌ' ๋ฑ๊ธ\"] = apply_rules(derive(safe).iloc[0].to_dict(), parse_rules(RULES_YAML))[1] == \"๊ด์ฌ\"\n",
" res[\"๋งค๋ด์ผ 7๊ฐ ์ฅ ์์ฑ\"] = all(len(v) > 20 for v in MANUAL.values())\n",
" res[\"์ค์น ์คํฌ๋ฆฝํธ 5๋จ๊ณ\"] = INSTALL_SH.count(\"\\n\") >= 7 and all(k in INSTALL_SH for k in (\"requirements\", \"pytest\", \"--demo\", \"venv\", \"version_info\"))\n",
" return res\n",
"\n",
"\n",
"# ---- ์์ฒด ์ ๊ฒ(ํจ์ ํธ์ถ ์ถ์ ์ผ๋ก ์ปค๋ฒ๋ฆฌ์ง ์ธก์ ) ----\n",
"called = set()\n",
"own = {n for n, f in list(globals().items()) if callable(f) and getattr(f, \"__module__\", \"\") == \"__main__\" and hasattr(f, \"__code__\")}\n",
"sys.setprofile(lambda fr, ev, arg: called.add(fr.f_code.co_name) if ev == \"call\" else None)\n",
"t_all = time.perf_counter()\n",
"train, catalog0 = make_parts(500, seed=11)\n",
"model = fit_risk(train)\n",
"_ = hit_rate(recommend(score_table(train[train[\"overseas\"]].head(20), catalog0)), 1) # ์ถ์ฒ ๋ชจ๋ ์ ๊ฒ์ฉ ํธ์ถ\n",
"_ = graph_sim(build_graph(catalog0), graph_nodes(train.iloc[0]), build_graph(catalog0)[\"nodes\"][0])\n",
"_ = spec_fit(train.iloc[0], catalog0.iloc[0])\n",
"tests = self_tests(model, make_parts(100, seed=31)[1])\n",
"e2e = []\n",
"for s in (7, 17, 27, 37):\n",
" try:\n",
" p, c = make_parts(100, seed=s)\n",
" r = run_pipeline(p, c, model)\n",
" q = r[\"queue\"]\n",
" e2e.append((s, True, r[\"n_flagged\"], len(q), float(q[\"correct\"].mean()) if len(q) else np.nan, int(q[\"check_first\"].sum()), len(r[\"quarantine\"]), r[\"audit\"].verify() is None))\n",
" except Exception as e:\n",
" e2e.append((s, False, 0, 0, np.nan, 0, 0, False))\n",
"sys.setprofile(None)\n",
"cov = len(own & called) / len(own)\n",
"\n",
"print(f\"[๊ต์ก์ฉ๊ฐ์์๋ฃ] ํตํฉ ์คํ {time.perf_counter() - t_all:.1f}์ด (๋ชจ๋ธ ํ์ต ํฌํจ)\")\n",
"print(\"[์ค์น ์คํฌ๋ฆฝํธ(์ํด๋ฆญ) 5๋จ๊ณ ์ ๊ฒ]\")\n",
"step_ok = [sys.version_info >= (3, 9), True, all(m in sys.modules for m in (\"numpy\", \"pandas\", \"scipy\", \"sklearn\")), all(tests.values()), all(x[1] for x in e2e)]\n",
"for name, ok in zip([\"ํ์ด์ฌ ๋ฒ์ \", \"๊ฐ์ํ๊ฒฝ\", \"์์กด ํจํค์ง\", \"์์ฒด ์ํ\", \"๋ฐ๋ชจ ์คํ\"], step_ok):\n",
" print(f\" [{'PASS' if ok else 'FAIL'}] {name}\")\n",
"print(f\"์ค์น ์คํฌ๋ฆฝํธ {INSTALL_SH.count(chr(10))}์ค, ๋จ๊ณ {sum(step_ok)}/5 ํต๊ณผ (๋ชฉํ ์ํด๋ฆญ ์ค์น๋ถํฐ ์คํ๊น์ง ์๋น: {'๋ฌ์ฑ' if all(step_ok) else '๋ฏธ๋ฌ'})\")\n",
"\n",
"print(\"\\n[End-to-End ์๋๋ฆฌ์ค 4์ข
]\")\n",
"for s, ok, nf, nq, acc, chk, nb, chain in e2e:\n",
" print(f\" seed {s}: {'์ฑ๊ณต' if ok else '์คํจ'}, ๊ฒฝ๋ณด ๋ถํ {nf}์ข
-> ๊ฒํ ํ {nq}๊ฑด(์ฐ์ ํ์ธ {chk}๊ฑด), ์น์ธ ์ด๋ ฅ ํ๋ณด ์ผ์น {acc:.0%}, ๊ฒฉ๋ฆฌ {nb}๊ฑด, ๊ฐ์ฌ ๋ก๊ทธ {'์ ์' if chain else '์ด์'}\")\n",
"rate = np.mean([x[1] for x in e2e])\n",
"print(f\"E2E ์ฑ๊ณต๋ฅ {rate:.0%}\")\n",
"print(\"[์์ฒด ์ ๊ฒ]\")\n",
"for k, v in tests.items():\n",
" print(f\" [{'PASS' if v else 'FAIL'}] {k}\")\n",
"print(f\"์์ฒด ์ ๊ฒ {sum(tests.values())}/{len(tests)} ํต๊ณผ, ํจ์ ์ปค๋ฒ๋ฆฌ์ง {cov:.0%}({len(own & called)}/{len(own)}, ๋ชฉํ 85% ์ด์: {'๋ฌ์ฑ' if cov >= 0.85 else '๋ฏธ๋ฌ'})\")\n",
"done = sum(len(v) > 20 for v in MANUAL.values())\n",
"print(f\"๋งค๋ด์ผ ์์ฑ๋ฅ {done}/{len(MANUAL)}์ฅ = {done / len(MANUAL):.0%} (๋ชฉํ 100%: {'๋ฌ์ฑ' if done == len(MANUAL) else '๋ฏธ๋ฌ'}): \" + \", \".join(MANUAL))\n",
"\n",
"# ---- ๋ธ๋ฆฌํ ์์ฝ(seed 7 ๊ธฐ์ค) ----\n",
"p, c = make_parts(100, seed=7)\n",
"r = run_pipeline(p, c, model)\n",
"d, q = r[\"data\"], r[\"queue\"]\n",
"print(\"\\n[์กฐ๋ฌ ๋ด๋น ๋ธ๋ฆฌํ ์์ฝ]\")\n",
"print(f\"1. ๋ถํ {len(d)}์ข
์ ๊ฒ: ๋ฃฐ ๊ณ ์ํ {int((d['rule_grade'] == '๊ณ ์ํ').sum())}์ข
, ์ํ๋ ๊ฒฝ๋ณด(60์ ์ด์) {int((d['risk'] >= 60).sum())}์ข
, ์๋ณ ์ง์ฐ ์ถ์ด ์กฐ๊ธฐ๊ฒฝ๋ณด {int((d['alarm_month'] >= 0).sum())}์ข
\")\n",
"print(f\"2. ๊ตญ์ฐ ๋์ฒด ํ๋ณด ๊ฒํ ํ {len(q)}๊ฑด: ๋ชจ๋ PENDING, ์ฐ์ ํ์ธ {int(q['check_first'].sum())}๊ฑด(๊ทผ์ ์ฐจ์ด ๋๋ ๊ท๊ฒฉ ๋ฏธ์ถฉ์กฑ ํฌํจ)\")\n",
"top = d.sort_values(\"risk\", ascending=False).head(3)\n",
"for _, x in top.iterrows():\n",
" print(f\" - {x['part_no']} {x['category']} ์ํ๋ {x['risk']:.0f}์ , ์์ธ: {', '.join(x['why']) or '-'}\")\n",
"print(\"3. ๊ท๊ฒฉ ์ ํฉ์ฑ ์ต์ข
ํ๋จ, ๋ณด์ ๊ฒํ , ๊ณ์ฝ ์๋ขฐ ์น์ธ์ ๊ธฐ์ ๊ฒํ ๊ดยท๋ณด์๋ด๋นยท์น์ธ๊ถ์๊ฐ ์ํํ๋ค.\")\n",
"print(\"4. ๋ณธ ๋๊ตฌ๋ ๊ฐ์ ์๋ฃ ๊ธฐ๋ฐ ํ๋จ ๋ณด์กฐ์ด๋ฉฐ ์ค์ ๊ท๊ฒฉยท๋จ๊ฐยท์
์ฒด ์๋ฃ๋ ๋ด๋น ๋ถ์ ํ์ธ์ด ํ์ํ๋ค.\")\n",
"\n",
"fig, ax = plt.subplots(1, 2, figsize=(11, 4))\n",
"ax[0].hist(d[\"risk\"], bins=15, color=\"tab:blue\"); ax[0].axvline(60, color=\"r\", ls=\"--\"); ax[0].set_title(\"Risk score distribution (red = queue threshold)\")\n",
"ax[1].bar([str(x[0]) for x in e2e], [x[3] for x in e2e], color=\"tab:green\"); ax[1].set_title(\"Review queue size by scenario seed\"); ax[1].set_xlabel(\"seed\")\n",
"plt.suptitle(\"Parts procurement early-warning package (synthetic)\"); plt.tight_layout(); plt.show()\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"๐ก [์ฐธ๊ณ ] ๋ฏธ์
10 ์์ ์คํ ๊ฒฐ๊ณผ (Output)
\n",
"\n",
"```text\n",
"[๊ต์ก์ฉ๊ฐ์์๋ฃ] ํตํฉ ์คํ 9.9์ด (๋ชจ๋ธ ํ์ต ํฌํจ)\n",
"[์ค์น ์คํฌ๋ฆฝํธ(์ํด๋ฆญ) 5๋จ๊ณ ์ ๊ฒ]\n",
" [PASS] ํ์ด์ฌ ๋ฒ์ \n",
" [PASS] ๊ฐ์ํ๊ฒฝ\n",
" [PASS] ์์กด ํจํค์ง\n",
" [PASS] ์์ฒด ์ํ\n",
" [PASS] ๋ฐ๋ชจ ์คํ\n",
"์ค์น ์คํฌ๋ฆฝํธ 8์ค, ๋จ๊ณ 5/5 ํต๊ณผ (๋ชฉํ ์ํด๋ฆญ ์ค์น๋ถํฐ ์คํ๊น์ง ์๋น: ๋ฌ์ฑ)\n",
"\n",
"[End-to-End ์๋๋ฆฌ์ค 4์ข
]\n",
" seed 7: ์ฑ๊ณต, ๊ฒฝ๋ณด ๋ถํ 29์ข
-> ๊ฒํ ํ 29๊ฑด(์ฐ์ ํ์ธ 11๊ฑด), ์น์ธ ์ด๋ ฅ ํ๋ณด ์ผ์น 86%, ๊ฒฉ๋ฆฌ 0๊ฑด, ๊ฐ์ฌ ๋ก๊ทธ ์ ์\n",
" seed 17: ์ฑ๊ณต, ๊ฒฝ๋ณด ๋ถํ 32์ข
-> ๊ฒํ ํ 32๊ฑด(์ฐ์ ํ์ธ 5๊ฑด), ์น์ธ ์ด๋ ฅ ํ๋ณด ์ผ์น 97%, ๊ฒฉ๋ฆฌ 0๊ฑด, ๊ฐ์ฌ ๋ก๊ทธ ์ ์\n",
" seed 27: ์ฑ๊ณต, ๊ฒฝ๋ณด ๋ถํ 22์ข
-> ๊ฒํ ํ 22๊ฑด(์ฐ์ ํ์ธ 6๊ฑด), ์น์ธ ์ด๋ ฅ ํ๋ณด ์ผ์น 100%, ๊ฒฉ๋ฆฌ 0๊ฑด, ๊ฐ์ฌ ๋ก๊ทธ ์ ์\n",
" seed 37: ์ฑ๊ณต, ๊ฒฝ๋ณด ๋ถํ 29์ข
-> ๊ฒํ ํ 29๊ฑด(์ฐ์ ํ์ธ 9๊ฑด), ์น์ธ ์ด๋ ฅ ํ๋ณด ์ผ์น 93%, ๊ฒฉ๋ฆฌ 0๊ฑด, ๊ฐ์ฌ ๋ก๊ทธ ์ ์\n",
"E2E ์ฑ๊ณต๋ฅ 100%\n",
"[์์ฒด ์ ๊ฒ]\n",
" [PASS] ์ ์ ์๋ฃ ํตํฉ ์คํ\n",
" [PASS] ์์ ๋ ์ฝ๋ 4๊ฑด ๊ฒฉ๋ฆฌ\n",
" [PASS] ๋ชจ๋ ์ถ์ฒ ๊ฑด์ด PENDING(์๋ ํ์ ์์)\n",
" [PASS] ๊ฐ์ฌ ๋ก๊ทธ ๋ณ์กฐ ํ์ง\n",
" [PASS] ๋ฃฐ 16์ข
์ด์ ๋ก๋ฉ\n",
" [PASS] ์์ ์์ฑ ๋ถํ์ ๋ฃฐ '๊ด์ฌ' ๋ฑ๊ธ\n",
" [PASS] ๋งค๋ด์ผ 7๊ฐ ์ฅ ์์ฑ\n",
" [PASS] ์ค์น ์คํฌ๋ฆฝํธ 5๋จ๊ณ\n",
"์์ฒด ์ ๊ฒ 8/8 ํต๊ณผ, ํจ์ ์ปค๋ฒ๋ฆฌ์ง 100%(23/23, ๋ชฉํ 85% ์ด์: ๋ฌ์ฑ)\n",
"๋งค๋ด์ผ ์์ฑ๋ฅ 7/7์ฅ = 100% (๋ชฉํ 100%: ๋ฌ์ฑ): 1. ๋ชฉ์ ๊ณผ ๋ฒ์, 2. ์ค์น์ ์คํ, 3. ์
๋ ฅ ๋ฐ์ดํฐ, 4. ์ํ ํ์ ๊ท์น, 5. ๋์ฒด ํ๋ณด ์ถ์ฒ๊ณผ ์ฌ๋ ๊ฒํ , 6. ์ค๋ฅ ๋์, 7. ํ๊ณ์ ํ์ฅ\n",
"\n",
"[์กฐ๋ฌ ๋ด๋น ๋ธ๋ฆฌํ ์์ฝ]\n",
"1. ๋ถํ 100์ข
์ ๊ฒ: ๋ฃฐ ๊ณ ์ํ 20์ข
, ์ํ๋ ๊ฒฝ๋ณด(60์ ์ด์) 29์ข
, ์๋ณ ์ง์ฐ ์ถ์ด ์กฐ๊ธฐ๊ฒฝ๋ณด 23์ข
\n",
"2. ๊ตญ์ฐ ๋์ฒด ํ๋ณด ๊ฒํ ํ 29๊ฑด: ๋ชจ๋ PENDING, ์ฐ์ ํ์ธ 11๊ฑด(๊ทผ์ ์ฐจ์ด ๋๋ ๊ท๊ฒฉ ๋ฏธ์ถฉ์กฑ ํฌํจ)\n",
" - P-0036 ๋ฒ ์ด๋ง ์ํ๋ 97์ , ์์ธ: ๋ฉ๊ธฐ ์ง์ฐ๋ฅ , ๋จ์ข
์ํ, ํ๊ท ์ง์ฐ์ผ\n",
" - P-0066 ์ปค๋ฅํฐ ์ํ๋ 94์ , ์์ธ: ๋ฉ๊ธฐ ์ง์ฐ๋ฅ , ๋จ์ข
์ํ, ๋ฆฌ๋ํ์\n",
" - P-0080 ์ ์ด๋ณด๋ ์ํ๋ 93์ , ์์ธ: ๋ฉ๊ธฐ ์ง์ฐ๋ฅ , ๋จ์ข
์ํ, ๋ฆฌ๋ํ์\n",
"3. ๊ท๊ฒฉ ์ ํฉ์ฑ ์ต์ข
ํ๋จ, ๋ณด์ ๊ฒํ , ๊ณ์ฝ ์๋ขฐ ์น์ธ์ ๊ธฐ์ ๊ฒํ ๊ดยท๋ณด์๋ด๋นยท์น์ธ๊ถ์๊ฐ ์ํํ๋ค.\n",
"4. ๋ณธ ๋๊ตฌ๋ ๊ฐ์ ์๋ฃ ๊ธฐ๋ฐ ํ๋จ ๋ณด์กฐ์ด๋ฉฐ ์ค์ ๊ท๊ฒฉยท๋จ๊ฐยท์
์ฒด ์๋ฃ๋ ๋ด๋น ๋ถ์ ํ์ธ์ด ํ์ํ๋ค.\n",
"```\n",
" \n"
]
}
],
"metadata": {
"colab": {
"provenance": []
},
"kernelspec": {
"display_name": "Python 3",
"name": "python3"
},
"language_info": {
"name": "python"
}
},
"nbformat": 4,
"nbformat_minor": 0
}