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