{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# ๐ŸŽฏ [QLT02] ๊ทœ๊ฒฉยท๊ตฌ๋งค์š”๊ตฌยท๋‚ฉ๊ธฐ ์—ฐ๊ณ„ ํ’ˆ์งˆ์˜ˆ์ธก ๋ชจ๋ธ\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/QLT02/QLT02_solution.ipynb)\n", "\n", "---\n", "\n", "## ๐Ÿ“Œ 1. ํ”„๋กœ์ ํŠธ ๊ฐœ์š” ๋ฐ ๋ฐฐ๊ฒฝ\n", "\n", "| ํ•ญ๋ชฉ | ๋‚ด์šฉ |\n", "| :--- | :--- |\n", "| **๊ณผ์ • ๋ถ„๋ฅ˜** | ํ’ˆ์งˆยท๊ทœ๊ฒฉยท๊ฒ€์‚ฌ (Level 3) |\n", "| **ํ•™์Šต ์†Œ์š” ์‹œ๊ฐ„** | 10๊ฐœ ๋ฏธ์…˜ (5์ผ / ์ด 28.5์‹œ๊ฐ„) |\n", "| **ํ™˜๊ฒฝ ์‚ฌ์–‘** | Python 3.10+, numpy, pandas, matplotlib, scipy, scikit-learn |\n", "\n", "### ๐Ÿ“– ํ”„๋กœ์ ํŠธ ๋ชฉํ‘œ\n", "๋ฏธ๊ตญ ๊ตญ๋ฐฉ๋ถ€(DoD) ํ’ˆ์งˆ๋ณด์ฆ ๋ถ€์„œ๋Š” ๊ตฌ๋งค์š”๊ตฌ์„œ์˜ ๊ทœ๊ฒฉ, ๊ณ„์•ฝ ๋‚ฉ๊ธฐ, ์—…์ฒด ์ด๋ ฅ๊ณผ ์ˆ˜๋ น๊ฒ€์‚ฌ ๊ฒฐ๊ณผ๋ฅผ ์—ฐ๊ฒฐํ•ด ๋ถˆ๋Ÿ‰ ๋กœํŠธ๋ฅผ ๋ฏธ๋ฆฌ ๊ฐ€๋ ค๋‚ด๋Š” ํ’ˆ์งˆ์˜ˆ์ธก ๋ฐฉ์‹์„ ๊ณต๊ฐœ ์ž๋ฃŒ๋กœ ์†Œ๊ฐœํ•ด ์™”๋‹ค. ๋ณธ ํ”„๋กœ์ ํŠธ๋Š” ์ด ์‚ฌ๋ก€๋ฅผ ๋ฐ”ํƒ•์œผ๋กœ ๊ตฌ๋งค์š”๊ตฌ-๊ณ„์•ฝ-๋‚ฉํ’ˆ-์ˆ˜๋ น๊ฒ€์‚ฌ-์‚ฌํ›„ ํ™•์ •์œผ๋กœ ์ด์–ด์ง€๋Š” ๊ต์œก์šฉ ๊ฐ€์ƒ ๋กœํŠธ ์ž๋ฃŒ๋ฅผ ์ฝ”๋“œ๋กœ ๋งŒ๋“ค์–ด, ๊ทœ๊ฒฉยท์ œ์› ์ •ํ•ฉ์„ฑ, ๊ฒ€์‚ฌ ๋ณ‘๋ชฉ, ํ•˜์ž์™€์˜ ์—ฐ๊ฒฐ, ์ด์ƒ ์‹ ํ˜ธ, ์„ค๋ช… ๊ทผ๊ฑฐ, ํ’ˆ์งˆ ๊ฐœ์„  ํšจ๊ณผ๋ฅผ ์ฐจ๋ก€๋กœ ๋ถ„์„ํ•œ๋‹ค. ํ•™์Šต์ž๋Š” ๋ฐ์ดํ„ฐ์…‹ ์ •์˜, ๊ฒฐ์ธก ์ •์ œ์™€ ์—…์ฒด ์ด๋ ฅ ์ธ์ฝ”๋”ฉ, ์ƒ˜ํ”Œ๋ง ํŒ์ • Baseline๊ณผ ํƒ์ƒ‰์  ๋ถ„์„, ์€๋‹‰ ๊ฒฐํ•จ ํŒจํ„ด ์‹๋ณ„, ํ’ˆ์งˆ ์œ„ํ—˜ ์ง€์ˆ˜์™€ ์กฐ๊ธฐ๊ฒฝ๋ณด, ๋Œ€์‹œ๋ณด๋“œ์™€ ์„ค๋ช… ๊ทผ๊ฑฐ, ๊ฒ€์‚ฌ๊ด€ ์žฌ๊ฒ€์‚ฌ(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", "๊ตฐ์ˆ˜ํ’ˆ ์ˆ˜์ž…/์ˆ˜๋ น ๊ฒ€์‚ฌ ๋ถˆ๋Ÿ‰ ๊ธฐ๋ก(๊ทœ๊ฒฉ ๋ฏธ๋‹ฌ, ์น˜์ˆ˜ ์˜ค์ฐจ) ๋ฐ์ดํ„ฐ์…‹์„ ์ •์˜ํ•œ๋‹ค. ์‹ค์ œ ์ „์ˆ  ํ˜„์žฅ์—์„œ๋Š” ๋‹จ์ˆœ ํƒ์ง€์— ๊ทธ์น˜์ง€ ์•Š๊ณ  ์‹ ์†ํ•œ ์œ„ํ˜‘ ํŒ๋‹จ๊ณผ ์‹ ๋ขฐ๋„ ๋†’์€ ์˜์‚ฌ๊ฒฐ์ •์ด ํ•„์ˆ˜์ ์ด๋ฏ€๋กœ, **๊ตฐ์ˆ˜ํ’ˆ ์ˆ˜์ž…ยท์ˆ˜๋ น ๊ฒ€์‚ฌ ๋ถˆ๋Ÿ‰ ๊ธฐ๋ก(๊ทœ๊ฒฉ ๋ฏธ๋‹ฌ, ์น˜์ˆ˜ ์˜ค์ฐจ, ์™ธ๊ด€ ๊ฒฐํ•จ)์„ ๊ตฌ๋งค์š”๊ตฌ-๊ณ„์•ฝ-๋‚ฉํ’ˆ-๊ฒ€์‚ฌ ํ๋ฆ„์— ๋งž์ถฐ ๋ฐ์ดํ„ฐ์…‹์œผ๋กœ ์ •์˜ํ•œ๋‹ค.** ๊ณผ์—…์„ ์™„์ˆ˜ํ•˜์—ฌ ์˜คํŒ์„ ๋ฐฉ์ง€ํ•˜๊ณ  ์ž‘์ „ ๋Œ€์‘๋ ฅ์„ ๊ทน๋Œ€ํ™”ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.\n", "\n", "์ด ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด ๋ณธ ๋ฏธ์…˜์—์„œ๋Š” **์—…๋ฌด ํ”„๋กœ์„ธ์Šค ๋งต(๊ตฌ๋งค์š”๊ตฌ, ๊ณ„์•ฝ, ๋‚ฉํ’ˆ, ์ˆ˜๋ น๊ฒ€์‚ฌ, ๋ถˆ์ถœยท์‚ฌ์šฉ, ์‚ฌํ›„ ํ™•์ •)์„ ๊ทธ๋ฆฌ๊ณ  ๋‹จ๊ณ„๋ณ„๋กœ ์ƒ๊ธฐ๋Š” ๋ฐ์ดํ„ฐ ์ •๋ฆฌํ•˜๊ณ  ๊ทœ๊ฒฉํ™”ํ•˜๋Š”** ์‹ค์Šต์„ ์ฒด๊ณ„์ ์œผ๋กœ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค.\n", "\n", "ํ•™์Šต์ž๋Š” ์‹ค์Šต๊ฐ€์ด๋“œ์— ์ œ์‹œ๋œ ์ด 4๊ฐœ ์„ธ๋ถ€ ํ™œ๋™(๊ธฐ๋ณธ ๊ธฐ๋Šฅ ๊ตฌํ˜„ โž” ๊ฒฐ๊ณผ ์ง€ํ‘œ ๊ฒ€์ฆ โž” ํŒŒ๋ผ๋ฏธํ„ฐ ํŠœ๋‹ ๋ฐ ๋„์ „๊ณผ์ œ)์„ ์ˆœ์ฐจ์ ์œผ๋กœ ์ง„ํ–‰ํ•˜๋ฉฐ ํ•ต์‹ฌ ํŒ์ • ๊ธฐ์ค€์„ ๋‹ฌ์„ฑํ•ฉ๋‹ˆ๋‹ค.\n", "\n", "**์ด๋ฒˆ ๊ฒฐ๊ณผ๋ฌผ:** `docs/qlt02_logistics_domain_spec.md ๋ฐ ๋ฐ์ดํ„ฐ ๋”•์…”๋„ˆ๋ฆฌ` ํ˜•ํƒœ์˜ ๊ฒฐ๊ณผ๋ฌผ์„ ์™„์„ฑํ•œ๋‹ค.\n", "\n", "### ๐Ÿ” ํ‰๊ฐ€ ๋ฐ ๊ฒ€์ฆ ๊ธฐ์ค€ (PASS ์กฐ๊ฑด)\n", "#### ๋ฏธ์…˜ ํŒ์ •\n", "\n", "**ํ‰๊ฐ€ ๊ตฌ๋ถ„:** ํ•„์ˆ˜\n", "\n", "**ํ•ต์‹ฌ ํ‰๊ฐ€์˜์—ญ:** ๊ตฐ์ˆ˜ ์‹ค๋ฌด ์—…๋ฌด ํ”„๋กœ์„ธ์Šค์™€์˜ ์ผ์น˜์„ฑ ๋ฐ ๋ถ„์„ ๋ชฉํ‘œ์˜ ๋ช…ํ™•์„ฑ\n", "\n", "#### ๋ฏธ์…˜ PASS ์กฐ๊ฑด\n", "\n", "- ์ •๋Ÿ‰ ๊ธฐ์ค€: ์—…๋ฌด ํ”„๋กœ์„ธ์Šค ๋งต ์ž‘์„ฑ ์™„๋ฃŒ, ํ•„์š” ํ”ผ์ฒ˜(Feature) ๋ชฉ๋ก 20๊ฐœ ์ด์ƒ ์ •์˜\n", "\n", "\n", "- ์ง„ํ–‰ ๊ธฐ์ค€: ๋„๋ฉ”์ธ ๋ถ„์„์„œ ๋ฐ ๋ฐ์ดํ„ฐ ์š”๊ตฌ๋ช…์„ธ์„œ ์Šน์ธ\n", "\n", "\n", "- ๊ฒ€์ฆ ๋ฐฉ๋ฒ•: ๊ตฐ์ˆ˜ ๋‹ด๋‹น ์‹ค๋ฌด์ž ์ธํ„ฐ๋ทฐ ๋ฐ ๊ธฐ์กด ์ „์‚ฐ์ฒด๊ณ„ ๋ฐ์ดํ„ฐ ๊ตฌ์กฐ ๋Œ€์กฐ\n", "\n", "\n", "#### ์ฑ„์ ์ž ํŒ์ • ๋ฉ”๋ชจ\n", "\n", "> ๋‹จ์ˆœ ์ฝ”๋“œ ์‹คํ–‰ ์—ฌ๋ถ€๋ฟ๋งŒ ์•„๋‹ˆ๋ผ ์ถœ๋ ฅ๋œ ์ˆ˜์น˜์™€ ๊ทผ๊ฑฐ ์ž๋ฃŒ๊ฐ€ ๊ตฐ ์‹ค๋ฌด ์š”๊ตฌ์‚ฌํ•ญ์— ๋ถ€ํ•ฉํ•˜๋Š”์ง€ ๋ฉด๋ฐ€ํžˆ ํ™•์ธํ•œ๋‹ค.\n", "\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "\"\"\"๊ตฐ์ˆ˜ํ’ˆ ์ˆ˜์ž…ยท์ˆ˜๋ น ๊ฒ€์‚ฌ ๋ถˆ๋Ÿ‰ ๊ธฐ๋ก(๊ทœ๊ฒฉ ๋ฏธ๋‹ฌยท์น˜์ˆ˜ ์˜ค์ฐจยท์™ธ๊ด€ ๊ฒฐํ•จ) ๋ฐ์ดํ„ฐ์…‹์„ ๊ตฌ๋งค์š”๊ตฌยท๊ณ„์•ฝ(๋‚ฉ๊ธฐ)ยท๋‚ฉํ’ˆยท๊ฒ€์‚ฌ ํ๋ฆ„์— ๋งž์ถฐ ์ •์˜ํ•˜๊ณ , ๊ตญ๋ฐฉ ํ‘œ์ค€ ๊ตฐ์ˆ˜ ์ฝ”๋“œ(FSCยทNIIN) ํ˜•์‹์„ ๋ฐ˜์˜ํ•œ ๋ฐ์ดํ„ฐ ๋”•์…”๋„ˆ๋ฆฌ์™€ ๊ฒ€์ฆ๊ธฐ๋ฅผ ๋งŒ๋“ ๋‹ค.\"\"\"\n", "import json\n", "import re\n", "import numpy as np\n", "import pandas as pd\n", "\n", "# 1) ์—…๋ฌด ํ”„๋กœ์„ธ์Šค ๋งต: ๋‹จ๊ณ„๋งˆ๋‹ค ์–ด๋–ค ๋ฐ์ดํ„ฐ๊ฐ€ ์ƒ๊ธฐ๋Š”์ง€ ์ •๋ฆฌ\n", "PROCESS = [(\"๊ตฌ๋งค์š”๊ตฌ\", \"์‚ฌ์šฉ๋ถ€๋Œ€ยท๋ณด๊ธ‰๋ถ€์„œ\", \"ํ’ˆ๋ชฉ(NIIN), ๊ทœ๊ฒฉ ๋“ฑ๊ธ‰ยท๊ฐœ์ •์ฐจ์ˆ˜, ์š”๊ตฌ์ˆ˜๋Ÿ‰, ๊ธด๊ธ‰ ์—ฌ๋ถ€\"),\n", " (\"๊ณ„์•ฝ\", \"์กฐ๋‹ฌ ๋‹ด๋‹น\", \"์—…์ฒด, ๊ณ„์•ฝ ๋‚ฉ๊ธฐ(์ผ), ๊ตฌ๋งค์š”๊ตฌ ๋Œ€๋น„ ๊ทœ๊ฒฉ ๋ณ€๊ฒฝ ์—ฌ๋ถ€\"),\n", " (\"๋‚ฉํ’ˆ\", \"์—…์ฒด\", \"๋กœํŠธ ๋ฒˆํ˜ธ, ๋กœํŠธ ํฌ๊ธฐ, ์‹ค์ œ ๋‚ฉํ’ˆ์ผ(์ง€์—ฐ์ผ)\"),\n", " (\"์ˆ˜๋ น๊ฒ€์‚ฌ\", \"ํ’ˆ์งˆ ๊ฒ€์‚ฌ๊ด€\", \"ํ‘œ๋ณธ ์ˆ˜, ๋ถˆ๋Ÿ‰ ์ˆ˜(์œ ํ˜•๋ณ„), ์น˜์ˆ˜ ํŽธ์ฐจ ์ธก์ •๊ฐ’, ํ•ฉ๋ถ€ ํŒ์ •\"),\n", " (\"๋ถˆ์ถœยท์‚ฌ์šฉ\", \"๋ณด๊ธ‰๋ถ€์„œยท์‚ฌ์šฉ๋ถ€๋Œ€\", \"๊ธด๊ธ‰ ๋ถˆ์ถœ ์—ฌ๋ถ€, ์‚ฌ์šฉ๋ถ€๋Œ€ ํ•˜์žยท๋ถˆ๋งŒ ์ ‘์ˆ˜\"),\n", " (\"์‚ฌํ›„ ํ™•์ •\", \"ํ’ˆ์งˆ ๋‹ด๋‹น\", \"์ „์ˆ˜๊ฒ€์‚ฌยทํ•˜์ž ๋ฐ˜์˜ ํ™•์ • ๋ถˆ๋Ÿ‰๋ฅ \")]\n", "print(\"[์—…๋ฌด ํ”„๋กœ์„ธ์Šค ๋งต] \" + \" -> \".join(p[0] for p in PROCESS))\n", "for i, (step, who, data) in enumerate(PROCESS, 1):\n", " print(f\" {i}. {step:5s} | {who:10s} | ์ƒ์„ฑ ๋ฐ์ดํ„ฐ: {data}\")\n", "\n", "# 2) ๋ฐ์ดํ„ฐ ๋”•์…”๋„ˆ๋ฆฌ(์Šคํ‚ค๋งˆ): ๋‹จ๊ณ„ / ํ•„๋“œ / ์ž๋ฃŒํ˜• / ๊ทœ์น™ / ์„ค๋ช…\n", "S = [\n", " (\"๊ตฌ๋งค์š”๊ตฌ\", \"niin\", str, {\"pattern\": r\"^\\d{4}-\\d{2}-\\d{3}-\\d{4}$\"}, \"FSC 4์ž๋ฆฌ + NIIN 9์ž๋ฆฌ(๊ฐ€์ƒ) ์žฌ๊ณ ๋ฒˆํ˜ธ\"),\n", " (\"๊ตฌ๋งค์š”๊ตฌ\", \"fsc\", str, {\"enum\": [\"5305\", \"5330\", \"5935\", \"6135\", \"2530\", \"8415\"]}, \"์—ฐ๋ฐฉ ๋ณด๊ธ‰ ๋ถ„๋ฅ˜(ํ’ˆ๋ชฉ๊ตฐ)\"),\n", " (\"๊ตฌ๋งค์š”๊ตฌ\", \"spec_class\", str, {\"enum\": [\"precision\", \"general\"]}, \"๊ทœ๊ฒฉ ๋“ฑ๊ธ‰(์ •๋ฐ€/์ผ๋ฐ˜)\"),\n", " (\"๊ตฌ๋งค์š”๊ตฌ\", \"tolerance_mm\", float, {\"min\": 0.01, \"max\": 1.0}, \"์น˜์ˆ˜ ๊ณต์ฐจ(mm)\"),\n", " (\"๊ตฌ๋งค์š”๊ตฌ\", \"spec_rev\", int, {\"min\": 1, \"max\": 20}, \"๊ทœ๊ฒฉ์„œ ๊ฐœ์ •์ฐจ์ˆ˜\"),\n", " (\"๊ตฌ๋งค์š”๊ตฌ\", \"pr_qty\", int, {\"min\": 1, \"max\": 100000}, \"๊ตฌ๋งค์š”๊ตฌ ์ˆ˜๋Ÿ‰\"),\n", " (\"๊ตฌ๋งค์š”๊ตฌ\", \"pr_urgent\", int, {\"enum\": [0, 1]}, \"๊ธด๊ธ‰ ๊ตฌ๋งค์š”๊ตฌ ์—ฌ๋ถ€\"),\n", " (\"๊ณ„์•ฝ\", \"vendor\", str, {\"pattern\": r\"^V\\d{2}$\"}, \"๋‚ฉํ’ˆ์—…์ฒด ์ฝ”๋“œ(๊ฐ€์ƒ)\"),\n", " (\"๊ณ„์•ฝ\", \"lead_days\", int, {\"min\": 1, \"max\": 365}, \"๊ณ„์•ฝ ๋‚ฉ๊ธฐ(์ผ)\"),\n", " (\"๊ณ„์•ฝ\", \"spec_changed\", int, {\"enum\": [0, 1]}, \"๊ตฌ๋งค์š”๊ตฌ ํ›„ ๊ทœ๊ฒฉ ๋ณ€๊ฒฝ ์—ฌ๋ถ€\"),\n", " (\"๊ณ„์•ฝ\", \"spec_mismatch\", int, {\"enum\": [0, 1]}, \"๊ตฌ๋งค์š”๊ตฌยท๊ณ„์•ฝ ๊ทœ๊ฒฉ ๋ถˆ์ผ์น˜ ์—ฌ๋ถ€\"),\n", " (\"๋‚ฉํ’ˆ\", \"lot_id\", str, {\"pattern\": r\"^LOT\\d{2}-\\d{5}$\"}, \"๋กœํŠธ ๋ฒˆํ˜ธ\"),\n", " (\"๋‚ฉํ’ˆ\", \"lot_size\", int, {\"min\": 1, \"max\": 100000}, \"๋กœํŠธ ํฌ๊ธฐ(๊ฐœ)\"),\n", " (\"๋‚ฉํ’ˆ\", \"delay_days\", int, {\"min\": -30, \"max\": 180}, \"๋‚ฉ๊ธฐ ์ง€์—ฐ์ผ(์Œ์ˆ˜=์กฐ๊ธฐ ๋‚ฉํ’ˆ)\"),\n", " (\"์ˆ˜๋ น๊ฒ€์‚ฌ\", \"receipt_date\", str, {\"pattern\": r\"^\\d{4}-\\d{2}-\\d{2}$\"}, \"์ˆ˜๋ น๊ฒ€์‚ฌ์ผ\"),\n", " (\"์ˆ˜๋ น๊ฒ€์‚ฌ\", \"inspector\", str, {\"pattern\": r\"^INS-\\d$\"}, \"๊ฒ€์‚ฌ๊ด€ ์ฝ”๋“œ\"),\n", " (\"์ˆ˜๋ น๊ฒ€์‚ฌ\", \"sample_n\", int, {\"min\": 1, \"max\": 1250}, \"ํ‘œ๋ณธ ์ˆ˜\"),\n", " (\"์ˆ˜๋ น๊ฒ€์‚ฌ\", \"defects\", int, {\"min\": 0}, \"ํ‘œ๋ณธ ๋ถˆ๋Ÿ‰ ์ˆ˜\"),\n", " (\"์ˆ˜๋ น๊ฒ€์‚ฌ\", \"def_spec\", int, {\"min\": 0}, \"๊ทœ๊ฒฉ ๋ฏธ๋‹ฌ ์ˆ˜\"),\n", " (\"์ˆ˜๋ น๊ฒ€์‚ฌ\", \"def_dim\", int, {\"min\": 0}, \"์น˜์ˆ˜ ์˜ค์ฐจ ์ˆ˜\"),\n", " (\"์ˆ˜๋ น๊ฒ€์‚ฌ\", \"def_visual\", int, {\"min\": 0}, \"์™ธ๊ด€ ๊ฒฐํ•จ ์ˆ˜\"),\n", " (\"์ˆ˜๋ น๊ฒ€์‚ฌ\", \"dim_dev_mm\", float, {\"min\": 0, \"max\": 5}, \"์น˜์ˆ˜ ํŽธ์ฐจ ํ‰๊ท (mm)\"),\n", " (\"๋ถˆ์ถœยท์‚ฌ์šฉ\", \"emergency_release\", int, {\"enum\": [0, 1]}, \"๊ธด๊ธ‰ ๋ถˆ์ถœ(๊ฒ€์‚ฌ ์ „ ์„ ๋ถˆ์ถœ) ์˜ˆ์™ธ ์—ฌ๋ถ€\"),\n", " (\"๋ถˆ์ถœยท์‚ฌ์šฉ\", \"field_claims\", int, {\"min\": 0}, \"์‚ฌ์šฉ๋ถ€๋Œ€ ํ•˜์žยท๋ถˆ๋งŒ ์ ‘์ˆ˜ ๊ฑด์ˆ˜\"),\n", " (\"์‚ฌํ›„ ํ™•์ •\", \"confirmed_rate\", float, {\"min\": 0, \"max\": 1}, \"ํ™•์ • ๋ถˆ๋Ÿ‰๋ฅ (๋ถ„์„ ๋ชฉํ‘œ ๋ณ€์ˆ˜)\"),\n", "]\n", "SCHEMA = {f: (t, r) for _, f, t, r, _ in S}\n", "CROSS_RULES = [\"ํ‘œ๋ณธ ์ˆ˜ <= ๋กœํŠธ ํฌ๊ธฐ\", \"์œ ํ˜•๋ณ„ ๋ถˆ๋Ÿ‰ ํ•ฉ = ํ‘œ๋ณธ ๋ถˆ๋Ÿ‰ ์ˆ˜\", \"๋ถˆ๋Ÿ‰ ์ˆ˜ <= ํ‘œ๋ณธ ์ˆ˜\", \"niin ์•ž 4์ž๋ฆฌ = fsc\"]\n", "\n", "\n", "def validate(rec):\n", " errs = [f\"{f}: ํ•„์ˆ˜ ํ•„๋“œ ๋ˆ„๋ฝ\" for f in SCHEMA if f not in rec]\n", " for f, (t, r) in SCHEMA.items():\n", " if f not in rec:\n", " continue\n", " v = rec[f]\n", " if not isinstance(v, t) or isinstance(v, bool):\n", " errs.append(f\"{f}: ์ž๋ฃŒํ˜• ์˜ค๋ฅ˜({type(v).__name__})\"); continue\n", " if \"enum\" in r and v not in r[\"enum\"]:\n", " errs.append(f\"{f}: ํ—ˆ์šฉ๋˜์ง€ ์•Š์€ ๊ฐ’ '{v}'\")\n", " if \"min\" in r and v < r[\"min\"]:\n", " errs.append(f\"{f}: ์ตœ์†Œ๊ฐ’ {r['min']} ๋ฏธ๋งŒ\")\n", " if \"max\" in r and v > r[\"max\"]:\n", " errs.append(f\"{f}: ์ตœ๋Œ€๊ฐ’ {r['max']} ์ดˆ๊ณผ\")\n", " if \"pattern\" in r and not re.match(r[\"pattern\"], v):\n", " errs.append(f\"{f}: ํ˜•์‹ ๋ถˆ์ผ์น˜ '{v}'\")\n", " if not errs:\n", " if rec[\"sample_n\"] > rec[\"lot_size\"]:\n", " errs.append(\"๊ต์ฐจ๊ฒ€์ฆ: ํ‘œ๋ณธ ์ˆ˜๊ฐ€ ๋กœํŠธ ํฌ๊ธฐ๋ณด๋‹ค ํผ\")\n", " if rec[\"def_spec\"] + rec[\"def_dim\"] + rec[\"def_visual\"] != rec[\"defects\"]:\n", " errs.append(\"๊ต์ฐจ๊ฒ€์ฆ: ์œ ํ˜•๋ณ„ ๋ถˆ๋Ÿ‰ ํ•ฉ ๋ถˆ์ผ์น˜\")\n", " if rec[\"defects\"] > rec[\"sample_n\"]:\n", " errs.append(\"๊ต์ฐจ๊ฒ€์ฆ: ๋ถˆ๋Ÿ‰ ์ˆ˜๊ฐ€ ํ‘œ๋ณธ ์ˆ˜๋ณด๋‹ค ํผ\")\n", " if rec[\"niin\"][:4] != rec[\"fsc\"]:\n", " errs.append(\"๊ต์ฐจ๊ฒ€์ฆ: ์žฌ๊ณ ๋ฒˆํ˜ธ์™€ ํ’ˆ๋ชฉ๊ตฐ ๋ถˆ์ผ์น˜\")\n", " return errs\n", "\n", "\n", "# 3) [๊ต์œก์šฉ๊ฐ€์ƒ์ž๋ฃŒ] ์ƒ˜ํ”Œ 20๊ฑด ์ƒ์„ฑ\n", "rng = np.random.default_rng(3)\n", "recs = []\n", "for i in range(20):\n", " fsc = str(rng.choice([\"5305\", \"5330\", \"5935\", \"6135\", \"2530\", \"8415\"]))\n", " n = int(rng.choice([50, 80, 125])); dfx = [int(x) for x in rng.multinomial(int(rng.integers(0, 6)), [0.4, 0.4, 0.2])]\n", " prec = bool(rng.random() < 0.4)\n", " recs.append({\"niin\": f\"{fsc}-01-{int(rng.integers(100, 999))}-{int(rng.integers(1000, 9999))}\", \"fsc\": fsc,\n", " \"spec_class\": \"precision\" if prec else \"general\", \"tolerance_mm\": round(float(rng.uniform(0.05, 0.1) if prec else rng.uniform(0.2, 0.5)), 3),\n", " \"spec_rev\": int(rng.integers(1, 6)), \"pr_qty\": int(rng.integers(500, 5000)), \"pr_urgent\": int(rng.random() < 0.2),\n", " \"vendor\": f\"V{int(rng.integers(1, 13)):02d}\", \"lead_days\": int(rng.integers(30, 150)), \"spec_changed\": int(rng.random() < 0.1),\n", " \"spec_mismatch\": 0, \"lot_id\": f\"LOT01-{i:05d}\", \"lot_size\": int(rng.integers(300, 3000)), \"delay_days\": int(rng.integers(-3, 20)),\n", " \"receipt_date\": f\"2025-{int(rng.integers(1, 13)):02d}-{int(rng.integers(1, 29)):02d}\", \"inspector\": f\"INS-{int(rng.integers(1, 5))}\",\n", " \"sample_n\": n, \"defects\": sum(dfx), \"def_spec\": dfx[0], \"def_dim\": dfx[1], \"def_visual\": dfx[2],\n", " \"dim_dev_mm\": round(float(rng.uniform(0.01, 0.2)), 4), \"emergency_release\": 0, \"field_claims\": int(rng.poisson(1)),\n", " \"confirmed_rate\": round(float(rng.uniform(0.005, 0.08)), 4)})\n", "ok = sum(not validate(r) for r in recs)\n", "print(f\"\\n์ •์ƒ ์ƒ˜ํ”Œ {len(recs)}๊ฑด ์ค‘ ๊ฒ€์ฆ ํ†ต๊ณผ {ok}๊ฑด, ์˜ค๋ฅ˜ {len(recs) - ok}๊ฑด\")\n", "\n", "# 4) ์˜ค๋ฅ˜ ์ฃผ์ž… ์‹œํ—˜\n", "bad = json.loads(json.dumps(recs[0]))\n", "bad[\"niin\"] = \"5305011234567\" # ํ•˜์ดํ”ˆ ์—†๋Š” ์žฌ๊ณ ๋ฒˆํ˜ธ\n", "bad[\"vendor\"] = \"์—…์ฒดA\" # ์ฝ”๋“œ ํ˜•์‹ ์œ„๋ฐ˜\n", "bad[\"spec_class\"] = \"high\" # ํ—ˆ์šฉ๊ฐ’ ์œ„๋ฐ˜\n", "bad[\"lot_size\"] = -10 # ๋ฒ”์œ„ ์œ„๋ฐ˜\n", "del bad[\"receipt_date\"] # ํ•„์ˆ˜ ํ•„๋“œ ๋ˆ„๋ฝ\n", "print(\"\\n[์˜ค๋ฅ˜ ์ฃผ์ž… ๊ฒฐ๊ณผ]\")\n", "for e in validate(bad):\n", " print(\" -\", e)\n", "bad2 = json.loads(json.dumps(recs[1])); bad2[\"def_dim\"] += 2; bad2[\"niin\"] = \"9999\" + bad2[\"niin\"][4:]\n", "print(\" - (๊ต์ฐจ๊ฒ€์ฆ) \" + \" / \".join(validate(bad2)))\n", "\n", "# 5) ๋ฐ์ดํ„ฐ ๋”•์…”๋„ˆ๋ฆฌ ์š”์•ฝ๊ณผ ๋ชฉํ‘œ ์ ๊ฒ€\n", "dd = pd.DataFrame([(s, f, t.__name__, json.dumps(r, ensure_ascii=False)) for s, f, t, r, _ in S], columns=[\"๋‹จ๊ณ„\", \"ํ•„๋“œ\", \"์ž๋ฃŒํ˜•\", \"๊ทœ์น™\"])\n", "print(\"\\n[๋ฐ์ดํ„ฐ ๋”•์…”๋„ˆ๋ฆฌ ์š”์•ฝ: ๋‹จ๊ณ„๋ณ„ ํ•„๋“œ ์ˆ˜]\")\n", "print(dd.groupby(\"๋‹จ๊ณ„\", sort=False)[\"ํ•„๋“œ\"].agg([\"count\", lambda x: \", \".join(x)]).rename(columns={\"count\": \"ํ•„๋“œ ์ˆ˜\", \"\": \"ํ•„๋“œ\"}).to_string())\n", "print(f\"\\nํ”ผ์ฒ˜(ํ•„๋“œ) {len(S)}๊ฐœ ์ •์˜ (๋ชฉํ‘œ 20๊ฐœ ์ด์ƒ: {'๋‹ฌ์„ฑ' if len(S) >= 20 else '๋ฏธ๋‹ฌ'}), ๊ต์ฐจ๊ฒ€์ฆ ๊ทœ์น™ {len(CROSS_RULES)}์ข…, ํ”„๋กœ์„ธ์Šค ๋งต {len(PROCESS)}๋‹จ๊ณ„ ์ž‘์„ฑ\")\n", "print(\"๋ถ„์„ ๋ชฉํ‘œ: ๊ตฌ๋งค์š”๊ตฌยท๊ทœ๊ฒฉยท๋‚ฉ๊ธฐ ์ •๋ณด๋กœ ๋กœํŠธ ํ™•์ • ๋ถˆ๋Ÿ‰๋ฅ ์„ ์ˆ˜๋ น๊ฒ€์‚ฌ ์‹œ์ ์— ๋ฏธ๋ฆฌ ์˜ˆ์ธกํ•ด ๊ณ ์œ„ํ—˜ ๋กœํŠธ๋ฅผ ๋จผ์ € ์ •๋ฐ€ ๊ฒ€์‚ฌํ•œ๋‹ค.\")\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "
\n", "๐Ÿ’ก [์ฐธ๊ณ ] ๋ฏธ์…˜ 01 ์˜ˆ์ƒ ์‹คํ–‰ ๊ฒฐ๊ณผ (Output)\n", "\n", "```text\n", "[์—…๋ฌด ํ”„๋กœ์„ธ์Šค ๋งต] ๊ตฌ๋งค์š”๊ตฌ -> ๊ณ„์•ฝ -> ๋‚ฉํ’ˆ -> ์ˆ˜๋ น๊ฒ€์‚ฌ -> ๋ถˆ์ถœยท์‚ฌ์šฉ -> ์‚ฌํ›„ ํ™•์ •\n", " 1. ๊ตฌ๋งค์š”๊ตฌ | ์‚ฌ์šฉ๋ถ€๋Œ€ยท๋ณด๊ธ‰๋ถ€์„œ | ์ƒ์„ฑ ๋ฐ์ดํ„ฐ: ํ’ˆ๋ชฉ(NIIN), ๊ทœ๊ฒฉ ๋“ฑ๊ธ‰ยท๊ฐœ์ •์ฐจ์ˆ˜, ์š”๊ตฌ์ˆ˜๋Ÿ‰, ๊ธด๊ธ‰ ์—ฌ๋ถ€\n", " 2. ๊ณ„์•ฝ | ์กฐ๋‹ฌ ๋‹ด๋‹น | ์ƒ์„ฑ ๋ฐ์ดํ„ฐ: ์—…์ฒด, ๊ณ„์•ฝ ๋‚ฉ๊ธฐ(์ผ), ๊ตฌ๋งค์š”๊ตฌ ๋Œ€๋น„ ๊ทœ๊ฒฉ ๋ณ€๊ฒฝ ์—ฌ๋ถ€\n", " 3. ๋‚ฉํ’ˆ | ์—…์ฒด | ์ƒ์„ฑ ๋ฐ์ดํ„ฐ: ๋กœํŠธ ๋ฒˆํ˜ธ, ๋กœํŠธ ํฌ๊ธฐ, ์‹ค์ œ ๋‚ฉํ’ˆ์ผ(์ง€์—ฐ์ผ)\n", " 4. ์ˆ˜๋ น๊ฒ€์‚ฌ | ํ’ˆ์งˆ ๊ฒ€์‚ฌ๊ด€ | ์ƒ์„ฑ ๋ฐ์ดํ„ฐ: ํ‘œ๋ณธ ์ˆ˜, ๋ถˆ๋Ÿ‰ ์ˆ˜(์œ ํ˜•๋ณ„), ์น˜์ˆ˜ ํŽธ์ฐจ ์ธก์ •๊ฐ’, ํ•ฉ๋ถ€ ํŒ์ •\n", " 5. ๋ถˆ์ถœยท์‚ฌ์šฉ | ๋ณด๊ธ‰๋ถ€์„œยท์‚ฌ์šฉ๋ถ€๋Œ€ | ์ƒ์„ฑ ๋ฐ์ดํ„ฐ: ๊ธด๊ธ‰ ๋ถˆ์ถœ ์—ฌ๋ถ€, ์‚ฌ์šฉ๋ถ€๋Œ€ ํ•˜์žยท๋ถˆ๋งŒ ์ ‘์ˆ˜\n", " 6. ์‚ฌํ›„ ํ™•์ • | ํ’ˆ์งˆ ๋‹ด๋‹น | ์ƒ์„ฑ ๋ฐ์ดํ„ฐ: ์ „์ˆ˜๊ฒ€์‚ฌยทํ•˜์ž ๋ฐ˜์˜ ํ™•์ • ๋ถˆ๋Ÿ‰๋ฅ \n", "\n", "์ •์ƒ ์ƒ˜ํ”Œ 20๊ฑด ์ค‘ ๊ฒ€์ฆ ํ†ต๊ณผ 20๊ฑด, ์˜ค๋ฅ˜ 0๊ฑด\n", "\n", "[์˜ค๋ฅ˜ ์ฃผ์ž… ๊ฒฐ๊ณผ]\n", " - receipt_date: ํ•„์ˆ˜ ํ•„๋“œ ๋ˆ„๋ฝ\n", " - niin: ํ˜•์‹ ๋ถˆ์ผ์น˜ '5305011234567'\n", " - spec_class: ํ—ˆ์šฉ๋˜์ง€ ์•Š์€ ๊ฐ’ 'high'\n", " - vendor: ํ˜•์‹ ๋ถˆ์ผ์น˜ '์—…์ฒดA'\n", " - lot_size: ์ตœ์†Œ๊ฐ’ 1 ๋ฏธ๋งŒ\n", " - (๊ต์ฐจ๊ฒ€์ฆ) ๊ต์ฐจ๊ฒ€์ฆ: ์œ ํ˜•๋ณ„ ๋ถˆ๋Ÿ‰ ํ•ฉ ๋ถˆ์ผ์น˜ / ๊ต์ฐจ๊ฒ€์ฆ: ์žฌ๊ณ ๋ฒˆํ˜ธ์™€ ํ’ˆ๋ชฉ๊ตฐ ๋ถˆ์ผ์น˜\n", "\n", "[๋ฐ์ดํ„ฐ ๋”•์…”๋„ˆ๋ฆฌ ์š”์•ฝ: ๋‹จ๊ณ„๋ณ„ ํ•„๋“œ ์ˆ˜]\n", " ํ•„๋“œ ์ˆ˜ ํ•„๋“œ\n", "๋‹จ๊ณ„ \n", "๊ตฌ๋งค์š”๊ตฌ 7 niin, fsc, spec_class, tolerance_mm, spec_rev, pr_qty, pr_urgent\n", "๊ณ„์•ฝ 4 vendor, lead_days, spec_changed, spec_mismatch\n", "๋‚ฉํ’ˆ 3 lot_id, lot_size, delay_days\n", "์ˆ˜๋ น๊ฒ€์‚ฌ 8 receipt_date, inspector, sample_n, defects, def_spec, def_dim, def_visual, dim_dev_mm\n", "๋ถˆ์ถœยท์‚ฌ์šฉ 2 emergency_release, field_claims\n", "์‚ฌํ›„ ํ™•์ • 1 confirmed_rate\n", "\n", "ํ”ผ์ฒ˜(ํ•„๋“œ) 25๊ฐœ ์ •์˜ (๋ชฉํ‘œ 20๊ฐœ ์ด์ƒ: ๋‹ฌ์„ฑ), ๊ต์ฐจ๊ฒ€์ฆ ๊ทœ์น™ 4์ข…, ํ”„๋กœ์„ธ์Šค ๋งต 6๋‹จ๊ณ„ ์ž‘์„ฑ\n", "๋ถ„์„ ๋ชฉํ‘œ: ๊ตฌ๋งค์š”๊ตฌยท๊ทœ๊ฒฉยท๋‚ฉ๊ธฐ ์ •๋ณด๋กœ ๋กœํŠธ ํ™•์ • ๋ถˆ๋Ÿ‰๋ฅ ์„ ์ˆ˜๋ น๊ฒ€์‚ฌ ์‹œ์ ์— ๋ฏธ๋ฆฌ ์˜ˆ์ธกํ•ด ๊ณ ์œ„ํ—˜ ๋กœํŠธ๋ฅผ ๋จผ์ € ์ •๋ฐ€ ๊ฒ€์‚ฌํ•œ๋‹ค.\n", "```\n", "
\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "---\n", "\n", "## ๐ŸŽฏ ๋ฏธ์…˜ 02. ๊ฒ€์‚ฌ ์ธก์ •์น˜ ๊ฒฐ์ธก์น˜ ์ •์ œ, ๋‚ฉํ’ˆ์—…์ฒด๋ณ„ ๋ถˆ๋Ÿ‰ ์ด๋ ฅ ์ธ์ฝ”๋”ฉ ์ „์ฒ˜๋ฆฌ ๋ชจ๋“ˆ ๊ตฌํ˜„\n", "**์†Œ์š”์‹œ๊ฐ„**: ์•ฝ 2.0์‹œ๊ฐ„\n", "\n", "### ๐Ÿ“‹ ๋ฏธ์…˜ ๋ฐฐ๊ฒฝ ๋ฐ ์ˆ˜ํ–‰ ์ง€์นจ\n", "#### ๋ฏธ์…˜ ๋ฐฐ๊ฒฝ ๋ฐ ๋‹น์œ„์„ฑ\n", "\n", "๊ฒ€์‚ฌ ์ธก์ •์น˜ ๊ฒฐ์ธก์น˜ ์ •์ œ ๋ฐ ์—…์ฒด๋ณ„ ๋ถˆ๋Ÿ‰ ์ด๋ ฅ ์ธ์ฝ”๋”ฉ ์ „์ฒ˜๋ฆฌ ๋ชจ๋“ˆ์„ ๊ตฌํ˜„ํ•œ๋‹ค. ์‹ค์ œ ์ „์ˆ  ํ˜„์žฅ์—์„œ๋Š” ๋‹จ์ˆœ ํƒ์ง€์— ๊ทธ์น˜์ง€ ์•Š๊ณ  ์‹ ์†ํ•œ ์œ„ํ˜‘ ํŒ๋‹จ๊ณผ ์‹ ๋ขฐ๋„ ๋†’์€ ์˜์‚ฌ๊ฒฐ์ •์ด ํ•„์ˆ˜์ ์ด๋ฏ€๋กœ, **๊ฒ€์‚ฌ ์ธก์ •์น˜ ๊ฒฐ์ธก์„ ์ •์ œํ•˜๊ณ  ๋‚ฉํ’ˆ์—…์ฒด๋ณ„ ๋ถˆ๋Ÿ‰ ์ด๋ ฅ์„ ์ •๋ณด ๋ˆ„์ˆ˜ ์—†์ด ์ธ์ฝ”๋”ฉํ•˜๋Š” ์ „์ฒ˜๋ฆฌ ๋ชจ๋“ˆ์„ ๊ตฌํ˜„ํ•œ๋‹ค.** ๊ณผ์—…์„ ์™„์ˆ˜ํ•˜์—ฌ ์˜คํŒ์„ ๋ฐฉ์ง€ํ•˜๊ณ  ์ž‘์ „ ๋Œ€์‘๋ ฅ์„ ๊ทน๋Œ€ํ™”ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.\n", "\n", "์ด ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด ๋ณธ ๋ฏธ์…˜์—์„œ๋Š” **์ค‘๋ณต ๋กœํŠธ, ์—…์ฒด ์ฝ”๋“œ ํ‘œ๊ธฐ, ๋‚ ์งœ ํ˜•์‹, ์žฌ๊ณ ๋ฒˆํ˜ธ ํ˜•์‹, ๋ฒ”์œ„ ์˜ค๋ฅ˜ ๋“ฑ ์ •์ œ ๊ทœ์น™ 10์ข…์„ ์ ์šฉํ•˜๊ณ  ์น˜์ˆ˜ ์ธก์ • ๊ฒฐ์ธก์€ KNN์œผ๋กœ ๋ณด๊ฐ„ ์ˆ˜ํ–‰ํ•˜๊ณ  ๊ฒ€์ฆํ•˜๋Š”** ์‹ค์Šต์„ ์ฒด๊ณ„์ ์œผ๋กœ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค.\n", "\n", "ํ•™์Šต์ž๋Š” ์‹ค์Šต๊ฐ€์ด๋“œ์— ์ œ์‹œ๋œ ์ด 4๊ฐœ ์„ธ๋ถ€ ํ™œ๋™(๊ธฐ๋ณธ ๊ธฐ๋Šฅ ๊ตฌํ˜„ โž” ๊ฒฐ๊ณผ ์ง€ํ‘œ ๊ฒ€์ฆ โž” ํŒŒ๋ผ๋ฏธํ„ฐ ํŠœ๋‹ ๋ฐ ๋„์ „๊ณผ์ œ)์„ ์ˆœ์ฐจ์ ์œผ๋กœ ์ง„ํ–‰ํ•˜๋ฉฐ ํ•ต์‹ฌ ํŒ์ • ๊ธฐ์ค€์„ ๋‹ฌ์„ฑํ•ฉ๋‹ˆ๋‹ค.\n", "\n", "**์ด๋ฒˆ ๊ฒฐ๊ณผ๋ฌผ:** `src/etl/qlt02_data_cleansing.py ๋ฐ ์ •์ œ ํ’ˆ์งˆ ๋ณด๊ณ ์„œ` ํ˜•ํƒœ์˜ ๊ฒฐ๊ณผ๋ฌผ์„ ์™„์„ฑํ•œ๋‹ค.\n", "\n", "### ๐Ÿ” ํ‰๊ฐ€ ๋ฐ ๊ฒ€์ฆ ๊ธฐ์ค€ (PASS ์กฐ๊ฑด)\n", "#### ๋ฏธ์…˜ ํŒ์ •\n", "\n", "**ํ‰๊ฐ€ ๊ตฌ๋ถ„:** ํ•„์ˆ˜\n", "\n", "**ํ•ต์‹ฌ ํ‰๊ฐ€์˜์—ญ:** ๊ตฐ์ˆ˜ ํŠน์ˆ˜ ์˜ˆ์™ธ(๋น„๊ณ„ํš ์ •๋น„, ๊ธด๊ธ‰ ๋ถˆ์ถœ ๋“ฑ) ๋ฐ์ดํ„ฐ ๋ณด์กด์˜ ํƒ€๋‹น์„ฑ\n", "\n", "#### ๋ฏธ์…˜ PASS ์กฐ๊ฑด\n", "\n", "- ์ •๋Ÿ‰ ๊ธฐ์ค€: ๋ฐ์ดํ„ฐ ์ •์ œ ๊ทœ์น™ 10์ข… ์ ์šฉ, ๋ฐ์ดํ„ฐ ์ •ํ•ฉ์„ฑ ๊ฒ€์ฆ ํ†ต๊ณผ์œจ 100%\n", "\n", "\n", "- ์ง„ํ–‰ ๊ธฐ์ค€: ETL ํŒŒ์ดํ”„๋ผ์ธ ๊ตฌํ˜„ ๋ฐ ์ •์ œ๋œ ๋งˆ์Šคํ„ฐ ๋ฐ์ดํ„ฐ์…‹ ๊ตฌ์ถ•\n", "\n", "\n", "- ๊ฒ€์ฆ ๋ฐฉ๋ฒ•: ์ „ํ›„ ๋ฐ์ดํ„ฐ ํ†ต๊ณ„๋Ÿ‰(๊ธฐ์ˆ ํ†ต๊ณ„) ๋น„๊ต ๋ฐ ๋ฌด๊ฒฐ์„ฑ ๊ฒ€์ฆ\n", "\n", "\n", "#### ์ฑ„์ ์ž ํŒ์ • ๋ฉ”๋ชจ\n", "\n", "> ๋‹จ์ˆœ ์ฝ”๋“œ ์‹คํ–‰ ์—ฌ๋ถ€๋ฟ๋งŒ ์•„๋‹ˆ๋ผ ์ถœ๋ ฅ๋œ ์ˆ˜์น˜์™€ ๊ทผ๊ฑฐ ์ž๋ฃŒ๊ฐ€ ๊ตฐ ์‹ค๋ฌด ์š”๊ตฌ์‚ฌํ•ญ์— ๋ถ€ํ•ฉํ•˜๋Š”์ง€ ๋ฉด๋ฐ€ํžˆ ํ™•์ธํ•œ๋‹ค.\n", "\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "\"\"\"์ˆ˜๋ น๊ฒ€์‚ฌ ์›์‹œ ๊ธฐ๋ก์˜ ๊ฒฐ์ธกยท์˜ค๊ธฐยท์ค‘๋ณต์„ ์ •์ œ ๊ทœ์น™ 10์ข…์œผ๋กœ ๋ฐ”๋กœ์žก๊ณ (์ธก์ •์น˜ ๊ฒฐ์ธก์€ KNN ๋ณด๊ฐ„), ๋‚ฉํ’ˆ์—…์ฒด๋ณ„ ๊ณผ๊ฑฐ ๋ถˆ๋Ÿ‰ ์ด๋ ฅ์„ ์ •๋ณด ๋ˆ„์ˆ˜ ์—†์ด ์ธ์ฝ”๋”ฉํ•˜๋Š” ์ „์ฒ˜๋ฆฌ ๋ชจ๋“ˆ์„ ๊ตฌํ˜„ํ•œ๋‹ค. ๊ธด๊ธ‰ ๋ถˆ์ถœ ๋“ฑ ๊ตฐ์ˆ˜ ํŠน์ˆ˜ ์˜ˆ์™ธ ๊ธฐ๋ก์€ ์‚ญ์ œํ•˜์ง€ ์•Š๊ณ  ๋ณด์กดํ•œ๋‹ค.\"\"\"\n", "import numpy as np\n", "import pandas as pd\n", "from sklearn.impute import KNNImputer\n", "\n", "# ---------------------------------------------------------------------------\n", "# [๊ต์œก์šฉ๊ฐ€์ƒ์ž๋ฃŒ] ๊ตฐ์ˆ˜ํ’ˆ ์ˆ˜๋ น๊ฒ€์‚ฌ ๋กœํŠธ ๋ฐ์ดํ„ฐ ์ƒ์„ฑ๊ธฐ (์‹ค์ œ ์—…์ฒดยทํ’ˆ๋ชฉยท๊ณ„์•ฝ ์ •๋ณด ์—†์Œ)\n", "# ๊ตฌ๋งค์š”๊ตฌ(๊ทœ๊ฒฉยท๊ธด๊ธ‰ ์—ฌ๋ถ€) -> ๊ณ„์•ฝ(๋‚ฉ๊ธฐ) -> ๋‚ฉํ’ˆ -> ์ˆ˜๋ น๊ฒ€์‚ฌ(ํ‘œ๋ณธ) -> ํ™•์ • ๋ถˆ๋Ÿ‰๋ฅ (์ „์ˆ˜๊ฒ€์‚ฌยท์‚ฌ์šฉ๋ถ€๋Œ€ ํ•˜์ž ๋ฐ˜์˜)\n", "# ---------------------------------------------------------------------------\n", "FSC = {\"5305\": \"screw/bolt\", \"5330\": \"gasket\", \"5935\": \"connector\", \"6135\": \"battery\", \"2530\": \"brake part\", \"8415\": \"clothing\"}\n", "VENDORS = [f\"V{i:02d}\" for i in range(1, 13)]\n", "VENDOR_Q = dict(zip(VENDORS, [-0.6, -0.4, -0.3, -0.2, -0.1, 0.0, 0.1, 0.2, 0.3, 0.5, 0.8, 1.1])) # ์—…์ฒด ๊ณ ์œ  ํ’ˆ์งˆ ์ˆ˜์ค€(์ˆจ์€ ๊ฐ’)\n", "FSC_Q = {\"5305\": -0.3, \"5330\": 0.0, \"5935\": 0.25, \"6135\": 0.1, \"2530\": 0.35, \"8415\": -0.2}\n", "SAMPLE_PLAN = [(500, 50, 3), (1200, 80, 5), (3200, 125, 7), (10 ** 9, 200, 10)] # (๋กœํŠธ ํฌ๊ธฐ ์ƒํ•œ, ํ‘œ๋ณธ n, ํ•ฉ๊ฒฉํŒ์ •์ˆ˜ c) ๋‹จ์ˆœํ™” ๊ธฐ์ค€\n", "BAD_RATE = 0.05 # ํ™•์ • ๋ถˆ๋Ÿ‰๋ฅ  5% ์ดˆ๊ณผ ๋กœํŠธ = ๋ถˆํ•ฉ๊ฒฉ ๋Œ€์ƒ\n", "\n", "\n", "def sample_plan(lot_size):\n", " for up, n, c in SAMPLE_PLAN:\n", " if lot_size <= up:\n", " return n, c\n", "\n", "\n", "def true_rate(d, rng=None):\n", " \"\"\"์ˆจ์€ ๋ถˆ๋Ÿ‰ ๋ฐœ์ƒ ๊ตฌ์กฐ: ์—…์ฒด ์ˆ˜์ค€ + ํ’ˆ๋ชฉ๊ตฐ + ์ •๋ฐ€ ๊ทœ๊ฒฉ + ๊ธด๊ธ‰ ๊ตฌ๋งค + ๊ทœ๊ฒฉ ๋ณ€๊ฒฝยท๋ถˆ์ผ์น˜ + ๋‚ฉ๊ธฐ ์ง€์—ฐ + (์ •๋ฐ€ ๊ทœ๊ฒฉ x ์ด‰๋ฐ• ๋‚ฉ๊ธฐ) ์ƒํ˜ธ์ž‘์šฉ.\"\"\"\n", " short = (d[\"lead_days\"] < 45).astype(float)\n", " z = (-4.35 + d[\"vendor\"].map(VENDOR_Q) + d[\"fsc\"].map(FSC_Q) + 0.55 * (d[\"spec_class\"] == \"precision\")\n", " + 0.35 * d[\"pr_urgent\"] + 0.6 * d[\"spec_changed\"] + 0.8 * d[\"spec_mismatch\"]\n", " + 0.9 * np.log1p(np.maximum(d[\"delay_days\"], 0) / 7) + 0.9 * short * (d[\"spec_class\"] == \"precision\")\n", " + 0.5 * (d[\"month\"] >= 18) * (d[\"vendor\"] == \"V10\")) # V10 ํ›„๋ฐ˜๊ธฐ ํ’ˆ์งˆ ์ €ํ•˜(๋“œ๋ฆฌํ”„ํŠธ)\n", " if rng is not None:\n", " z = z + rng.normal(0, 0.12, len(d))\n", " return np.clip(np.exp(z), 0.002, 0.35)\n", "\n", "\n", "def make_lots(n=1200, seed=42, months=24):\n", " rng = np.random.default_rng(seed)\n", " d = pd.DataFrame({\"lot_id\": [f\"LOT{seed % 100:02d}-{i:05d}\" for i in range(n)]})\n", " d[\"month\"] = rng.integers(0, months, n)\n", " d[\"receipt_date\"] = pd.Timestamp(\"2024-01-01\") + pd.to_timedelta(d[\"month\"] * 30 + rng.integers(0, 30, n), unit=\"D\")\n", " d = d.sort_values(\"receipt_date\").reset_index(drop=True)\n", " d[\"month\"] = ((d[\"receipt_date\"] - pd.Timestamp(\"2024-01-01\")).dt.days // 30).clip(0, months - 1)\n", " d[\"vendor\"] = rng.choice(VENDORS, n, p=np.r_[np.full(6, 0.1), np.full(6, 0.4 / 6)])\n", " d[\"fsc\"] = rng.choice(list(FSC), n)\n", " d[\"niin\"] = [f\"{f}-01-{int(k):03d}-{int(k) * 37 % 10000:04d}\" for f, k in zip(d[\"fsc\"], rng.integers(100, 110, n))]\n", " d[\"spec_class\"] = np.where(rng.random(n) < np.where(d[\"fsc\"].isin([\"5935\", \"2530\"]), 0.6, 0.25), \"precision\", \"general\")\n", " d[\"tolerance_mm\"] = np.where(d[\"spec_class\"] == \"precision\", rng.uniform(0.05, 0.1, n), rng.uniform(0.2, 0.5, n)).round(3)\n", " d[\"spec_rev\"] = rng.integers(1, 6, n)\n", " d[\"pr_urgent\"] = (rng.random(n) < 0.2).astype(int)\n", " d[\"spec_changed\"] = (rng.random(n) < 0.12).astype(int)\n", " d[\"spec_mismatch\"] = (rng.random(n) < 0.08).astype(int)\n", " d[\"pr_qty\"] = rng.integers(200, 5000, n)\n", " d[\"lot_size\"] = (d[\"pr_qty\"] * rng.uniform(0.5, 1.0, n)).astype(int)\n", " d[\"lead_days\"] = np.where(d[\"pr_urgent\"] == 1, rng.integers(20, 60, n), rng.integers(40, 150, n))\n", " d[\"delay_days\"] = np.round(rng.gamma(1.2, 6, n) * rng.binomial(1, 0.45, n) - rng.integers(0, 5, n)).astype(int)\n", " d[\"unit_cost\"] = rng.choice([2_000, 15_000, 40_000, 120_000], n, p=[0.35, 0.3, 0.25, 0.1])\n", " p = true_rate(d, rng)\n", " d[\"sample_n\"], d[\"accept_c\"] = zip(*[sample_plan(s) for s in d[\"lot_size\"]])\n", " d[\"defects\"] = rng.binomial(d[\"sample_n\"], p)\n", " share = np.where(d[\"spec_class\"] == \"precision\", 0.55, 0.3)\n", " d[\"def_dim\"] = rng.binomial(d[\"defects\"], share)\n", " d[\"def_spec\"] = rng.binomial(d[\"defects\"] - d[\"def_dim\"], 0.6)\n", " d[\"def_visual\"] = d[\"defects\"] - d[\"def_dim\"] - d[\"def_spec\"]\n", " d[\"dim_dev_mm\"] = (d[\"tolerance_mm\"] * (0.3 + 2.0 * p) * rng.lognormal(0, 0.3, n)).round(4)\n", " d[\"confirmed_rate\"] = np.clip(p * rng.lognormal(0, 0.05, n), 0.001, 0.4).round(4) # ์ „์ˆ˜๊ฒ€์‚ฌยทํ•˜์ž ๋ฐ˜์˜ ํ™•์ • ๋ถˆ๋Ÿ‰๋ฅ \n", " d[\"field_claims\"] = rng.poisson(d[\"lot_size\"] * p * 0.03)\n", " d[\"inspector\"] = rng.choice([\"INS-1\", \"INS-2\", \"INS-3\", \"INS-4\"], n)\n", " d[\"emergency_release\"] = ((d[\"pr_urgent\"] == 1) & (rng.random(n) < 0.3)).astype(int) # ๊ธด๊ธ‰ ๋ถˆ์ถœ(๊ฒ€์‚ฌ ์ „ ์„ ๋ถˆ์ถœ) ์˜ˆ์™ธ\n", " d[\"bad_lot\"] = (d[\"confirmed_rate\"] > BAD_RATE).astype(int)\n", " return d\n", "# ---------------------------------------------------------------------------\n", "\n", "\n", "def make_raw(clean, seed=5):\n", " \"\"\"[๊ต์œก์šฉ๊ฐ€์ƒ์ž๋ฃŒ] ํ˜„์žฅ ์ž…๋ ฅ ์˜ค๋ฅ˜๋ฅผ ํ‰๋‚ด ๋‚ธ ์›์‹œ ๋ฐ์ดํ„ฐ: ๊ฒฐ์ธกยทํ˜•์‹ ์˜ค๋ฅ˜ยท๋‹จ์œ„ ํ˜ผ์šฉยท์ค‘๋ณต ๋“ฑ์„ ์ผ๋ถ€๋Ÿฌ ๋„ฃ๋Š”๋‹ค.\"\"\"\n", " rng = np.random.default_rng(seed)\n", " r = clean.copy()\n", " r[\"receipt_date\"] = r[\"receipt_date\"].dt.strftime(\"%Y-%m-%d\").astype(object)\n", " idx = lambda k: rng.choice(len(r), k, replace=False)\n", " r.loc[idx(40), \"dim_dev_mm\"] = np.nan # ์ธก์ •์น˜ ๊ฒฐ์ธก\n", " i = idx(15); r.loc[i, \"dim_dev_mm\"] = r.loc[i, \"dim_dev_mm\"] * 1000 # mm/um ๋‹จ์œ„ ํ˜ผ์šฉ\n", " i = idx(20); r.loc[i, \"vendor\"] = [\" \" + v.lower() + \" \" for v in r.loc[i, \"vendor\"]] # ๋Œ€์†Œ๋ฌธ์žยท๊ณต๋ฐฑ\n", " i = idx(15); r.loc[i, \"receipt_date\"] = [d.replace(\"-\", \".\") for d in r.loc[i, \"receipt_date\"]] # ๋‚ ์งœ ํ˜•์‹\n", " i = idx(10); r.loc[i, \"niin\"] = [n.replace(\"-\", \"\") for n in r.loc[i, \"niin\"]] # ์žฌ๊ณ ๋ฒˆํ˜ธ ํ•˜์ดํ”ˆ ๋ˆ„๋ฝ\n", " i = idx(8); r.loc[i, \"lot_size\"] = -r.loc[i, \"lot_size\"] # ๋ถ€ํ˜ธ ์˜ค๋ฅ˜\n", " i = idx(6); r.loc[i, \"defects\"] = r.loc[i, \"defects\"] + 3 # ์œ ํ˜•๋ณ„ ํ•ฉ ๋ถˆ์ผ์น˜\n", " i = idx(5); r.loc[i, \"spec_class\"] = \"PRECISION\" # ์ฝ”๋“œ ํ‘œ๊ธฐ ๋ถˆ์ผ์น˜\n", " i = idx(5); r.loc[i, \"inspector\"] = np.nan # ๊ฒ€์‚ฌ๊ด€ ๋ˆ„๋ฝ\n", " r = pd.concat([r, r.iloc[idx(12)]], ignore_index=True) # ์ค‘๋ณต ์ž…๋ ฅ\n", " return r\n", "\n", "\n", "def clean(raw):\n", " d, log = raw.copy(), []\n", "\n", " def rule(name, mask, fix):\n", " n = int(mask.sum())\n", " if n:\n", " fix(mask)\n", " log.append((name, n))\n", "\n", " rule(\"R01 ์ค‘๋ณต ๋กœํŠธ ์ œ๊ฑฐ\", d.duplicated(\"lot_id\"), lambda m: None)\n", " d = d[~d.duplicated(\"lot_id\")].reset_index(drop=True)\n", " rule(\"R02 ์—…์ฒด ์ฝ”๋“œ ๊ณต๋ฐฑยท๋Œ€์†Œ๋ฌธ์ž ์ •๊ทœํ™”\", d[\"vendor\"] != d[\"vendor\"].str.strip().str.upper(),\n", " lambda m: d.__setitem__(\"vendor\", d[\"vendor\"].str.strip().str.upper()))\n", " rule(\"R03 ๋‚ ์งœ ํ˜•์‹ ํ†ต์ผ(YYYY-MM-DD)\", d[\"receipt_date\"].str.contains(r\"\\.\"),\n", " lambda m: d.__setitem__(\"receipt_date\", d[\"receipt_date\"].str.replace(\".\", \"-\", regex=False)))\n", " d[\"receipt_date\"] = pd.to_datetime(d[\"receipt_date\"])\n", " bad_niin = ~d[\"niin\"].str.match(r\"^\\d{4}-\\d{2}-\\d{3}-\\d{4}$\")\n", " fixed_niin = d[\"niin\"].str.replace(r\"^(\\d{4})(\\d{2})(\\d{3})(\\d{4})$\", r\"\\1-\\2-\\3-\\4\", regex=True)\n", " rule(\"R04 ์žฌ๊ณ ๋ฒˆํ˜ธ(NIIN) ํ˜•์‹ ๋ณต์›\", bad_niin, lambda m: d.__setitem__(\"niin\", fixed_niin))\n", " rule(\"R05 ๊ทœ๊ฒฉ ๋“ฑ๊ธ‰ ์ฝ”๋“œ ์†Œ๋ฌธ์ž ํ†ต์ผ\", d[\"spec_class\"] != d[\"spec_class\"].str.lower(),\n", " lambda m: d.__setitem__(\"spec_class\", d[\"spec_class\"].str.lower()))\n", " rule(\"R06 ๋กœํŠธ ํฌ๊ธฐ ๋ถ€ํ˜ธ ์˜ค๋ฅ˜ ์ˆ˜์ •\", d[\"lot_size\"] < 0, lambda m: d.__setitem__(\"lot_size\", d[\"lot_size\"].abs()))\n", " rule(\"R07 ์ธก์ • ๋‹จ์œ„ ํ™˜์‚ฐ(um -> mm)\", d[\"dim_dev_mm\"] > 5, lambda m: d.__setitem__(\"dim_dev_mm\", d[\"dim_dev_mm\"].where(~m, d[\"dim_dev_mm\"] / 1000)))\n", " rule(\"R08 ์œ ํ˜•๋ณ„ ํ•ฉ์— ๋งž์ถฐ ๋ถˆ๋Ÿ‰ ์ˆ˜ ์žฌ๊ณ„์‚ฐ\", d[\"defects\"] != d[[\"def_spec\", \"def_dim\", \"def_visual\"]].sum(axis=1),\n", " lambda m: d.__setitem__(\"defects\", d[[\"def_spec\", \"def_dim\", \"def_visual\"]].sum(axis=1)))\n", " rule(\"R09 ๊ฒ€์‚ฌ๊ด€ ๋ˆ„๋ฝ -> 'UNKNOWN' ํ‘œ์‹œ(์‚ญ์ œํ•˜์ง€ ์•Š์Œ)\", d[\"inspector\"].isna(), lambda m: d.__setitem__(\"inspector\", d[\"inspector\"].fillna(\"UNKNOWN\")))\n", " miss = d[\"dim_dev_mm\"].isna()\n", " num = [\"dim_dev_mm\", \"tolerance_mm\", \"defects\", \"sample_n\", \"lot_size\"]\n", " Z = d[num].astype(float); mu, sd = Z.mean(), Z.std()\n", " filled = KNNImputer(n_neighbors=5).fit_transform((Z - mu) / sd) * sd.values + mu.values\n", " rule(\"R10 ์น˜์ˆ˜ ํŽธ์ฐจ ๊ฒฐ์ธก KNN ๋ณด๊ฐ„(k=5)\", miss, lambda m: d.__setitem__(\"dim_dev_mm\", np.where(m, filled[:, 0].round(4), d[\"dim_dev_mm\"])))\n", " d[\"dim_imputed\"] = miss.astype(int) # ๋ณด๊ฐ„ ์—ฌ๋ถ€ ํ‘œ์‹œ(์ถ”์ ์„ฑ)\n", " return d, log\n", "\n", "\n", "def integrity(d):\n", " \"\"\"์ •ํ•ฉ์„ฑ ๊ฒ€์ฆ ํ•ญ๋ชฉ: ๋ชจ๋‘ True์—ฌ์•ผ ํ†ต๊ณผ.\"\"\"\n", " return {\"๋กœํŠธ ๋ฒˆํ˜ธ ์œ ์ผ\": d[\"lot_id\"].is_unique,\n", " \"์—…์ฒด ์ฝ”๋“œ ํ˜•์‹\": d[\"vendor\"].str.match(r\"^V\\d{2}$\").all(),\n", " \"์žฌ๊ณ ๋ฒˆํ˜ธ ํ˜•์‹\": d[\"niin\"].str.match(r\"^\\d{4}-\\d{2}-\\d{3}-\\d{4}$\").all(),\n", " \"์žฌ๊ณ ๋ฒˆํ˜ธ-ํ’ˆ๋ชฉ๊ตฐ ์ผ์น˜\": (d[\"niin\"].str[:4] == d[\"fsc\"]).all(),\n", " \"๋กœํŠธ ํฌ๊ธฐ ์–‘์ˆ˜\": (d[\"lot_size\"] > 0).all(),\n", " \"ํ‘œ๋ณธ <= ๋กœํŠธ\": (d[\"sample_n\"] <= d[\"lot_size\"]).all(),\n", " \"๋ถˆ๋Ÿ‰ ์œ ํ˜• ํ•ฉ ์ผ์น˜\": (d[\"defects\"] == d[[\"def_spec\", \"def_dim\", \"def_visual\"]].sum(axis=1)).all(),\n", " \"์น˜์ˆ˜ ํŽธ์ฐจ ๊ฒฐ์ธก ์—†์Œยท๋ฒ”์œ„\": d[\"dim_dev_mm\"].between(0, 5).all(),\n", " \"๋‚ ์งœํ˜• ๋ณ€ํ™˜\": pd.api.types.is_datetime64_any_dtype(d[\"receipt_date\"]),\n", " \"๊ทœ๊ฒฉ ๋“ฑ๊ธ‰ ํ—ˆ์šฉ๊ฐ’\": d[\"spec_class\"].isin([\"precision\", \"general\"]).all()}\n", "\n", "\n", "def encode_vendor_history(d):\n", " \"\"\"์—…์ฒด๋ณ„ ๋ถˆ๋Ÿ‰ ์ด๋ ฅ ์ธ์ฝ”๋”ฉ: ๊ฐ™์€ ์—…์ฒด์˜ '์ด์ „' ๋กœํŠธ ํ™•์ • ๋ถˆ๋Ÿ‰๋ฅ ๋งŒ ์จ์„œ ํ‰๊ท ยท๊ฑด์ˆ˜ยท์ตœ๊ทผ 5๋กœํŠธ ํ‰๊ท ์„ ๋งŒ๋“ ๋‹ค.\"\"\"\n", " d = d.sort_values(\"receipt_date\").reset_index(drop=True)\n", " g = d.groupby(\"vendor\")[\"confirmed_rate\"]\n", " d[\"vendor_prev_n\"] = d.groupby(\"vendor\").cumcount()\n", " d[\"vendor_hist_rate\"] = g.transform(lambda s: s.shift().expanding().mean())\n", " d[\"vendor_recent_rate\"] = g.transform(lambda s: s.shift().rolling(5, min_periods=1).mean())\n", " prior = d[\"confirmed_rate\"].mean() # ์ด๋ ฅ์ด ์ ์€ ์—…์ฒด๋Š” ์ „์ฒด ํ‰๊ท  ์ชฝ์œผ๋กœ ์ˆ˜์ถ•\n", " w = d[\"vendor_prev_n\"] / (d[\"vendor_prev_n\"] + 10)\n", " d[\"vendor_hist_enc\"] = (w * d[\"vendor_hist_rate\"].fillna(prior) + (1 - w) * prior).round(4)\n", " return d\n", "\n", "\n", "truth = make_lots(1200, seed=42)\n", "raw = make_raw(truth)\n", "print(f\"์›์‹œ ๊ธฐ๋ก {len(raw)}๊ฑด (์ค‘๋ณต ํฌํ•จ), ๊ฒฐ์ธก ์น˜์ˆ˜๊ฐ’ {raw['dim_dev_mm'].isna().sum()}๊ฑด\")\n", "pre = integrity(raw)\n", "print(f\"์ •์ œ ์ „ ์ •ํ•ฉ์„ฑ ๊ฒ€์ฆ {sum(pre.values())}/{len(pre)} ํ†ต๊ณผ, ์‹คํŒจ ํ•ญ๋ชฉ: {', '.join(k for k, v in pre.items() if not v)}\")\n", "cleaned, log = clean(raw)\n", "print(\"\\n[์ •์ œ ๊ทœ์น™ ์ ์šฉ ๋‚ด์—ญ]\")\n", "for name, n in log:\n", " print(f\" {name:34s} {n:4d}๊ฑด\")\n", "chk = integrity(cleaned)\n", "rate = np.mean(list(chk.values()))\n", "print(f\"\\n์ •์ œ ๊ทœ์น™ {len(log)}์ข… ์ ์šฉ, ์ •ํ•ฉ์„ฑ ๊ฒ€์ฆ {sum(chk.values())}/{len(chk)} ํ†ต๊ณผ ({rate:.0%}) (๋ชฉํ‘œ 100%: {'๋‹ฌ์„ฑ' if rate == 1 else '๋ฏธ๋‹ฌ'})\")\n", "print(f\"๊ธด๊ธ‰ ๋ถˆ์ถœ ์˜ˆ์™ธ ๊ธฐ๋ก ๋ณด์กด: ์›๋ณธ {truth['emergency_release'].sum()}๊ฑด -> ์ •์ œ ํ›„ {cleaned['emergency_release'].sum()}๊ฑด (์‚ญ์ œ ์—†์Œ)\")\n", "\n", "m = cleaned[\"dim_imputed\"] == 1\n", "ref = truth.set_index(\"lot_id\").loc[cleaned.loc[m, \"lot_id\"], \"dim_dev_mm\"].values\n", "print(f\"KNN ๋ณด๊ฐ„ {m.sum()}๊ฑด ํ‰๊ท  ์ ˆ๋Œ€์˜ค์ฐจ {np.mean(np.abs(cleaned.loc[m, 'dim_dev_mm'].values - ref)):.4f} mm (๋‹จ์ˆœ ํ‰๊ท  ๋Œ€์น˜ {np.mean(np.abs(truth['dim_dev_mm'].mean() - ref)):.4f} mm)\")\n", "\n", "print(\"\\n[์ „ํ›„ ๊ธฐ์ˆ ํ†ต๊ณ„ ๋น„๊ต]\")\n", "cmp = pd.DataFrame({\"์ •์ œ ์ „ ํ‰๊ท \": raw[[\"lot_size\", \"dim_dev_mm\", \"defects\"]].mean(), \"์ •์ œ ํ›„ ํ‰๊ท \": cleaned[[\"lot_size\", \"dim_dev_mm\", \"defects\"]].mean(),\n", " \"์›๋ณธ(์ •๋‹ต) ํ‰๊ท \": truth[[\"lot_size\", \"dim_dev_mm\", \"defects\"]].mean()}).round(3)\n", "print(cmp.to_string())\n", "\n", "enc = encode_vendor_history(cleaned)\n", "print(\"\\n[์—…์ฒด๋ณ„ ๋ถˆ๋Ÿ‰ ์ด๋ ฅ ์ธ์ฝ”๋”ฉ ์˜ˆ์‹œ: ๋งˆ์ง€๋ง‰ ๋กœํŠธ ๊ธฐ์ค€]\")\n", "print(enc.groupby(\"vendor\").tail(1)[[\"vendor\", \"vendor_prev_n\", \"vendor_hist_enc\", \"vendor_recent_rate\"]].sort_values(\"vendor_hist_enc\", ascending=False).head(6).round(4).to_string(index=False))\n", "print(\"์ฃผ์˜: ํ˜„์žฌ ๋กœํŠธ์˜ ํ™•์ • ๋ถˆ๋Ÿ‰๋ฅ ์„ ์ธ์ฝ”๋”ฉ์— ๋„ฃ์œผ๋ฉด ์ •๋ณด ๋ˆ„์ˆ˜๊ฐ€ ์ƒ๊ธฐ๋ฏ€๋กœ ์ด์ „ ๋กœํŠธ๋งŒ ์‚ฌ์šฉํ–ˆ๋‹ค.\")\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "
\n", "๐Ÿ’ก [์ฐธ๊ณ ] ๋ฏธ์…˜ 02 ์˜ˆ์ƒ ์‹คํ–‰ ๊ฒฐ๊ณผ (Output)\n", "\n", "```text\n", "์›์‹œ ๊ธฐ๋ก 1212๊ฑด (์ค‘๋ณต ํฌํ•จ), ๊ฒฐ์ธก ์น˜์ˆ˜๊ฐ’ 40๊ฑด\n", "์ •์ œ ์ „ ์ •ํ•ฉ์„ฑ ๊ฒ€์ฆ 1/10 ํ†ต๊ณผ, ์‹คํŒจ ํ•ญ๋ชฉ: ๋กœํŠธ ๋ฒˆํ˜ธ ์œ ์ผ, ์—…์ฒด ์ฝ”๋“œ ํ˜•์‹, ์žฌ๊ณ ๋ฒˆํ˜ธ ํ˜•์‹, ๋กœํŠธ ํฌ๊ธฐ ์–‘์ˆ˜, ํ‘œ๋ณธ <= ๋กœํŠธ, ๋ถˆ๋Ÿ‰ ์œ ํ˜• ํ•ฉ ์ผ์น˜, ์น˜์ˆ˜ ํŽธ์ฐจ ๊ฒฐ์ธก ์—†์Œยท๋ฒ”์œ„, ๋‚ ์งœํ˜• ๋ณ€ํ™˜, ๊ทœ๊ฒฉ ๋“ฑ๊ธ‰ ํ—ˆ์šฉ๊ฐ’\n", "\n", "[์ •์ œ ๊ทœ์น™ ์ ์šฉ ๋‚ด์—ญ]\n", " R01 ์ค‘๋ณต ๋กœํŠธ ์ œ๊ฑฐ 12๊ฑด\n", " R02 ์—…์ฒด ์ฝ”๋“œ ๊ณต๋ฐฑยท๋Œ€์†Œ๋ฌธ์ž ์ •๊ทœํ™” 20๊ฑด\n", " R03 ๋‚ ์งœ ํ˜•์‹ ํ†ต์ผ(YYYY-MM-DD) 15๊ฑด\n", " R04 ์žฌ๊ณ ๋ฒˆํ˜ธ(NIIN) ํ˜•์‹ ๋ณต์› 10๊ฑด\n", " R05 ๊ทœ๊ฒฉ ๋“ฑ๊ธ‰ ์ฝ”๋“œ ์†Œ๋ฌธ์ž ํ†ต์ผ 5๊ฑด\n", " R06 ๋กœํŠธ ํฌ๊ธฐ ๋ถ€ํ˜ธ ์˜ค๋ฅ˜ ์ˆ˜์ • 8๊ฑด\n", " R07 ์ธก์ • ๋‹จ์œ„ ํ™˜์‚ฐ(um -> mm) 14๊ฑด\n", " R08 ์œ ํ˜•๋ณ„ ํ•ฉ์— ๋งž์ถฐ ๋ถˆ๋Ÿ‰ ์ˆ˜ ์žฌ๊ณ„์‚ฐ 6๊ฑด\n", " R09 ๊ฒ€์‚ฌ๊ด€ ๋ˆ„๋ฝ -> 'UNKNOWN' ํ‘œ์‹œ(์‚ญ์ œํ•˜์ง€ ์•Š์Œ) 5๊ฑด\n", " R10 ์น˜์ˆ˜ ํŽธ์ฐจ ๊ฒฐ์ธก KNN ๋ณด๊ฐ„(k=5) 40๊ฑด\n", "\n", "์ •์ œ ๊ทœ์น™ 10์ข… ์ ์šฉ, ์ •ํ•ฉ์„ฑ ๊ฒ€์ฆ 10/10 ํ†ต๊ณผ (100%) (๋ชฉํ‘œ 100%: ๋‹ฌ์„ฑ)\n", "๊ธด๊ธ‰ ๋ถˆ์ถœ ์˜ˆ์™ธ ๊ธฐ๋ก ๋ณด์กด: ์›๋ณธ 76๊ฑด -> ์ •์ œ ํ›„ 76๊ฑด (์‚ญ์ œ ์—†์Œ)\n", "KNN ๋ณด๊ฐ„ 40๊ฑด ํ‰๊ท  ์ ˆ๋Œ€์˜ค์ฐจ 0.0233 mm (๋‹จ์ˆœ ํ‰๊ท  ๋Œ€์น˜ 0.0543 mm)\n", "\n", "[์ „ํ›„ ๊ธฐ์ˆ ํ†ต๊ณ„ ๋น„๊ต]\n", " ์ •์ œ ์ „ ํ‰๊ท  ์ •์ œ ํ›„ ํ‰๊ท  ์›๋ณธ(์ •๋‹ต) ํ‰๊ท \n", "lot_size 1970.468 1983.859 1983.859\n", "dim_dev_mm 0.973 0.094 0.094\n", "defects 5.051 5.061 5.061\n", "\n", "[์—…์ฒด๋ณ„ ๋ถˆ๋Ÿ‰ ์ด๋ ฅ ์ธ์ฝ”๋”ฉ ์˜ˆ์‹œ: ๋งˆ์ง€๋ง‰ ๋กœํŠธ ๊ธฐ์ค€]\n", "vendor vendor_prev_n vendor_hist_enc vendor_recent_rate\n", " V12 86 0.1072 0.1152\n", " V11 80 0.0699 0.0366\n", " V10 79 0.0692 0.1406\n", " V08 57 0.0487 0.0278\n", " V09 81 0.0450 0.0539\n", " V07 69 0.0402 0.0307\n", "์ฃผ์˜: ํ˜„์žฌ ๋กœํŠธ์˜ ํ™•์ • ๋ถˆ๋Ÿ‰๋ฅ ์„ ์ธ์ฝ”๋”ฉ์— ๋„ฃ์œผ๋ฉด ์ •๋ณด ๋ˆ„์ˆ˜๊ฐ€ ์ƒ๊ธฐ๋ฏ€๋กœ ์ด์ „ ๋กœํŠธ๋งŒ ์‚ฌ์šฉํ–ˆ๋‹ค.\n", "```\n", "
\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "---\n", "\n", "## ๐ŸŽฏ ๋ฏธ์…˜ 03. ๋ถˆ๋Ÿ‰๋ฅ  ํ†ต๊ณ„ ๊ธฐ๋ฐ˜ 1์ฐจ ๋กœํŠธ(Lot) ํ•ฉ๊ฒฉ/๋ถˆํ•ฉ๊ฒฉ ํŒ์ • Baseline ์•Œ๊ณ ๋ฆฌ์ฆ˜ ๊ตฌํ˜„\n", "**์†Œ์š”์‹œ๊ฐ„**: ์•ฝ 2.5์‹œ๊ฐ„\n", "\n", "### ๐Ÿ“‹ ๋ฏธ์…˜ ๋ฐฐ๊ฒฝ ๋ฐ ์ˆ˜ํ–‰ ์ง€์นจ\n", "#### ๋ฏธ์…˜ ๋ฐฐ๊ฒฝ ๋ฐ ๋‹น์œ„์„ฑ\n", "\n", "๋ถˆ๋Ÿ‰๋ฅ  ํ†ต๊ณ„ ๊ธฐ๋ฐ˜ ๋กœํŠธ(Lot) ํ•ฉ๊ฒฉ/๋ถˆํ•ฉ๊ฒฉ ํŒ์ • Baseline ์•Œ๊ณ ๋ฆฌ์ฆ˜์„ ๊ตฌํ˜„ํ•œ๋‹ค. ์‹ค์ œ ์ „์ˆ  ํ˜„์žฅ์—์„œ๋Š” ๋‹จ์ˆœ ํƒ์ง€์— ๊ทธ์น˜์ง€ ์•Š๊ณ  ์‹ ์†ํ•œ ์œ„ํ˜‘ ํŒ๋‹จ๊ณผ ์‹ ๋ขฐ๋„ ๋†’์€ ์˜์‚ฌ๊ฒฐ์ •์ด ํ•„์ˆ˜์ ์ด๋ฏ€๋กœ, **ํ‘œ๋ณธ ๋ถˆ๋Ÿ‰ ์ˆ˜๊ฐ€ ํ•ฉ๊ฒฉํŒ์ •์ˆ˜ ์ดํ•˜์ด๋ฉด ํ•ฉ๊ฒฉ์‹œํ‚ค๋Š” ํ†ต๊ณ„์  ์ƒ˜ํ”Œ๋ง ๊ธฐ๋ฐ˜ ๋กœํŠธ ํ•ฉ๊ฒฉ/๋ถˆํ•ฉ๊ฒฉ ํŒ์ • Baseline์„ ๊ตฌํ˜„ํ•˜๊ณ , ํƒ์ƒ‰์  ๋ถ„์„์œผ๋กœ ๋ถˆ๋Ÿ‰ ๊ด€๋ จ ์š”์ธ์„ ์ฐพ๋Š”๋‹ค.** ๊ณผ์—…์„ ์™„์ˆ˜ํ•˜์—ฌ ์˜คํŒ์„ ๋ฐฉ์ง€ํ•˜๊ณ  ์ž‘์ „ ๋Œ€์‘๋ ฅ์„ ๊ทน๋Œ€ํ™”ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.\n", "\n", "์ด ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด ๋ณธ ๋ฏธ์…˜์—์„œ๋Š” **๋กœํŠธ ํฌ๊ธฐ๋ณ„ ํ‘œ๋ณธ ์ˆ˜์™€ ํ•ฉ๊ฒฉํŒ์ •์ˆ˜๋กœ ํŒ์ •ํ•œ ๋’ค ํ™•์ • ๋ถˆ๋Ÿ‰๋ฅ ๊ณผ ๋น„๊ตํ•ด ์˜คํ•ฉ๊ฒฉยท์˜ค๋ถˆํ•ฉ๊ฒฉ์„ ์ธก์ • ์ˆ˜ํ–‰ํ•˜๊ณ  ๊ฒ€์ฆํ•˜๋Š”** ์‹ค์Šต์„ ์ฒด๊ณ„์ ์œผ๋กœ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค.\n", "\n", "ํ•™์Šต์ž๋Š” ์‹ค์Šต๊ฐ€์ด๋“œ์— ์ œ์‹œ๋œ ์ด 4๊ฐœ ์„ธ๋ถ€ ํ™œ๋™(๊ธฐ๋ณธ ๊ธฐ๋Šฅ ๊ตฌํ˜„ โž” ๊ฒฐ๊ณผ ์ง€ํ‘œ ๊ฒ€์ฆ โž” ํŒŒ๋ผ๋ฏธํ„ฐ ํŠœ๋‹ ๋ฐ ๋„์ „๊ณผ์ œ)์„ ์ˆœ์ฐจ์ ์œผ๋กœ ์ง„ํ–‰ํ•˜๋ฉฐ ํ•ต์‹ฌ ํŒ์ • ๊ธฐ์ค€์„ ๋‹ฌ์„ฑํ•ฉ๋‹ˆ๋‹ค.\n", "\n", "**์ด๋ฒˆ ๊ฒฐ๊ณผ๋ฌผ:** `notebooks/qlt02_eda_analysis.ipynb ๋ฐ EDA ์š”์•ฝ ๋ฆฌํฌํŠธ` ํ˜•ํƒœ์˜ ๊ฒฐ๊ณผ๋ฌผ์„ ์™„์„ฑํ•œ๋‹ค.\n", "\n", "### ๐Ÿ” ํ‰๊ฐ€ ๋ฐ ๊ฒ€์ฆ ๊ธฐ์ค€ (PASS ์กฐ๊ฑด)\n", "#### ๋ฏธ์…˜ ํŒ์ •\n", "\n", "**ํ‰๊ฐ€ ๊ตฌ๋ถ„:** ํ•„์ˆ˜\n", "\n", "**ํ•ต์‹ฌ ํ‰๊ฐ€์˜์—ญ:** ๊ตฐ์ˆ˜ ์˜์‚ฌ๊ฒฐ์ •์— ์ง๊ด€์ ์ธ ์ธ์‚ฌ์ดํŠธ ์ œ๊ณต ์ˆ˜์ค€\n", "\n", "#### ๋ฏธ์…˜ PASS ์กฐ๊ฑด\n", "\n", "- ์ •๋Ÿ‰ ๊ธฐ์ค€: EDA ์‹œ๊ฐํ™” ์ฐจํŠธ 8์ข… ์ด์ƒ ์ƒ์„ฑ, ์œ ์˜๋ฏธํ•œ ์ƒ๊ด€๊ด€๊ณ„ ํ”ผ์ฒ˜ 5๊ฐœ ์ด์ƒ ๋„์ถœ\n", "\n", "\n", "- ์ง„ํ–‰ ๊ธฐ์ค€: EDA ๋…ธํŠธ๋ถ ์‹คํ–‰ ์™„๋ฃŒ ๋ฐ ํ•ต์‹ฌ ๋ฐœ๊ฒฌ์ (Key Findings) ๋ฌธ์„œํ™”\n", "\n", "\n", "- ๊ฒ€์ฆ ๋ฐฉ๋ฒ•: ํ†ต๊ณ„์  ๊ฐ€์„ค ๊ฒ€์ •(p-value, ์ƒ๊ด€๊ณ„์ˆ˜)์„ ํ†ตํ•œ ์œ ์˜์„ฑ ๊ฒ€์ฆ\n", "\n", "\n", "#### ์ฑ„์ ์ž ํŒ์ • ๋ฉ”๋ชจ\n", "\n", "> ๋‹จ์ˆœ ์ฝ”๋“œ ์‹คํ–‰ ์—ฌ๋ถ€๋ฟ๋งŒ ์•„๋‹ˆ๋ผ ์ถœ๋ ฅ๋œ ์ˆ˜์น˜์™€ ๊ทผ๊ฑฐ ์ž๋ฃŒ๊ฐ€ ๊ตฐ ์‹ค๋ฌด ์š”๊ตฌ์‚ฌํ•ญ์— ๋ถ€ํ•ฉํ•˜๋Š”์ง€ ๋ฉด๋ฐ€ํžˆ ํ™•์ธํ•œ๋‹ค.\n", "\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "\"\"\"ํ‘œ๋ณธ ๋ถˆ๋Ÿ‰ ์ˆ˜๊ฐ€ ํ•ฉ๊ฒฉํŒ์ •์ˆ˜(c) ์ดํ•˜์ด๋ฉด ํ•ฉ๊ฒฉ์‹œํ‚ค๋Š” ํ†ต๊ณ„์  ์ƒ˜ํ”Œ๋ง ๊ธฐ๋ฐ˜ ๋กœํŠธ ํ•ฉ๊ฒฉ/๋ถˆํ•ฉ๊ฒฉ ํŒ์ • Baseline์„ ๊ตฌํ˜„ํ•ด ์˜คํŒ์ •(์˜คํ•ฉ๊ฒฉยท์˜ค๋ถˆํ•ฉ๊ฒฉ)์„ ์ธก์ •ํ•˜๊ณ , ํƒ์ƒ‰์  ๋ฐ์ดํ„ฐ ๋ถ„์„(์ฐจํŠธ 8์ข…ยท์ƒ๊ด€ ๊ฒ€์ •ยทํŒŒ๋ ˆํ†  ABC)์œผ๋กœ ๋ถˆ๋Ÿ‰๊ณผ ๊ด€๋ จ๋œ ๊ทœ๊ฒฉยท๊ตฌ๋งค์š”๊ตฌยท๋‚ฉ๊ธฐ ์š”์ธ์„ ์ฐพ๋Š”๋‹ค.\"\"\"\n", "import numpy as np\n", "import pandas as pd\n", "import matplotlib.pyplot as plt\n", "from scipy import stats\n", "\n", "# ---------------------------------------------------------------------------\n", "# [๊ต์œก์šฉ๊ฐ€์ƒ์ž๋ฃŒ] ๊ตฐ์ˆ˜ํ’ˆ ์ˆ˜๋ น๊ฒ€์‚ฌ ๋กœํŠธ ๋ฐ์ดํ„ฐ ์ƒ์„ฑ๊ธฐ (์‹ค์ œ ์—…์ฒดยทํ’ˆ๋ชฉยท๊ณ„์•ฝ ์ •๋ณด ์—†์Œ)\n", "# ๊ตฌ๋งค์š”๊ตฌ(๊ทœ๊ฒฉยท๊ธด๊ธ‰ ์—ฌ๋ถ€) -> ๊ณ„์•ฝ(๋‚ฉ๊ธฐ) -> ๋‚ฉํ’ˆ -> ์ˆ˜๋ น๊ฒ€์‚ฌ(ํ‘œ๋ณธ) -> ํ™•์ • ๋ถˆ๋Ÿ‰๋ฅ (์ „์ˆ˜๊ฒ€์‚ฌยท์‚ฌ์šฉ๋ถ€๋Œ€ ํ•˜์ž ๋ฐ˜์˜)\n", "# ---------------------------------------------------------------------------\n", "FSC = {\"5305\": \"screw/bolt\", \"5330\": \"gasket\", \"5935\": \"connector\", \"6135\": \"battery\", \"2530\": \"brake part\", \"8415\": \"clothing\"}\n", "VENDORS = [f\"V{i:02d}\" for i in range(1, 13)]\n", "VENDOR_Q = dict(zip(VENDORS, [-0.6, -0.4, -0.3, -0.2, -0.1, 0.0, 0.1, 0.2, 0.3, 0.5, 0.8, 1.1])) # ์—…์ฒด ๊ณ ์œ  ํ’ˆ์งˆ ์ˆ˜์ค€(์ˆจ์€ ๊ฐ’)\n", "FSC_Q = {\"5305\": -0.3, \"5330\": 0.0, \"5935\": 0.25, \"6135\": 0.1, \"2530\": 0.35, \"8415\": -0.2}\n", "SAMPLE_PLAN = [(500, 50, 3), (1200, 80, 5), (3200, 125, 7), (10 ** 9, 200, 10)] # (๋กœํŠธ ํฌ๊ธฐ ์ƒํ•œ, ํ‘œ๋ณธ n, ํ•ฉ๊ฒฉํŒ์ •์ˆ˜ c) ๋‹จ์ˆœํ™” ๊ธฐ์ค€\n", "BAD_RATE = 0.05 # ํ™•์ • ๋ถˆ๋Ÿ‰๋ฅ  5% ์ดˆ๊ณผ ๋กœํŠธ = ๋ถˆํ•ฉ๊ฒฉ ๋Œ€์ƒ\n", "\n", "\n", "def sample_plan(lot_size):\n", " for up, n, c in SAMPLE_PLAN:\n", " if lot_size <= up:\n", " return n, c\n", "\n", "\n", "def true_rate(d, rng=None):\n", " \"\"\"์ˆจ์€ ๋ถˆ๋Ÿ‰ ๋ฐœ์ƒ ๊ตฌ์กฐ: ์—…์ฒด ์ˆ˜์ค€ + ํ’ˆ๋ชฉ๊ตฐ + ์ •๋ฐ€ ๊ทœ๊ฒฉ + ๊ธด๊ธ‰ ๊ตฌ๋งค + ๊ทœ๊ฒฉ ๋ณ€๊ฒฝยท๋ถˆ์ผ์น˜ + ๋‚ฉ๊ธฐ ์ง€์—ฐ + (์ •๋ฐ€ ๊ทœ๊ฒฉ x ์ด‰๋ฐ• ๋‚ฉ๊ธฐ) ์ƒํ˜ธ์ž‘์šฉ.\"\"\"\n", " short = (d[\"lead_days\"] < 45).astype(float)\n", " z = (-4.35 + d[\"vendor\"].map(VENDOR_Q) + d[\"fsc\"].map(FSC_Q) + 0.55 * (d[\"spec_class\"] == \"precision\")\n", " + 0.35 * d[\"pr_urgent\"] + 0.6 * d[\"spec_changed\"] + 0.8 * d[\"spec_mismatch\"]\n", " + 0.9 * np.log1p(np.maximum(d[\"delay_days\"], 0) / 7) + 0.9 * short * (d[\"spec_class\"] == \"precision\")\n", " + 0.5 * (d[\"month\"] >= 18) * (d[\"vendor\"] == \"V10\")) # V10 ํ›„๋ฐ˜๊ธฐ ํ’ˆ์งˆ ์ €ํ•˜(๋“œ๋ฆฌํ”„ํŠธ)\n", " if rng is not None:\n", " z = z + rng.normal(0, 0.12, len(d))\n", " return np.clip(np.exp(z), 0.002, 0.35)\n", "\n", "\n", "def make_lots(n=1200, seed=42, months=24):\n", " rng = np.random.default_rng(seed)\n", " d = pd.DataFrame({\"lot_id\": [f\"LOT{seed % 100:02d}-{i:05d}\" for i in range(n)]})\n", " d[\"month\"] = rng.integers(0, months, n)\n", " d[\"receipt_date\"] = pd.Timestamp(\"2024-01-01\") + pd.to_timedelta(d[\"month\"] * 30 + rng.integers(0, 30, n), unit=\"D\")\n", " d = d.sort_values(\"receipt_date\").reset_index(drop=True)\n", " d[\"month\"] = ((d[\"receipt_date\"] - pd.Timestamp(\"2024-01-01\")).dt.days // 30).clip(0, months - 1)\n", " d[\"vendor\"] = rng.choice(VENDORS, n, p=np.r_[np.full(6, 0.1), np.full(6, 0.4 / 6)])\n", " d[\"fsc\"] = rng.choice(list(FSC), n)\n", " d[\"niin\"] = [f\"{f}-01-{int(k):03d}-{int(k) * 37 % 10000:04d}\" for f, k in zip(d[\"fsc\"], rng.integers(100, 110, n))]\n", " d[\"spec_class\"] = np.where(rng.random(n) < np.where(d[\"fsc\"].isin([\"5935\", \"2530\"]), 0.6, 0.25), \"precision\", \"general\")\n", " d[\"tolerance_mm\"] = np.where(d[\"spec_class\"] == \"precision\", rng.uniform(0.05, 0.1, n), rng.uniform(0.2, 0.5, n)).round(3)\n", " d[\"spec_rev\"] = rng.integers(1, 6, n)\n", " d[\"pr_urgent\"] = (rng.random(n) < 0.2).astype(int)\n", " d[\"spec_changed\"] = (rng.random(n) < 0.12).astype(int)\n", " d[\"spec_mismatch\"] = (rng.random(n) < 0.08).astype(int)\n", " d[\"pr_qty\"] = rng.integers(200, 5000, n)\n", " d[\"lot_size\"] = (d[\"pr_qty\"] * rng.uniform(0.5, 1.0, n)).astype(int)\n", " d[\"lead_days\"] = np.where(d[\"pr_urgent\"] == 1, rng.integers(20, 60, n), rng.integers(40, 150, n))\n", " d[\"delay_days\"] = np.round(rng.gamma(1.2, 6, n) * rng.binomial(1, 0.45, n) - rng.integers(0, 5, n)).astype(int)\n", " d[\"unit_cost\"] = rng.choice([2_000, 15_000, 40_000, 120_000], n, p=[0.35, 0.3, 0.25, 0.1])\n", " p = true_rate(d, rng)\n", " d[\"sample_n\"], d[\"accept_c\"] = zip(*[sample_plan(s) for s in d[\"lot_size\"]])\n", " d[\"defects\"] = rng.binomial(d[\"sample_n\"], p)\n", " share = np.where(d[\"spec_class\"] == \"precision\", 0.55, 0.3)\n", " d[\"def_dim\"] = rng.binomial(d[\"defects\"], share)\n", " d[\"def_spec\"] = rng.binomial(d[\"defects\"] - d[\"def_dim\"], 0.6)\n", " d[\"def_visual\"] = d[\"defects\"] - d[\"def_dim\"] - d[\"def_spec\"]\n", " d[\"dim_dev_mm\"] = (d[\"tolerance_mm\"] * (0.3 + 2.0 * p) * rng.lognormal(0, 0.3, n)).round(4)\n", " d[\"confirmed_rate\"] = np.clip(p * rng.lognormal(0, 0.05, n), 0.001, 0.4).round(4) # ์ „์ˆ˜๊ฒ€์‚ฌยทํ•˜์ž ๋ฐ˜์˜ ํ™•์ • ๋ถˆ๋Ÿ‰๋ฅ \n", " d[\"field_claims\"] = rng.poisson(d[\"lot_size\"] * p * 0.03)\n", " d[\"inspector\"] = rng.choice([\"INS-1\", \"INS-2\", \"INS-3\", \"INS-4\"], n)\n", " d[\"emergency_release\"] = ((d[\"pr_urgent\"] == 1) & (rng.random(n) < 0.3)).astype(int) # ๊ธด๊ธ‰ ๋ถˆ์ถœ(๊ฒ€์‚ฌ ์ „ ์„ ๋ถˆ์ถœ) ์˜ˆ์™ธ\n", " d[\"bad_lot\"] = (d[\"confirmed_rate\"] > BAD_RATE).astype(int)\n", " return d\n", "# ---------------------------------------------------------------------------\n", "\n", "d = make_lots(1200, seed=42)\n", "\n", "# 1) Baseline: ๋‹จ์ผ ์ƒ˜ํ”Œ๋ง ํŒ์ • (ํ‘œ๋ณธ ๋ถˆ๋Ÿ‰ ์ˆ˜ <= c ์ด๋ฉด ํ•ฉ๊ฒฉ)\n", "d[\"base_accept\"] = (d[\"defects\"] <= d[\"accept_c\"]).astype(int)\n", "cm = pd.crosstab(d[\"bad_lot\"].map({0: \"์‹ค์ œ ์–‘ํ˜ธ\", 1: \"์‹ค์ œ ๋ถˆ๋Ÿ‰\"}), d[\"base_accept\"].map({1: \"ํ•ฉ๊ฒฉ ํŒ์ •\", 0: \"๋ถˆํ•ฉ๊ฒฉ ํŒ์ •\"}))\n", "print(\"[Baseline ํŒ์ • ํ˜ผ๋™ํ‘œ] (๋ถˆ๋Ÿ‰ ๋กœํŠธ ๊ธฐ์ค€: ํ™•์ • ๋ถˆ๋Ÿ‰๋ฅ  5% ์ดˆ๊ณผ)\")\n", "print(cm.to_string())\n", "acc = (d[\"base_accept\"] == 1 - d[\"bad_lot\"]).mean()\n", "cons = ((d[\"base_accept\"] == 1) & (d[\"bad_lot\"] == 1)).sum() / d[\"bad_lot\"].sum()\n", "prod = ((d[\"base_accept\"] == 0) & (d[\"bad_lot\"] == 0)).sum() / (d[\"bad_lot\"] == 0).sum()\n", "print(f\"ํŒ์ • ์ •ํ™•๋„ {acc:.1%}, ์˜คํ•ฉ๊ฒฉ(์†Œ๋น„์ž ์œ„ํ—˜) {cons:.1%}, ์˜ค๋ถˆํ•ฉ๊ฒฉ(์ƒ์‚ฐ์ž ์œ„ํ—˜) {prod:.1%}\")\n", "print(\"ํ•œ๊ณ„: ํ‘œ๋ณธ ๋ถˆ๋Ÿ‰ ์ˆ˜๋งŒ ๋ณด๋ฏ€๋กœ ์—…์ฒด ์ด๋ ฅยท๊ทœ๊ฒฉ ๋ณ€๊ฒฝยท๋‚ฉ๊ธฐ ์ง€์—ฐ ๊ฐ™์€ ์‚ฌ์ „ ์ •๋ณด๋ฅผ ์“ฐ์ง€ ๋ชปํ•˜๊ณ , ๊ฒฝ๊ณ„์„  ๋กœํŠธ์—์„œ ์˜คํŒ์ •์ด ๋ชฐ๋ฆฐ๋‹ค.\")\n", "\n", "# 2) ์ƒ๊ด€ ๋ถ„์„: ํ™•์ • ๋ถˆ๋Ÿ‰๋ฅ ๊ณผ ๊ฐ ์š”์ธ์˜ ์Šคํ”ผ์–ด๋งŒ ์ƒ๊ด€๊ณ„์ˆ˜ยทp-value\n", "cand = {\"delay_days\": d[\"delay_days\"], \"lead_days\": d[\"lead_days\"], \"precision_spec\": (d[\"spec_class\"] == \"precision\").astype(int),\n", " \"pr_urgent\": d[\"pr_urgent\"], \"spec_changed\": d[\"spec_changed\"], \"spec_mismatch\": d[\"spec_mismatch\"],\n", " \"dim_dev/tol\": d[\"dim_dev_mm\"] / d[\"tolerance_mm\"], \"lot_size\": d[\"lot_size\"], \"spec_rev\": d[\"spec_rev\"], \"unit_cost\": d[\"unit_cost\"]}\n", "rows = []\n", "for k, v in cand.items():\n", " r, p = stats.spearmanr(v, d[\"confirmed_rate\"])\n", " rows.append({\"์š”์ธ\": k, \"์ƒ๊ด€๊ณ„์ˆ˜\": round(r, 3), \"p-value\": f\"{p:.1e}\", \"์œ ์˜(p<0.05)\": \"์˜ˆ\" if p < 0.05 else \"์•„๋‹ˆ์˜ค\"})\n", "cr = pd.DataFrame(rows).sort_values(\"์ƒ๊ด€๊ณ„์ˆ˜\", key=abs, ascending=False)\n", "print(\"\\n[ํ™•์ • ๋ถˆ๋Ÿ‰๋ฅ ๊ณผ ์š”์ธ๋ณ„ ์ƒ๊ด€ ๊ฒ€์ •]\")\n", "print(cr.to_string(index=False))\n", "n_sig = (cr[\"์œ ์˜(p<0.05)\"] == \"์˜ˆ\").sum()\n", "print(f\"์œ ์˜๋ฏธํ•œ ์ƒ๊ด€ ํ”ผ์ฒ˜ {n_sig}๊ฐœ (๋ชฉํ‘œ 5๊ฐœ ์ด์ƒ: {'๋‹ฌ์„ฑ' if n_sig >= 5 else '๋ฏธ๋‹ฌ'})\")\n", "chi = stats.chi2_contingency(pd.crosstab(d[\"vendor\"], d[\"bad_lot\"]))\n", "print(f\"์—…์ฒด์™€ ๋ถˆ๋Ÿ‰ ๋กœํŠธ ์—ฌ๋ถ€์˜ ๋…๋ฆฝ์„ฑ ์นด์ด์ œ๊ณฑ ๊ฒ€์ • p-value {chi.pvalue:.1e} -> ์—…์ฒด๋ณ„ ์ฐจ์ด {'์žˆ์Œ' if chi.pvalue < 0.05 else '์—†์Œ'}\")\n", "\n", "# 3) ํŒŒ๋ ˆํ† (ABC) ๋ถ„์„: ๋ถˆ๋Ÿ‰ ๊ฐœ์ˆ˜ ์ถ”์ •์น˜(๋กœํŠธ ํฌ๊ธฐ x ํ™•์ • ๋ถˆ๋Ÿ‰๋ฅ ) ๊ธฐ์ค€ ์—…์ฒด ์ˆœ์œ„\n", "d[\"def_units\"] = d[\"lot_size\"] * d[\"confirmed_rate\"]\n", "par = d.groupby(\"vendor\")[\"def_units\"].sum().sort_values(ascending=False)\n", "cum = par.cumsum() / par.sum()\n", "abc = np.where(cum <= 0.5, \"A\", np.where(cum <= 0.8, \"B\", \"C\"))\n", "top = par.index[: int(np.ceil(len(par) * 0.2))]\n", "print(f\"\\n[ํŒŒ๋ ˆํ†  ABC] ์ƒ์œ„ 20% ์—…์ฒด {list(top)}๊ฐ€ ์ถ”์ • ๋ถˆ๋Ÿ‰ ๊ฐœ์ˆ˜์˜ {par[top].sum() / par.sum():.0%}๋ฅผ ์ฐจ์ง€ -> ํ•ต์‹ฌ ๊ด€๋ฆฌ ๋Œ€์ƒ\")\n", "print(\"A๋“ฑ๊ธ‰:\", list(par.index[abc == \"A\"]), \"/ B๋“ฑ๊ธ‰:\", list(par.index[abc == \"B\"]))\n", "\n", "# 4) EDA ์ฐจํŠธ 8์ข…\n", "fig, ax = plt.subplots(2, 4, figsize=(17, 8))\n", "ax[0, 0].hist(d[\"confirmed_rate\"], bins=40, color=\"tab:blue\"); ax[0, 0].axvline(BAD_RATE, c=\"r\", ls=\"--\"); ax[0, 0].set_title(\"1. Confirmed defect rate\")\n", "d.groupby(\"month\")[\"confirmed_rate\"].mean().plot(ax=ax[0, 1], marker=\"o\"); ax[0, 1].set_title(\"2. Monthly mean defect rate\")\n", "par.plot.bar(ax=ax[0, 2], color=\"tab:orange\"); ax2 = ax[0, 2].twinx(); ax2.plot(range(len(cum)), cum.values, \"k-o\"); ax[0, 2].set_title(\"3. Vendor Pareto\")\n", "d.boxplot(column=\"confirmed_rate\", by=\"spec_class\", ax=ax[0, 3]); ax[0, 3].set_title(\"4. Rate by spec class\")\n", "ax[1, 0].scatter(d[\"delay_days\"], d[\"confirmed_rate\"], s=6, alpha=0.5); ax[1, 0].set_title(\"5. Delivery delay vs rate\"); ax[1, 0].set_xlabel(\"delay days\")\n", "pd.crosstab(d[\"fsc\"], d[\"bad_lot\"], normalize=\"index\")[1].plot.bar(ax=ax[1, 1], color=\"tab:red\"); ax[1, 1].set_title(\"6. Bad-lot share by FSC\")\n", "d[[\"def_spec\", \"def_dim\", \"def_visual\"]].sum().plot.pie(ax=ax[1, 2], autopct=\"%.0f%%\", labels=[\"spec\", \"dimension\", \"visual\"]); ax[1, 2].set_ylabel(\"\"); ax[1, 2].set_title(\"7. Defect type mix\")\n", "cmat = pd.DataFrame(cand).assign(rate=d[\"confirmed_rate\"]).corr(method=\"spearman\")\n", "ax[1, 3].imshow(cmat, cmap=\"coolwarm\", vmin=-1, vmax=1); ax[1, 3].set_xticks(range(len(cmat))); ax[1, 3].set_xticklabels(cmat.columns, rotation=90, fontsize=7)\n", "ax[1, 3].set_yticks(range(len(cmat))); ax[1, 3].set_yticklabels(cmat.columns, fontsize=7); ax[1, 3].set_title(\"8. Spearman correlation\")\n", "plt.suptitle(\"QLT02 EDA - synthetic receiving inspection data\"); plt.tight_layout(); plt.show()\n", "print(f\"\\nEDA ์ฐจํŠธ {len(fig.axes) - 1}์ข… ์ƒ์„ฑ (๋ชฉํ‘œ 8์ข… ์ด์ƒ: ๋‹ฌ์„ฑ)\")\n", "\n", "print(\"\\n[ํ•ต์‹ฌ ๋ฐœ๊ฒฌ์ ]\")\n", "print(f\" 1. ์ •๋ฐ€ ๊ทœ๊ฒฉ ๋กœํŠธ์˜ ํ‰๊ท  ํ™•์ • ๋ถˆ๋Ÿ‰๋ฅ  {d[d.spec_class == 'precision'].confirmed_rate.mean():.1%}, ์ผ๋ฐ˜ ๊ทœ๊ฒฉ {d[d.spec_class == 'general'].confirmed_rate.mean():.1%}\")\n", "print(f\" 2. ๊ทœ๊ฒฉ ๋ถˆ์ผ์น˜ ๋กœํŠธ {d[d.spec_mismatch == 1].confirmed_rate.mean():.1%} vs ์ผ์น˜ {d[d.spec_mismatch == 0].confirmed_rate.mean():.1%}\")\n", "print(f\" 3. ๋‚ฉ๊ธฐ 14์ผ ์ด์ƒ ์ง€์—ฐ ๋กœํŠธ {d[d.delay_days >= 14].confirmed_rate.mean():.1%} vs ์ •์‹œ ๋‚ฉํ’ˆ {d[d.delay_days <= 0].confirmed_rate.mean():.1%}\")\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "
\n", "๐Ÿ’ก [์ฐธ๊ณ ] ๋ฏธ์…˜ 03 ์˜ˆ์ƒ ์‹คํ–‰ ๊ฒฐ๊ณผ (Output)\n", "\n", "```text\n", "[Baseline ํŒ์ • ํ˜ผ๋™ํ‘œ] (๋ถˆ๋Ÿ‰ ๋กœํŠธ ๊ธฐ์ค€: ํ™•์ • ๋ถˆ๋Ÿ‰๋ฅ  5% ์ดˆ๊ณผ)\n", "base_accept ๋ถˆํ•ฉ๊ฒฉ ํŒ์ • ํ•ฉ๊ฒฉ ํŒ์ •\n", "bad_lot \n", "์‹ค์ œ ๋ถˆ๋Ÿ‰ 203 79\n", "์‹ค์ œ ์–‘ํ˜ธ 34 884\n", "ํŒ์ • ์ •ํ™•๋„ 90.6%, ์˜คํ•ฉ๊ฒฉ(์†Œ๋น„์ž ์œ„ํ—˜) 28.0%, ์˜ค๋ถˆํ•ฉ๊ฒฉ(์ƒ์‚ฐ์ž ์œ„ํ—˜) 3.7%\n", "ํ•œ๊ณ„: ํ‘œ๋ณธ ๋ถˆ๋Ÿ‰ ์ˆ˜๋งŒ ๋ณด๋ฏ€๋กœ ์—…์ฒด ์ด๋ ฅยท๊ทœ๊ฒฉ ๋ณ€๊ฒฝยท๋‚ฉ๊ธฐ ์ง€์—ฐ ๊ฐ™์€ ์‚ฌ์ „ ์ •๋ณด๋ฅผ ์“ฐ์ง€ ๋ชปํ•˜๊ณ , ๊ฒฝ๊ณ„์„  ๋กœํŠธ์—์„œ ์˜คํŒ์ •์ด ๋ชฐ๋ฆฐ๋‹ค.\n", "\n", "[ํ™•์ • ๋ถˆ๋Ÿ‰๋ฅ ๊ณผ ์š”์ธ๋ณ„ ์ƒ๊ด€ ๊ฒ€์ •]\n", " ์š”์ธ ์ƒ๊ด€๊ณ„์ˆ˜ p-value ์œ ์˜(p<0.05)\n", " dim_dev/tol 0.459 1.1e-63 ์˜ˆ\n", "precision_spec 0.442 1.7e-58 ์˜ˆ\n", " delay_days 0.288 2.5e-24 ์˜ˆ\n", " pr_urgent 0.277 1.6e-22 ์˜ˆ\n", " spec_mismatch 0.267 5.3e-21 ์˜ˆ\n", " spec_changed 0.211 1.4e-13 ์˜ˆ\n", " lead_days -0.182 2.1e-10 ์˜ˆ\n", " lot_size 0.024 4.1e-01 ์•„๋‹ˆ์˜ค\n", " unit_cost 0.014 6.3e-01 ์•„๋‹ˆ์˜ค\n", " spec_rev 0.002 9.3e-01 ์•„๋‹ˆ์˜ค\n", "์œ ์˜๋ฏธํ•œ ์ƒ๊ด€ ํ”ผ์ฒ˜ 7๊ฐœ (๋ชฉํ‘œ 5๊ฐœ ์ด์ƒ: ๋‹ฌ์„ฑ)\n", "์—…์ฒด์™€ ๋ถˆ๋Ÿ‰ ๋กœํŠธ ์—ฌ๋ถ€์˜ ๋…๋ฆฝ์„ฑ ์นด์ด์ œ๊ณฑ ๊ฒ€์ • p-value 2.3e-55 -> ์—…์ฒด๋ณ„ ์ฐจ์ด ์žˆ์Œ\n", "\n", "[ํŒŒ๋ ˆํ†  ABC] ์ƒ์œ„ 20% ์—…์ฒด ['V12', 'V11', 'V10']๊ฐ€ ์ถ”์ • ๋ถˆ๋Ÿ‰ ๊ฐœ์ˆ˜์˜ 43%๋ฅผ ์ฐจ์ง€ -> ํ•ต์‹ฌ ๊ด€๋ฆฌ ๋Œ€์ƒ\n", "A๋“ฑ๊ธ‰: ['V12', 'V11', 'V10'] / B๋“ฑ๊ธ‰: ['V05', 'V09', 'V03', 'V06', 'V04']\n", "\n", "EDA ์ฐจํŠธ 8์ข… ์ƒ์„ฑ (๋ชฉํ‘œ 8์ข… ์ด์ƒ: ๋‹ฌ์„ฑ)\n", "\n", "[ํ•ต์‹ฌ ๋ฐœ๊ฒฌ์ ]\n", " 1. ์ •๋ฐ€ ๊ทœ๊ฒฉ ๋กœํŠธ์˜ ํ‰๊ท  ํ™•์ • ๋ถˆ๋Ÿ‰๋ฅ  6.6%, ์ผ๋ฐ˜ ๊ทœ๊ฒฉ 2.8%\n", " 2. ๊ทœ๊ฒฉ ๋ถˆ์ผ์น˜ ๋กœํŠธ 8.1% vs ์ผ์น˜ 3.9%\n", " 3. ๋‚ฉ๊ธฐ 14์ผ ์ด์ƒ ์ง€์—ฐ ๋กœํŠธ 9.4% vs ์ •์‹œ ๋‚ฉํ’ˆ 3.4%\n", "```\n", "
\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "---\n", "\n", "## ๐ŸŽฏ ๋ฏธ์…˜ 04. ๋‹ค๋ณ€๋Ÿ‰ ๋ถ„์„ ๋ฐ ๋จธ์‹ ๋Ÿฌ๋‹ ๊ธฐ๋ฐ˜ ์€๋‹‰ ํ’ˆ์งˆ ๊ฒฐํ•จ ํŒจํ„ด ์‹๋ณ„ ๋ชจ๋ธ ๊ณ ๋„ํ™”\n", "**์†Œ์š”์‹œ๊ฐ„**: ์•ฝ 3.0์‹œ๊ฐ„\n", "\n", "### ๐Ÿ“‹ ๋ฏธ์…˜ ๋ฐฐ๊ฒฝ ๋ฐ ์ˆ˜ํ–‰ ์ง€์นจ\n", "#### ๋ฏธ์…˜ ๋ฐฐ๊ฒฝ ๋ฐ ๋‹น์œ„์„ฑ\n", "\n", "๋‹ค๋ณ€๋Ÿ‰ ๋ถ„์„ ๊ธฐ๋ฐ˜ ์€๋‹‰ ํ’ˆ์งˆ ๊ฒฐํ•จ ํŒจํ„ด ์‹๋ณ„ ๋ชจ๋ธ์„ ๊ณ ๋„ํ™”ํ•œ๋‹ค. ์‹ค์ œ ์ „์ˆ  ํ˜„์žฅ์—์„œ๋Š” ๋‹จ์ˆœ ํƒ์ง€์— ๊ทธ์น˜์ง€ ์•Š๊ณ  ์‹ ์†ํ•œ ์œ„ํ˜‘ ํŒ๋‹จ๊ณผ ์‹ ๋ขฐ๋„ ๋†’์€ ์˜์‚ฌ๊ฒฐ์ •์ด ํ•„์ˆ˜์ ์ด๋ฏ€๋กœ, **๋‹ค๋ณ€๋Ÿ‰ ๋ถ„์„์œผ๋กœ ๋‹จ์ผ ์š”์ธ์—์„œ ๋“œ๋Ÿฌ๋‚˜์ง€ ์•Š๋Š” ์€๋‹‰ ํ’ˆ์งˆ ๊ฒฐํ•จ ํŒจํ„ด(๊ทœ๊ฒฉ x ๋‚ฉ๊ธฐ ์ƒํ˜ธ์ž‘์šฉ, ์—…์ฒด ํ’ˆ์งˆ ์ถ”์„ธ)์„ ์‹๋ณ„ํ•˜๋Š” ํ”ผ์ฒ˜์™€ ๊ธฐ์ค€ ๋ชจ๋ธ์„ ๋งŒ๋“ ๋‹ค.** ๊ณผ์—…์„ ์™„์ˆ˜ํ•˜์—ฌ ์˜คํŒ์„ ๋ฐฉ์ง€ํ•˜๊ณ  ์ž‘์ „ ๋Œ€์‘๋ ฅ์„ ๊ทน๋Œ€ํ™”ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.\n", "\n", "์ด ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด ๋ณธ ๋ฏธ์…˜์—์„œ๋Š” **ํ‘œ๋ณธ ๋ถˆ๋Ÿ‰๋ฅ , ์ง€์—ฐ ๋น„์œจ, ์ •๋ฐ€ ๊ทœ๊ฒฉ x ์ด‰๋ฐ• ๋‚ฉ๊ธฐ, ์น˜์ˆ˜ ํŽธ์ฐจ/๊ณต์ฐจ, ์—…์ฒด ์ตœ๊ทผ ์ถ”์„ธ ๋“ฑ ํŒŒ์ƒ ํ”ผ์ฒ˜ 10๊ฐœ ์ด์ƒ ์ž‘์„ฑํ•˜์—ฌ ์›์ธ์„ ๋ถ„์„ํ•˜๋Š”** ์‹ค์Šต์„ ์ฒด๊ณ„์ ์œผ๋กœ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค.\n", "\n", "ํ•™์Šต์ž๋Š” ์‹ค์Šต๊ฐ€์ด๋“œ์— ์ œ์‹œ๋œ ์ด 4๊ฐœ ์„ธ๋ถ€ ํ™œ๋™(๊ธฐ๋ณธ ๊ธฐ๋Šฅ ๊ตฌํ˜„ โž” ๊ฒฐ๊ณผ ์ง€ํ‘œ ๊ฒ€์ฆ โž” ํŒŒ๋ผ๋ฏธํ„ฐ ํŠœ๋‹ ๋ฐ ๋„์ „๊ณผ์ œ)์„ ์ˆœ์ฐจ์ ์œผ๋กœ ์ง„ํ–‰ํ•˜๋ฉฐ ํ•ต์‹ฌ ํŒ์ • ๊ธฐ์ค€์„ ๋‹ฌ์„ฑํ•ฉ๋‹ˆ๋‹ค.\n", "\n", "**์ด๋ฒˆ ๊ฒฐ๊ณผ๋ฌผ:** `src/features/qlt02_feature_engineering.py ๋ฐ ๊ธฐ์ค€ ๋ชจ๋ธ` ํ˜•ํƒœ์˜ ๊ฒฐ๊ณผ๋ฌผ์„ ์™„์„ฑํ•œ๋‹ค.\n", "\n", "### ๐Ÿ” ํ‰๊ฐ€ ๋ฐ ๊ฒ€์ฆ ๊ธฐ์ค€ (PASS ์กฐ๊ฑด)\n", "#### ๋ฏธ์…˜ ํŒ์ •\n", "\n", "**ํ‰๊ฐ€ ๊ตฌ๋ถ„:** ํ•„์ˆ˜\n", "\n", "**ํ•ต์‹ฌ ํ‰๊ฐ€์˜์—ญ:** ๊ตฐ์ˆ˜ ์‹ค๋ฌด์—์„œ ๋‚ฉ๋“ ๊ฐ€๋Šฅํ•œ ํ”ผ์ฒ˜ ์„ค๊ณ„์˜ ๋…ผ๋ฆฌ์„ฑ\n", "\n", "#### ๋ฏธ์…˜ PASS ์กฐ๊ฑด\n", "\n", "- ์ •๋Ÿ‰ ๊ธฐ์ค€: ํŒŒ์ƒ ํ”ผ์ฒ˜ 10๊ฐœ ์ด์ƒ ์ƒ์„ฑ, ๊ธฐ์ค€ ๋ชจ๋ธ MAPE 25% ์ดํ•˜ ๋‹ฌ์„ฑ\n", "\n", "\n", "- ์ง„ํ–‰ ๊ธฐ์ค€: ํ”ผ์ฒ˜ ํŒŒ์ดํ”„๋ผ์ธ ๋ฐ ๋ฒ ์ด์Šค๋ผ์ธ ๋ชจ๋ธ ์„ฑ๋Šฅ ์ง€ํ‘œ ๋„์ถœ\n", "\n", "\n", "- ๊ฒ€์ฆ ๋ฐฉ๋ฒ•: ๊ต์ฐจ ๊ฒ€์ฆ(Time-series Cross Validation)์„ ํ†ตํ•œ ๊ณผ์ ํ•ฉ ๋ฐฉ์ง€\n", "\n", "\n", "#### ์ฑ„์ ์ž ํŒ์ • ๋ฉ”๋ชจ\n", "\n", "> ๋‹จ์ˆœ ์ฝ”๋“œ ์‹คํ–‰ ์—ฌ๋ถ€๋ฟ๋งŒ ์•„๋‹ˆ๋ผ ์ถœ๋ ฅ๋œ ์ˆ˜์น˜์™€ ๊ทผ๊ฑฐ ์ž๋ฃŒ๊ฐ€ ๊ตฐ ์‹ค๋ฌด ์š”๊ตฌ์‚ฌํ•ญ์— ๋ถ€ํ•ฉํ•˜๋Š”์ง€ ๋ฉด๋ฐ€ํžˆ ํ™•์ธํ•œ๋‹ค.\n", "\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "\"\"\"๊ทœ๊ฒฉยท๊ตฌ๋งค์š”๊ตฌยท๋‚ฉ๊ธฐยท๊ฒ€์‚ฌ ์ •๋ณด๋กœ ํŒŒ์ƒ ํ”ผ์ฒ˜๋ฅผ ๋งŒ๋“ค๊ณ  ๋‹ค๋ณ€๋Ÿ‰ ์„ ํ˜• ๊ธฐ์ค€ ๋ชจ๋ธ(Ridge)๋กœ ๋กœํŠธ ํ™•์ • ๋ถˆ๋Ÿ‰๋ฅ ์„ ์˜ˆ์ธกํ•œ๋‹ค. ์‹œ๊ณ„์—ด ๊ต์ฐจ๊ฒ€์ฆ์œผ๋กœ ๊ณผ์ ํ•ฉ์„ ์ ๊ฒ€ํ•˜๊ณ , ์ƒํ˜ธ์ž‘์šฉยท์ถ”์„ธ ํ”ผ์ฒ˜๋กœ ๋‹จ์ผ ์š”์ธ ๋ถ„์„์—์„œ ๋ณด์ด์ง€ ์•Š๋˜ ์€๋‹‰ ๊ฒฐํ•จ ํŒจํ„ด์„ ์ฐพ๋Š”๋‹ค.\"\"\"\n", "import numpy as np\n", "import pandas as pd\n", "import matplotlib.pyplot as plt\n", "from sklearn.linear_model import Ridge\n", "from sklearn.pipeline import make_pipeline\n", "from sklearn.preprocessing import StandardScaler\n", "from sklearn.model_selection import TimeSeriesSplit\n", "from sklearn.inspection import permutation_importance\n", "\n", "# ---------------------------------------------------------------------------\n", "# [๊ต์œก์šฉ๊ฐ€์ƒ์ž๋ฃŒ] ๊ตฐ์ˆ˜ํ’ˆ ์ˆ˜๋ น๊ฒ€์‚ฌ ๋กœํŠธ ๋ฐ์ดํ„ฐ ์ƒ์„ฑ๊ธฐ (์‹ค์ œ ์—…์ฒดยทํ’ˆ๋ชฉยท๊ณ„์•ฝ ์ •๋ณด ์—†์Œ)\n", "# ๊ตฌ๋งค์š”๊ตฌ(๊ทœ๊ฒฉยท๊ธด๊ธ‰ ์—ฌ๋ถ€) -> ๊ณ„์•ฝ(๋‚ฉ๊ธฐ) -> ๋‚ฉํ’ˆ -> ์ˆ˜๋ น๊ฒ€์‚ฌ(ํ‘œ๋ณธ) -> ํ™•์ • ๋ถˆ๋Ÿ‰๋ฅ (์ „์ˆ˜๊ฒ€์‚ฌยท์‚ฌ์šฉ๋ถ€๋Œ€ ํ•˜์ž ๋ฐ˜์˜)\n", "# ---------------------------------------------------------------------------\n", "FSC = {\"5305\": \"screw/bolt\", \"5330\": \"gasket\", \"5935\": \"connector\", \"6135\": \"battery\", \"2530\": \"brake part\", \"8415\": \"clothing\"}\n", "VENDORS = [f\"V{i:02d}\" for i in range(1, 13)]\n", "VENDOR_Q = dict(zip(VENDORS, [-0.6, -0.4, -0.3, -0.2, -0.1, 0.0, 0.1, 0.2, 0.3, 0.5, 0.8, 1.1])) # ์—…์ฒด ๊ณ ์œ  ํ’ˆ์งˆ ์ˆ˜์ค€(์ˆจ์€ ๊ฐ’)\n", "FSC_Q = {\"5305\": -0.3, \"5330\": 0.0, \"5935\": 0.25, \"6135\": 0.1, \"2530\": 0.35, \"8415\": -0.2}\n", "SAMPLE_PLAN = [(500, 50, 3), (1200, 80, 5), (3200, 125, 7), (10 ** 9, 200, 10)] # (๋กœํŠธ ํฌ๊ธฐ ์ƒํ•œ, ํ‘œ๋ณธ n, ํ•ฉ๊ฒฉํŒ์ •์ˆ˜ c) ๋‹จ์ˆœํ™” ๊ธฐ์ค€\n", "BAD_RATE = 0.05 # ํ™•์ • ๋ถˆ๋Ÿ‰๋ฅ  5% ์ดˆ๊ณผ ๋กœํŠธ = ๋ถˆํ•ฉ๊ฒฉ ๋Œ€์ƒ\n", "\n", "\n", "def sample_plan(lot_size):\n", " for up, n, c in SAMPLE_PLAN:\n", " if lot_size <= up:\n", " return n, c\n", "\n", "\n", "def true_rate(d, rng=None):\n", " \"\"\"์ˆจ์€ ๋ถˆ๋Ÿ‰ ๋ฐœ์ƒ ๊ตฌ์กฐ: ์—…์ฒด ์ˆ˜์ค€ + ํ’ˆ๋ชฉ๊ตฐ + ์ •๋ฐ€ ๊ทœ๊ฒฉ + ๊ธด๊ธ‰ ๊ตฌ๋งค + ๊ทœ๊ฒฉ ๋ณ€๊ฒฝยท๋ถˆ์ผ์น˜ + ๋‚ฉ๊ธฐ ์ง€์—ฐ + (์ •๋ฐ€ ๊ทœ๊ฒฉ x ์ด‰๋ฐ• ๋‚ฉ๊ธฐ) ์ƒํ˜ธ์ž‘์šฉ.\"\"\"\n", " short = (d[\"lead_days\"] < 45).astype(float)\n", " z = (-4.35 + d[\"vendor\"].map(VENDOR_Q) + d[\"fsc\"].map(FSC_Q) + 0.55 * (d[\"spec_class\"] == \"precision\")\n", " + 0.35 * d[\"pr_urgent\"] + 0.6 * d[\"spec_changed\"] + 0.8 * d[\"spec_mismatch\"]\n", " + 0.9 * np.log1p(np.maximum(d[\"delay_days\"], 0) / 7) + 0.9 * short * (d[\"spec_class\"] == \"precision\")\n", " + 0.5 * (d[\"month\"] >= 18) * (d[\"vendor\"] == \"V10\")) # V10 ํ›„๋ฐ˜๊ธฐ ํ’ˆ์งˆ ์ €ํ•˜(๋“œ๋ฆฌํ”„ํŠธ)\n", " if rng is not None:\n", " z = z + rng.normal(0, 0.12, len(d))\n", " return np.clip(np.exp(z), 0.002, 0.35)\n", "\n", "\n", "def make_lots(n=1200, seed=42, months=24):\n", " rng = np.random.default_rng(seed)\n", " d = pd.DataFrame({\"lot_id\": [f\"LOT{seed % 100:02d}-{i:05d}\" for i in range(n)]})\n", " d[\"month\"] = rng.integers(0, months, n)\n", " d[\"receipt_date\"] = pd.Timestamp(\"2024-01-01\") + pd.to_timedelta(d[\"month\"] * 30 + rng.integers(0, 30, n), unit=\"D\")\n", " d = d.sort_values(\"receipt_date\").reset_index(drop=True)\n", " d[\"month\"] = ((d[\"receipt_date\"] - pd.Timestamp(\"2024-01-01\")).dt.days // 30).clip(0, months - 1)\n", " d[\"vendor\"] = rng.choice(VENDORS, n, p=np.r_[np.full(6, 0.1), np.full(6, 0.4 / 6)])\n", " d[\"fsc\"] = rng.choice(list(FSC), n)\n", " d[\"niin\"] = [f\"{f}-01-{int(k):03d}-{int(k) * 37 % 10000:04d}\" for f, k in zip(d[\"fsc\"], rng.integers(100, 110, n))]\n", " d[\"spec_class\"] = np.where(rng.random(n) < np.where(d[\"fsc\"].isin([\"5935\", \"2530\"]), 0.6, 0.25), \"precision\", \"general\")\n", " d[\"tolerance_mm\"] = np.where(d[\"spec_class\"] == \"precision\", rng.uniform(0.05, 0.1, n), rng.uniform(0.2, 0.5, n)).round(3)\n", " d[\"spec_rev\"] = rng.integers(1, 6, n)\n", " d[\"pr_urgent\"] = (rng.random(n) < 0.2).astype(int)\n", " d[\"spec_changed\"] = (rng.random(n) < 0.12).astype(int)\n", " d[\"spec_mismatch\"] = (rng.random(n) < 0.08).astype(int)\n", " d[\"pr_qty\"] = rng.integers(200, 5000, n)\n", " d[\"lot_size\"] = (d[\"pr_qty\"] * rng.uniform(0.5, 1.0, n)).astype(int)\n", " d[\"lead_days\"] = np.where(d[\"pr_urgent\"] == 1, rng.integers(20, 60, n), rng.integers(40, 150, n))\n", " d[\"delay_days\"] = np.round(rng.gamma(1.2, 6, n) * rng.binomial(1, 0.45, n) - rng.integers(0, 5, n)).astype(int)\n", " d[\"unit_cost\"] = rng.choice([2_000, 15_000, 40_000, 120_000], n, p=[0.35, 0.3, 0.25, 0.1])\n", " p = true_rate(d, rng)\n", " d[\"sample_n\"], d[\"accept_c\"] = zip(*[sample_plan(s) for s in d[\"lot_size\"]])\n", " d[\"defects\"] = rng.binomial(d[\"sample_n\"], p)\n", " share = np.where(d[\"spec_class\"] == \"precision\", 0.55, 0.3)\n", " d[\"def_dim\"] = rng.binomial(d[\"defects\"], share)\n", " d[\"def_spec\"] = rng.binomial(d[\"defects\"] - d[\"def_dim\"], 0.6)\n", " d[\"def_visual\"] = d[\"defects\"] - d[\"def_dim\"] - d[\"def_spec\"]\n", " d[\"dim_dev_mm\"] = (d[\"tolerance_mm\"] * (0.3 + 2.0 * p) * rng.lognormal(0, 0.3, n)).round(4)\n", " d[\"confirmed_rate\"] = np.clip(p * rng.lognormal(0, 0.05, n), 0.001, 0.4).round(4) # ์ „์ˆ˜๊ฒ€์‚ฌยทํ•˜์ž ๋ฐ˜์˜ ํ™•์ • ๋ถˆ๋Ÿ‰๋ฅ \n", " d[\"field_claims\"] = rng.poisson(d[\"lot_size\"] * p * 0.03)\n", " d[\"inspector\"] = rng.choice([\"INS-1\", \"INS-2\", \"INS-3\", \"INS-4\"], n)\n", " d[\"emergency_release\"] = ((d[\"pr_urgent\"] == 1) & (rng.random(n) < 0.3)).astype(int) # ๊ธด๊ธ‰ ๋ถˆ์ถœ(๊ฒ€์‚ฌ ์ „ ์„ ๋ถˆ์ถœ) ์˜ˆ์™ธ\n", " d[\"bad_lot\"] = (d[\"confirmed_rate\"] > BAD_RATE).astype(int)\n", " return d\n", "# ---------------------------------------------------------------------------\n", "\n", "# ---- ํŒŒ์ƒ ํ”ผ์ฒ˜ ๊ณตํ†ต ํ•จ์ˆ˜ (๋ฏธ์…˜ 4 ์ดํ›„ ๊ณตํ†ต) ----\n", "def add_history(d):\n", " \"\"\"์—…์ฒด๋ณ„ ๊ณผ๊ฑฐ ํ™•์ • ๋ถˆ๋Ÿ‰๋ฅ (ํ˜„์žฌ ๋กœํŠธ ์ด์ „ ๋กœํŠธ๋งŒ ์‚ฌ์šฉ: ์ •๋ณด ๋ˆ„์ˆ˜ ๋ฐฉ์ง€)๊ณผ ์ตœ๊ทผ 5๋กœํŠธ ์ถ”์„ธ.\"\"\"\n", " d = d.sort_values(\"receipt_date\").reset_index(drop=True)\n", " g = d.groupby(\"vendor\")[\"confirmed_rate\"]\n", " d[\"vendor_hist_rate\"] = g.transform(lambda s: s.shift().expanding().mean()).fillna(d[\"confirmed_rate\"].mean())\n", " d[\"vendor_recent_rate\"] = g.transform(lambda s: s.shift().rolling(5, min_periods=1).mean()).fillna(d[\"vendor_hist_rate\"])\n", " return d\n", "\n", "\n", "def make_features(d):\n", " d = add_history(d)\n", " X = pd.DataFrame(index=d.index)\n", " X[\"obs_rate\"] = np.log((d[\"defects\"] + 0.5) / d[\"sample_n\"]) # 1 ํ‘œ๋ณธ ๋ถˆ๋Ÿ‰๋ฅ \n", " X[\"log_lot\"] = np.log(d[\"lot_size\"]) # 2 ๋กœํŠธ ๊ทœ๋ชจ(๋กœ๊ทธ)\n", " X[\"delay_pos\"] = d[\"delay_days\"].clip(lower=0) # 3 ๋‚ฉ๊ธฐ ์ง€์—ฐ์ผ\n", " X[\"delay_ratio\"] = X[\"delay_pos\"] / d[\"lead_days\"] # 4 ์ง€์—ฐ/๊ณ„์•ฝ๊ธฐ๊ฐ„ ๋น„\n", " X[\"short_lead\"] = (d[\"lead_days\"] < 45).astype(int) # 5 ์ด‰๋ฐ• ๋‚ฉ๊ธฐ\n", " X[\"precision\"] = (d[\"spec_class\"] == \"precision\").astype(int) # 6 ์ •๋ฐ€ ๊ทœ๊ฒฉ\n", " X[\"prec_x_short\"] = X[\"precision\"] * X[\"short_lead\"] # 7 ์ •๋ฐ€ x ์ด‰๋ฐ•(์ƒํ˜ธ์ž‘์šฉ)\n", " X[\"spec_risk\"] = d[\"spec_changed\"] + d[\"spec_mismatch\"] # 8 ๊ทœ๊ฒฉ ๋ณ€๊ฒฝยท๋ถˆ์ผ์น˜ ํ•ฉ\n", " X[\"dim_ratio\"] = np.log(d[\"dim_dev_mm\"] / d[\"tolerance_mm\"]) # 9 ์น˜์ˆ˜ ํŽธ์ฐจ/๊ณต์ฐจ\n", " X[\"vendor_hist\"] = np.log(d[\"vendor_hist_rate\"]) # 10 ์—…์ฒด ๊ณผ๊ฑฐ ๋ถˆ๋Ÿ‰๋ฅ \n", " X[\"vendor_trend\"] = d[\"vendor_recent_rate\"] - d[\"vendor_hist_rate\"] # 11 ์—…์ฒด ์ตœ๊ทผ ์ถ”์„ธ\n", " X[\"urgent\"] = d[\"pr_urgent\"] # 12 ๊ธด๊ธ‰ ๊ตฌ๋งค์š”๊ตฌ\n", " X[\"fill_ratio\"] = d[\"lot_size\"] / d[\"pr_qty\"] # 13 ๊ตฌ๋งค์š”๊ตฌ ๋Œ€๋น„ ๋‚ฉํ’ˆ ๋น„์œจ\n", " X[\"dim_share\"] = d[\"def_dim\"] / d[\"defects\"].clip(lower=1) # 14 ์น˜์ˆ˜ ์˜ค์ฐจ ๋น„์ค‘\n", " for f in FSC: # 15~ ํ’ˆ๋ชฉ๊ตฐ ์›-ํ•ซ\n", " X[f\"fsc_{f}\"] = (d[\"fsc\"] == f).astype(int)\n", " return d, X\n", "\n", "\n", "def mape(y, p):\n", " return float(np.mean(np.abs(y - p) / y))\n", "\n", "\n", "d, X = make_features(make_lots(1200, seed=42))\n", "y = np.log(d[\"confirmed_rate\"].values) # ๋ถˆ๋Ÿ‰๋ฅ ์€ ์น˜์šฐ์นœ ๋ถ„ํฌ๋ผ ๋กœ๊ทธ๋กœ ํ•™์Šต\n", "print(f\"ํŒŒ์ƒ ํ”ผ์ฒ˜ {X.shape[1]}๊ฐœ ์ƒ์„ฑ (๋ชฉํ‘œ 10๊ฐœ ์ด์ƒ: {'๋‹ฌ์„ฑ' if X.shape[1] >= 10 else '๋ฏธ๋‹ฌ'})\")\n", "print(\"ํ”ผ์ฒ˜:\", \", \".join(X.columns))\n", "\n", "# 1) ์‹œ๊ณ„์—ด ๊ต์ฐจ๊ฒ€์ฆ(๊ณผ๊ฑฐ๋กœ ํ•™์Šต -> ์ดํ›„ ๊ธฐ๊ฐ„ ๊ฒ€์ฆ)\n", "model = make_pipeline(StandardScaler(), Ridge(alpha=1.0))\n", "rows = []\n", "for k, (tr, te) in enumerate(TimeSeriesSplit(n_splits=4).split(X), 1):\n", " model.fit(X.iloc[tr], y[tr])\n", " rows.append({\"ํด๋“œ\": k, \"ํ•™์Šต ๋กœํŠธ\": len(tr), \"๊ฒ€์ฆ ๋กœํŠธ\": len(te),\n", " \"ํ•™์Šต MAPE\": round(mape(np.exp(y[tr]), np.exp(model.predict(X.iloc[tr]))), 3),\n", " \"๊ฒ€์ฆ MAPE\": round(mape(np.exp(y[te]), np.exp(model.predict(X.iloc[te]))), 3)})\n", "cv = pd.DataFrame(rows)\n", "print(\"\\n[์‹œ๊ณ„์—ด ๊ต์ฐจ๊ฒ€์ฆ]\")\n", "print(cv.to_string(index=False))\n", "m = cv[\"๊ฒ€์ฆ MAPE\"].mean()\n", "print(f\"๊ธฐ์ค€ ๋ชจ๋ธ ํ‰๊ท  ๊ฒ€์ฆ MAPE {m:.1%} (๋ชฉํ‘œ 25% ์ดํ•˜: {'๋‹ฌ์„ฑ' if m <= 0.25 else '๋ฏธ๋‹ฌ'}), ํ•™์Šต-๊ฒ€์ฆ ์ฐจ์ด {(cv['๊ฒ€์ฆ MAPE'] - cv['ํ•™์Šต MAPE']).mean():+.1%}p\")\n", "\n", "# 2) ํ”ผ์ฒ˜ ์ค‘์š”๋„(์ˆœ์—ด ์ค‘์š”๋„)๋กœ ์ฐจ์› ์ถ•์†Œ ํ›„๋ณด ํ™•์ธ\n", "tr = d[\"month\"] < 18\n", "model.fit(X[tr], y[tr])\n", "imp = permutation_importance(model, X[~tr], y[~tr], n_repeats=5, random_state=0)\n", "fi = pd.Series(imp.importances_mean, index=X.columns).sort_values(ascending=False)\n", "print(\"\\n[์ˆœ์—ด ์ค‘์š”๋„ ์ƒ์œ„ 8๊ฐœ]\")\n", "print(fi.head(8).round(4).to_string())\n", "low = list(fi[fi < 0.002].index)\n", "Xr = X.drop(columns=low)\n", "model.fit(Xr[tr], y[tr])\n", "print(f\"์ค‘์š”๋„ 0.002 ๋ฏธ๋งŒ {len(low)}๊ฐœ ์ œ๊ฑฐ ํ›„ ๊ฒ€์ฆ MAPE {mape(np.exp(y[~tr]), np.exp(model.predict(Xr[~tr]))):.1%} (ํ”ผ์ฒ˜ {Xr.shape[1]}๊ฐœ)\")\n", "\n", "# 3) ์€๋‹‰ ๊ฒฐํ•จ ํŒจํ„ด: ๋‹จ์ผ ์š”์ธ์œผ๋กœ๋Š” ์•ฝํ•˜์ง€๋งŒ ์กฐํ•ฉ๋  ๋•Œ ์ปค์ง€๋Š” ๊ตฌ๊ฐ„\n", "piv = d.assign(๊ทœ๊ฒฉ=np.where(X[\"precision\"] == 1, \"์ •๋ฐ€\", \"์ผ๋ฐ˜\"), ๋‚ฉ๊ธฐ=np.where(X[\"short_lead\"] == 1, \"์ด‰๋ฐ•(<45์ผ)\", \"์—ฌ์œ \")).pivot_table(\n", " index=\"๊ทœ๊ฒฉ\", columns=\"๋‚ฉ๊ธฐ\", values=\"confirmed_rate\", aggfunc=\"mean\")\n", "print(\"\\n[์€๋‹‰ ํŒจํ„ด 1] ๊ทœ๊ฒฉ x ๋‚ฉ๊ธฐ ํ‰๊ท  ํ™•์ • ๋ถˆ๋Ÿ‰๋ฅ \")\n", "print((piv * 100).round(1).astype(str).add(\"%\").to_string())\n", "lift = piv.loc[\"์ •๋ฐ€\", \"์ด‰๋ฐ•(<45์ผ)\"] / piv.loc[\"์ผ๋ฐ˜\", \"์—ฌ์œ \"]\n", "print(f\" -> ์ •๋ฐ€ ๊ทœ๊ฒฉ์„ ์ด‰๋ฐ•ํ•œ ๋‚ฉ๊ธฐ๋กœ ๊ณ„์•ฝํ•˜๋ฉด ๋ถˆ๋Ÿ‰๋ฅ ์ด {lift:.1f}๋ฐฐ: ๊ตฌ๋งค์š”๊ตฌ ๋‹จ๊ณ„์—์„œ ๋‚ฉ๊ธฐ ์—ฌ์œ ๋ฅผ ํ™•๋ณดํ•ด์•ผ ํ•œ๋‹ค.\")\n", "drift = d[d[\"vendor\"] == \"V10\"].groupby(d[\"month\"] >= 18)[\"confirmed_rate\"].mean()\n", "print(f\"[์€๋‹‰ ํŒจํ„ด 2] V10 ์—…์ฒด ๋ถˆ๋Ÿ‰๋ฅ  ์ „๋ฐ˜๊ธฐ {drift[False]:.1%} -> ํ›„๋ฐ˜๊ธฐ {drift[True]:.1%} (์ตœ๊ทผ ์ถ”์„ธ ํ”ผ์ฒ˜ vendor_trend๋กœ ํฌ์ฐฉ)\")\n", "no_int = make_pipeline(StandardScaler(), Ridge(1.0)).fit(X.drop(columns=[\"prec_x_short\", \"vendor_trend\"])[tr], y[tr])\n", "print(f\"์ƒํ˜ธ์ž‘์šฉยท์ถ”์„ธ ํ”ผ์ฒ˜ ์ œ์™ธ ์‹œ ๊ฒ€์ฆ MAPE {mape(np.exp(y[~tr]), np.exp(no_int.predict(X.drop(columns=['prec_x_short', 'vendor_trend'])[~tr]))):.1%} -> ํฌํ•จ ์‹œ {mape(np.exp(y[~tr]), np.exp(make_pipeline(StandardScaler(), Ridge(1.0)).fit(X[tr], y[tr]).predict(X[~tr]))):.1%}\")\n", "\n", "fig, ax = plt.subplots(1, 2, figsize=(12, 4))\n", "fi.head(10)[::-1].plot.barh(ax=ax[0], color=\"tab:blue\"); ax[0].set_title(\"Permutation importance (Ridge baseline)\")\n", "p = np.exp(make_pipeline(StandardScaler(), Ridge(1.0)).fit(X[tr], y[tr]).predict(X[~tr]))\n", "ax[1].scatter(np.exp(y[~tr]), p, s=8, alpha=0.6); ax[1].plot([0, 0.4], [0, 0.4], \"k--\")\n", "ax[1].set_xlabel(\"confirmed defect rate\"); ax[1].set_ylabel(\"predicted\"); ax[1].set_title(\"Baseline prediction (hold-out months 18-23)\")\n", "plt.tight_layout(); plt.show()\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "
\n", "๐Ÿ’ก [์ฐธ๊ณ ] ๋ฏธ์…˜ 04 ์˜ˆ์ƒ ์‹คํ–‰ ๊ฒฐ๊ณผ (Output)\n", "\n", "```text\n", "ํŒŒ์ƒ ํ”ผ์ฒ˜ 20๊ฐœ ์ƒ์„ฑ (๋ชฉํ‘œ 10๊ฐœ ์ด์ƒ: ๋‹ฌ์„ฑ)\n", "ํ”ผ์ฒ˜: obs_rate, log_lot, delay_pos, delay_ratio, short_lead, precision, prec_x_short, spec_risk, dim_ratio, vendor_hist, vendor_trend, urgent, fill_ratio, dim_share, fsc_5305, fsc_5330, fsc_5935, fsc_6135, fsc_2530, fsc_8415\n", "\n", "[์‹œ๊ณ„์—ด ๊ต์ฐจ๊ฒ€์ฆ]\n", " ํด๋“œ ํ•™์Šต ๋กœํŠธ ๊ฒ€์ฆ ๋กœํŠธ ํ•™์Šต MAPE ๊ฒ€์ฆ MAPE\n", " 1 240 240 0.211 0.204\n", " 2 480 240 0.197 0.184\n", " 3 720 240 0.191 0.167\n", " 4 960 240 0.182 0.199\n", "๊ธฐ์ค€ ๋ชจ๋ธ ํ‰๊ท  ๊ฒ€์ฆ MAPE 18.9% (๋ชฉํ‘œ 25% ์ดํ•˜: ๋‹ฌ์„ฑ), ํ•™์Šต-๊ฒ€์ฆ ์ฐจ์ด -0.7%p\n", "\n", "[์ˆœ์—ด ์ค‘์š”๋„ ์ƒ์œ„ 8๊ฐœ]\n", "vendor_hist 0.3258\n", "delay_pos 0.2496\n", "spec_risk 0.1612\n", "precision 0.1299\n", "prec_x_short 0.0878\n", "obs_rate 0.0634\n", "urgent 0.0443\n", "fsc_5305 0.0283\n", "์ค‘์š”๋„ 0.002 ๋ฏธ๋งŒ 7๊ฐœ ์ œ๊ฑฐ ํ›„ ๊ฒ€์ฆ MAPE 20.1% (ํ”ผ์ฒ˜ 13๊ฐœ)\n", "\n", "[์€๋‹‰ ํŒจํ„ด 1] ๊ทœ๊ฒฉ x ๋‚ฉ๊ธฐ ํ‰๊ท  ํ™•์ • ๋ถˆ๋Ÿ‰๋ฅ \n", "๋‚ฉ๊ธฐ ์—ฌ์œ  ์ด‰๋ฐ•(<45์ผ)\n", "๊ทœ๊ฒฉ \n", "์ผ๋ฐ˜ 2.7% 3.5%\n", "์ •๋ฐ€ 5.0% 15.0%\n", " -> ์ •๋ฐ€ ๊ทœ๊ฒฉ์„ ์ด‰๋ฐ•ํ•œ ๋‚ฉ๊ธฐ๋กœ ๊ณ„์•ฝํ•˜๋ฉด ๋ถˆ๋Ÿ‰๋ฅ ์ด 5.7๋ฐฐ: ๊ตฌ๋งค์š”๊ตฌ ๋‹จ๊ณ„์—์„œ ๋‚ฉ๊ธฐ ์—ฌ์œ ๋ฅผ ํ™•๋ณดํ•ด์•ผ ํ•œ๋‹ค.\n", "[์€๋‹‰ ํŒจํ„ด 2] V10 ์—…์ฒด ๋ถˆ๋Ÿ‰๋ฅ  ์ „๋ฐ˜๊ธฐ 6.3% -> ํ›„๋ฐ˜๊ธฐ 11.1% (์ตœ๊ทผ ์ถ”์„ธ ํ”ผ์ฒ˜ vendor_trend๋กœ ํฌ์ฐฉ)\n", "์ƒํ˜ธ์ž‘์šฉยท์ถ”์„ธ ํ”ผ์ฒ˜ ์ œ์™ธ ์‹œ ๊ฒ€์ฆ MAPE 23.0% -> ํฌํ•จ ์‹œ 20.0%\n", "```\n", "
\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "---\n", "\n", "## ๐ŸŽฏ ๋ฏธ์…˜ 05. ๋กœํŠธ๋ณ„ ํ’ˆ์งˆ ์œ„ํ—˜ ์ง€์ˆ˜(Quality Risk Score) ์‚ฐ์ถœ ๋ฐ ์กฐ๊ธฐ๊ฒฝ๋ณด ์—”์ง„ ๊ฐœ๋ฐœ\n", "**์†Œ์š”์‹œ๊ฐ„**: ์•ฝ 3.5์‹œ๊ฐ„\n", "\n", "### ๐Ÿ“‹ ๋ฏธ์…˜ ๋ฐฐ๊ฒฝ ๋ฐ ์ˆ˜ํ–‰ ์ง€์นจ\n", "#### ๋ฏธ์…˜ ๋ฐฐ๊ฒฝ ๋ฐ ๋‹น์œ„์„ฑ\n", "\n", "๋กœํŠธ๋ณ„ ํ’ˆ์งˆ ์œ„ํ—˜ ์ง€์ˆ˜ ์‚ฐ์ถœ ๋ฐ ์กฐ๊ธฐ๊ฒฝ๋ณด ์—”์ง„์„ ๊ฐœ๋ฐœํ•œ๋‹ค. ์‹ค์ œ ์ „์ˆ  ํ˜„์žฅ์—์„œ๋Š” ๋‹จ์ˆœ ํƒ์ง€์— ๊ทธ์น˜์ง€ ์•Š๊ณ  ์‹ ์†ํ•œ ์œ„ํ˜‘ ํŒ๋‹จ๊ณผ ์‹ ๋ขฐ๋„ ๋†’์€ ์˜์‚ฌ๊ฒฐ์ •์ด ํ•„์ˆ˜์ ์ด๋ฏ€๋กœ, **์•™์ƒ๋ธ” ๋ชจ๋ธ๋กœ ๋กœํŠธ๋ณ„ ํ™•์ • ๋ถˆ๋Ÿ‰๋ฅ ์„ ์˜ˆ์ธกํ•ด ํ’ˆ์งˆ ์œ„ํ—˜ ์ง€์ˆ˜(0~100)์™€ ๊ฒฝ๋ณด ๋“ฑ๊ธ‰์„ ์‚ฐ์ถœํ•˜๋Š” ์กฐ๊ธฐ๊ฒฝ๋ณด ์—”์ง„์„ ๊ฐœ๋ฐœํ•œ๋‹ค.** ๊ณผ์—…์„ ์™„์ˆ˜ํ•˜์—ฌ ์˜คํŒ์„ ๋ฐฉ์ง€ํ•˜๊ณ  ์ž‘์ „ ๋Œ€์‘๋ ฅ์„ ๊ทน๋Œ€ํ™”ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.\n", "\n", "์ด ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด ๋ณธ ๋ฏธ์…˜์—์„œ๋Š” **๊ทธ๋ž˜๋””์–ธํŠธ ๋ถ€์ŠคํŒ…๊ณผ ๋žœ๋ค ํฌ๋ ˆ์ŠคํŠธ๋ฅผ ๋ฌถ์€ ์•™์ƒ๋ธ”์„ ์•ž 18๊ฐœ์›”๋กœ ํ•™์Šตํ•˜๊ณ  ์ตœ๊ทผ 6๊ฐœ์›” hold-out์—์„œ ๊ธฐ์ค€ ๋ชจ๋ธ๊ณผ MAEยทRMSEยทR2๋ฅผ ๋น„๊ต ์ˆ˜ํ–‰ํ•˜๊ณ  ๊ฒ€์ฆํ•˜๋Š”** ์‹ค์Šต์„ ์ฒด๊ณ„์ ์œผ๋กœ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค.\n", "\n", "ํ•™์Šต์ž๋Š” ์‹ค์Šต๊ฐ€์ด๋“œ์— ์ œ์‹œ๋œ ์ด 4๊ฐœ ์„ธ๋ถ€ ํ™œ๋™(๊ธฐ๋ณธ ๊ธฐ๋Šฅ ๊ตฌํ˜„ โž” ๊ฒฐ๊ณผ ์ง€ํ‘œ ๊ฒ€์ฆ โž” ํŒŒ๋ผ๋ฏธํ„ฐ ํŠœ๋‹ ๋ฐ ๋„์ „๊ณผ์ œ)์„ ์ˆœ์ฐจ์ ์œผ๋กœ ์ง„ํ–‰ํ•˜๋ฉฐ ํ•ต์‹ฌ ํŒ์ • ๊ธฐ์ค€์„ ๋‹ฌ์„ฑํ•ฉ๋‹ˆ๋‹ค.\n", "\n", "**์ด๋ฒˆ ๊ฒฐ๊ณผ๋ฌผ:** `src/models/qlt02_advanced_model.py` ํ˜•ํƒœ์˜ ๊ฒฐ๊ณผ๋ฌผ์„ ์™„์„ฑํ•œ๋‹ค.\n", "\n", "### ๐Ÿ” ํ‰๊ฐ€ ๋ฐ ๊ฒ€์ฆ ๊ธฐ์ค€ (PASS ์กฐ๊ฑด)\n", "#### ๋ฏธ์…˜ ํŒ์ •\n", "\n", "**ํ‰๊ฐ€ ๊ตฌ๋ถ„:** ํ•„์ˆ˜\n", "\n", "**ํ•ต์‹ฌ ํ‰๊ฐ€์˜์—ญ:** ๋น„์ •ํ˜•/๋ถˆ๊ทœ์น™ ์ˆ˜์š”์— ๋Œ€ํ•œ ๋ชจ๋ธ์˜ ์ ์‘๋ ฅ ๋ฐ ์˜ˆ์ธก ์•ˆ์ •์„ฑ\n", "\n", "#### ๋ฏธ์…˜ PASS ์กฐ๊ฑด\n", "\n", "- ์ •๋Ÿ‰ ๊ธฐ์ค€: ๊ธฐ์ค€ ๋ชจ๋ธ ๋Œ€๋น„ ์˜ค์ฐจ์œจ(MAE/RMSE) 20% ์ด์ƒ ๊ฐœ์„ , R2 Score 0.85 ์ด์ƒ\n", "\n", "\n", "- ์ง„ํ–‰ ๊ธฐ์ค€: ๋ชจ๋ธ ํ•˜์ดํผํŒŒ๋ผ๋ฏธํ„ฐ ํŠœ๋‹ ์™„๋ฃŒ ๋ฐ ์ตœ์  ๋ชจ๋ธ ๊ฐ€์ค‘์น˜ ์ €์žฅ\n", "\n", "\n", "- ๊ฒ€์ฆ ๋ฐฉ๋ฒ•: Hold-out ํ…Œ์ŠคํŠธ ๋ฐ์ดํ„ฐ์…‹ ๋Œ€์ƒ ์ตœ์ข… ์„ฑ๋Šฅ ํ‰๊ฐ€\n", "\n", "\n", "#### ์ฑ„์ ์ž ํŒ์ • ๋ฉ”๋ชจ\n", "\n", "> ๋‹จ์ˆœ ์ฝ”๋“œ ์‹คํ–‰ ์—ฌ๋ถ€๋ฟ๋งŒ ์•„๋‹ˆ๋ผ ์ถœ๋ ฅ๋œ ์ˆ˜์น˜์™€ ๊ทผ๊ฑฐ ์ž๋ฃŒ๊ฐ€ ๊ตฐ ์‹ค๋ฌด ์š”๊ตฌ์‚ฌํ•ญ์— ๋ถ€ํ•ฉํ•˜๋Š”์ง€ ๋ฉด๋ฐ€ํžˆ ํ™•์ธํ•œ๋‹ค.\n", "\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "\"\"\"๊ทธ๋ž˜๋””์–ธํŠธ ๋ถ€์ŠคํŒ…๊ณผ ๋žœ๋ค ํฌ๋ ˆ์ŠคํŠธ ์•™์ƒ๋ธ”๋กœ ๋กœํŠธ ํ™•์ • ๋ถˆ๋Ÿ‰๋ฅ ์„ ์˜ˆ์ธกํ•ด ๊ธฐ์ค€ ๋ชจ๋ธ(Ridge) ๋Œ€๋น„ ์˜ค์ฐจ ๊ฐœ์„ ๊ณผ R2๋ฅผ ๊ฒ€์ฆํ•˜๊ณ , ์˜ˆ์ธก ๋ถˆ๋Ÿ‰๋ฅ ์„ 'ํ—ˆ์šฉ ๋ถˆ๋Ÿ‰๋ฅ  ์ดˆ๊ณผ ํ™•๋ฅ ' ๊ธฐ๋ฐ˜ ํ’ˆ์งˆ ์œ„ํ—˜ ์ง€์ˆ˜(0~100)์™€ ๊ฒฝ๋ณด ๋“ฑ๊ธ‰์œผ๋กœ ๋ฐ”๊พธ๋Š” ์กฐ๊ธฐ๊ฒฝ๋ณด ์—”์ง„์„ ๋งŒ๋“ ๋‹ค.\"\"\"\n", "import numpy as np\n", "import pandas as pd\n", "import matplotlib.pyplot as plt\n", "from scipy import stats\n", "from sklearn.linear_model import Ridge\n", "from sklearn.pipeline import make_pipeline\n", "from sklearn.preprocessing import StandardScaler\n", "from sklearn.ensemble import GradientBoostingRegressor, RandomForestRegressor\n", "from sklearn.metrics import mean_absolute_error, mean_squared_error, r2_score\n", "\n", "# ---------------------------------------------------------------------------\n", "# [๊ต์œก์šฉ๊ฐ€์ƒ์ž๋ฃŒ] ๊ตฐ์ˆ˜ํ’ˆ ์ˆ˜๋ น๊ฒ€์‚ฌ ๋กœํŠธ ๋ฐ์ดํ„ฐ ์ƒ์„ฑ๊ธฐ (์‹ค์ œ ์—…์ฒดยทํ’ˆ๋ชฉยท๊ณ„์•ฝ ์ •๋ณด ์—†์Œ)\n", "# ๊ตฌ๋งค์š”๊ตฌ(๊ทœ๊ฒฉยท๊ธด๊ธ‰ ์—ฌ๋ถ€) -> ๊ณ„์•ฝ(๋‚ฉ๊ธฐ) -> ๋‚ฉํ’ˆ -> ์ˆ˜๋ น๊ฒ€์‚ฌ(ํ‘œ๋ณธ) -> ํ™•์ • ๋ถˆ๋Ÿ‰๋ฅ (์ „์ˆ˜๊ฒ€์‚ฌยท์‚ฌ์šฉ๋ถ€๋Œ€ ํ•˜์ž ๋ฐ˜์˜)\n", "# ---------------------------------------------------------------------------\n", "FSC = {\"5305\": \"screw/bolt\", \"5330\": \"gasket\", \"5935\": \"connector\", \"6135\": \"battery\", \"2530\": \"brake part\", \"8415\": \"clothing\"}\n", "VENDORS = [f\"V{i:02d}\" for i in range(1, 13)]\n", "VENDOR_Q = dict(zip(VENDORS, [-0.6, -0.4, -0.3, -0.2, -0.1, 0.0, 0.1, 0.2, 0.3, 0.5, 0.8, 1.1])) # ์—…์ฒด ๊ณ ์œ  ํ’ˆ์งˆ ์ˆ˜์ค€(์ˆจ์€ ๊ฐ’)\n", "FSC_Q = {\"5305\": -0.3, \"5330\": 0.0, \"5935\": 0.25, \"6135\": 0.1, \"2530\": 0.35, \"8415\": -0.2}\n", "SAMPLE_PLAN = [(500, 50, 3), (1200, 80, 5), (3200, 125, 7), (10 ** 9, 200, 10)] # (๋กœํŠธ ํฌ๊ธฐ ์ƒํ•œ, ํ‘œ๋ณธ n, ํ•ฉ๊ฒฉํŒ์ •์ˆ˜ c) ๋‹จ์ˆœํ™” ๊ธฐ์ค€\n", "BAD_RATE = 0.05 # ํ™•์ • ๋ถˆ๋Ÿ‰๋ฅ  5% ์ดˆ๊ณผ ๋กœํŠธ = ๋ถˆํ•ฉ๊ฒฉ ๋Œ€์ƒ\n", "\n", "\n", "def sample_plan(lot_size):\n", " for up, n, c in SAMPLE_PLAN:\n", " if lot_size <= up:\n", " return n, c\n", "\n", "\n", "def true_rate(d, rng=None):\n", " \"\"\"์ˆจ์€ ๋ถˆ๋Ÿ‰ ๋ฐœ์ƒ ๊ตฌ์กฐ: ์—…์ฒด ์ˆ˜์ค€ + ํ’ˆ๋ชฉ๊ตฐ + ์ •๋ฐ€ ๊ทœ๊ฒฉ + ๊ธด๊ธ‰ ๊ตฌ๋งค + ๊ทœ๊ฒฉ ๋ณ€๊ฒฝยท๋ถˆ์ผ์น˜ + ๋‚ฉ๊ธฐ ์ง€์—ฐ + (์ •๋ฐ€ ๊ทœ๊ฒฉ x ์ด‰๋ฐ• ๋‚ฉ๊ธฐ) ์ƒํ˜ธ์ž‘์šฉ.\"\"\"\n", " short = (d[\"lead_days\"] < 45).astype(float)\n", " z = (-4.35 + d[\"vendor\"].map(VENDOR_Q) + d[\"fsc\"].map(FSC_Q) + 0.55 * (d[\"spec_class\"] == \"precision\")\n", " + 0.35 * d[\"pr_urgent\"] + 0.6 * d[\"spec_changed\"] + 0.8 * d[\"spec_mismatch\"]\n", " + 0.9 * np.log1p(np.maximum(d[\"delay_days\"], 0) / 7) + 0.9 * short * (d[\"spec_class\"] == \"precision\")\n", " + 0.5 * (d[\"month\"] >= 18) * (d[\"vendor\"] == \"V10\")) # V10 ํ›„๋ฐ˜๊ธฐ ํ’ˆ์งˆ ์ €ํ•˜(๋“œ๋ฆฌํ”„ํŠธ)\n", " if rng is not None:\n", " z = z + rng.normal(0, 0.12, len(d))\n", " return np.clip(np.exp(z), 0.002, 0.35)\n", "\n", "\n", "def make_lots(n=1200, seed=42, months=24):\n", " rng = np.random.default_rng(seed)\n", " d = pd.DataFrame({\"lot_id\": [f\"LOT{seed % 100:02d}-{i:05d}\" for i in range(n)]})\n", " d[\"month\"] = rng.integers(0, months, n)\n", " d[\"receipt_date\"] = pd.Timestamp(\"2024-01-01\") + pd.to_timedelta(d[\"month\"] * 30 + rng.integers(0, 30, n), unit=\"D\")\n", " d = d.sort_values(\"receipt_date\").reset_index(drop=True)\n", " d[\"month\"] = ((d[\"receipt_date\"] - pd.Timestamp(\"2024-01-01\")).dt.days // 30).clip(0, months - 1)\n", " d[\"vendor\"] = rng.choice(VENDORS, n, p=np.r_[np.full(6, 0.1), np.full(6, 0.4 / 6)])\n", " d[\"fsc\"] = rng.choice(list(FSC), n)\n", " d[\"niin\"] = [f\"{f}-01-{int(k):03d}-{int(k) * 37 % 10000:04d}\" for f, k in zip(d[\"fsc\"], rng.integers(100, 110, n))]\n", " d[\"spec_class\"] = np.where(rng.random(n) < np.where(d[\"fsc\"].isin([\"5935\", \"2530\"]), 0.6, 0.25), \"precision\", \"general\")\n", " d[\"tolerance_mm\"] = np.where(d[\"spec_class\"] == \"precision\", rng.uniform(0.05, 0.1, n), rng.uniform(0.2, 0.5, n)).round(3)\n", " d[\"spec_rev\"] = rng.integers(1, 6, n)\n", " d[\"pr_urgent\"] = (rng.random(n) < 0.2).astype(int)\n", " d[\"spec_changed\"] = (rng.random(n) < 0.12).astype(int)\n", " d[\"spec_mismatch\"] = (rng.random(n) < 0.08).astype(int)\n", " d[\"pr_qty\"] = rng.integers(200, 5000, n)\n", " d[\"lot_size\"] = (d[\"pr_qty\"] * rng.uniform(0.5, 1.0, n)).astype(int)\n", " d[\"lead_days\"] = np.where(d[\"pr_urgent\"] == 1, rng.integers(20, 60, n), rng.integers(40, 150, n))\n", " d[\"delay_days\"] = np.round(rng.gamma(1.2, 6, n) * rng.binomial(1, 0.45, n) - rng.integers(0, 5, n)).astype(int)\n", " d[\"unit_cost\"] = rng.choice([2_000, 15_000, 40_000, 120_000], n, p=[0.35, 0.3, 0.25, 0.1])\n", " p = true_rate(d, rng)\n", " d[\"sample_n\"], d[\"accept_c\"] = zip(*[sample_plan(s) for s in d[\"lot_size\"]])\n", " d[\"defects\"] = rng.binomial(d[\"sample_n\"], p)\n", " share = np.where(d[\"spec_class\"] == \"precision\", 0.55, 0.3)\n", " d[\"def_dim\"] = rng.binomial(d[\"defects\"], share)\n", " d[\"def_spec\"] = rng.binomial(d[\"defects\"] - d[\"def_dim\"], 0.6)\n", " d[\"def_visual\"] = d[\"defects\"] - d[\"def_dim\"] - d[\"def_spec\"]\n", " d[\"dim_dev_mm\"] = (d[\"tolerance_mm\"] * (0.3 + 2.0 * p) * rng.lognormal(0, 0.3, n)).round(4)\n", " d[\"confirmed_rate\"] = np.clip(p * rng.lognormal(0, 0.05, n), 0.001, 0.4).round(4) # ์ „์ˆ˜๊ฒ€์‚ฌยทํ•˜์ž ๋ฐ˜์˜ ํ™•์ • ๋ถˆ๋Ÿ‰๋ฅ \n", " d[\"field_claims\"] = rng.poisson(d[\"lot_size\"] * p * 0.03)\n", " d[\"inspector\"] = rng.choice([\"INS-1\", \"INS-2\", \"INS-3\", \"INS-4\"], n)\n", " d[\"emergency_release\"] = ((d[\"pr_urgent\"] == 1) & (rng.random(n) < 0.3)).astype(int) # ๊ธด๊ธ‰ ๋ถˆ์ถœ(๊ฒ€์‚ฌ ์ „ ์„ ๋ถˆ์ถœ) ์˜ˆ์™ธ\n", " d[\"bad_lot\"] = (d[\"confirmed_rate\"] > BAD_RATE).astype(int)\n", " return d\n", "# ---------------------------------------------------------------------------\n", "\n", "# ---- ํŒŒ์ƒ ํ”ผ์ฒ˜ ๊ณตํ†ต ํ•จ์ˆ˜ (๋ฏธ์…˜ 4 ์ดํ›„ ๊ณตํ†ต) ----\n", "def add_history(d):\n", " \"\"\"์—…์ฒด๋ณ„ ๊ณผ๊ฑฐ ํ™•์ • ๋ถˆ๋Ÿ‰๋ฅ (ํ˜„์žฌ ๋กœํŠธ ์ด์ „ ๋กœํŠธ๋งŒ ์‚ฌ์šฉ: ์ •๋ณด ๋ˆ„์ˆ˜ ๋ฐฉ์ง€)๊ณผ ์ตœ๊ทผ 5๋กœํŠธ ์ถ”์„ธ.\"\"\"\n", " d = d.sort_values(\"receipt_date\").reset_index(drop=True)\n", " g = d.groupby(\"vendor\")[\"confirmed_rate\"]\n", " d[\"vendor_hist_rate\"] = g.transform(lambda s: s.shift().expanding().mean()).fillna(d[\"confirmed_rate\"].mean())\n", " d[\"vendor_recent_rate\"] = g.transform(lambda s: s.shift().rolling(5, min_periods=1).mean()).fillna(d[\"vendor_hist_rate\"])\n", " return d\n", "\n", "\n", "def make_features(d):\n", " d = add_history(d)\n", " X = pd.DataFrame(index=d.index)\n", " X[\"obs_rate\"] = np.log((d[\"defects\"] + 0.5) / d[\"sample_n\"]) # 1 ํ‘œ๋ณธ ๋ถˆ๋Ÿ‰๋ฅ \n", " X[\"log_lot\"] = np.log(d[\"lot_size\"]) # 2 ๋กœํŠธ ๊ทœ๋ชจ(๋กœ๊ทธ)\n", " X[\"delay_pos\"] = d[\"delay_days\"].clip(lower=0) # 3 ๋‚ฉ๊ธฐ ์ง€์—ฐ์ผ\n", " X[\"delay_ratio\"] = X[\"delay_pos\"] / d[\"lead_days\"] # 4 ์ง€์—ฐ/๊ณ„์•ฝ๊ธฐ๊ฐ„ ๋น„\n", " X[\"short_lead\"] = (d[\"lead_days\"] < 45).astype(int) # 5 ์ด‰๋ฐ• ๋‚ฉ๊ธฐ\n", " X[\"precision\"] = (d[\"spec_class\"] == \"precision\").astype(int) # 6 ์ •๋ฐ€ ๊ทœ๊ฒฉ\n", " X[\"prec_x_short\"] = X[\"precision\"] * X[\"short_lead\"] # 7 ์ •๋ฐ€ x ์ด‰๋ฐ•(์ƒํ˜ธ์ž‘์šฉ)\n", " X[\"spec_risk\"] = d[\"spec_changed\"] + d[\"spec_mismatch\"] # 8 ๊ทœ๊ฒฉ ๋ณ€๊ฒฝยท๋ถˆ์ผ์น˜ ํ•ฉ\n", " X[\"dim_ratio\"] = np.log(d[\"dim_dev_mm\"] / d[\"tolerance_mm\"]) # 9 ์น˜์ˆ˜ ํŽธ์ฐจ/๊ณต์ฐจ\n", " X[\"vendor_hist\"] = np.log(d[\"vendor_hist_rate\"]) # 10 ์—…์ฒด ๊ณผ๊ฑฐ ๋ถˆ๋Ÿ‰๋ฅ \n", " X[\"vendor_trend\"] = d[\"vendor_recent_rate\"] - d[\"vendor_hist_rate\"] # 11 ์—…์ฒด ์ตœ๊ทผ ์ถ”์„ธ\n", " X[\"urgent\"] = d[\"pr_urgent\"] # 12 ๊ธด๊ธ‰ ๊ตฌ๋งค์š”๊ตฌ\n", " X[\"fill_ratio\"] = d[\"lot_size\"] / d[\"pr_qty\"] # 13 ๊ตฌ๋งค์š”๊ตฌ ๋Œ€๋น„ ๋‚ฉํ’ˆ ๋น„์œจ\n", " X[\"dim_share\"] = d[\"def_dim\"] / d[\"defects\"].clip(lower=1) # 14 ์น˜์ˆ˜ ์˜ค์ฐจ ๋น„์ค‘\n", " for f in FSC: # 15~ ํ’ˆ๋ชฉ๊ตฐ ์›-ํ•ซ\n", " X[f\"fsc_{f}\"] = (d[\"fsc\"] == f).astype(int)\n", " return d, X\n", "\n", "\n", "class RiskEngine:\n", " \"\"\"์•™์ƒ๋ธ”(GBM 0.7 + RF 0.3, ๋กœ๊ทธ ๋ถˆ๋Ÿ‰๋ฅ ) -> ์œ„ํ—˜ ์ง€์ˆ˜ = P(ํ™•์ • ๋ถˆ๋Ÿ‰๋ฅ  > 5%) x 100\"\"\"\n", "\n", " def __init__(self, seed=0):\n", " self.gbm = GradientBoostingRegressor(n_estimators=600, max_depth=2, learning_rate=0.05, subsample=0.8, random_state=seed)\n", " self.rf = RandomForestRegressor(n_estimators=300, min_samples_leaf=2, random_state=seed)\n", "\n", " def fit(self, X, rate):\n", " y = np.log(rate)\n", " self.gbm.fit(X, y); self.rf.fit(X, y)\n", " self.sigma = float(np.std(y - self._log_pred(X)) * 1.5) # ํ•™์Šต ์ž”์ฐจ๋Š” ์ž‘๊ฒŒ ๋‚˜์˜ค๋ฏ€๋กœ 1.5๋ฐฐ๋กœ ๋ณด์ˆ˜์  ์„ค์ •\n", " return self\n", "\n", " def _log_pred(self, X):\n", " return 0.7 * self.gbm.predict(X) + 0.3 * self.rf.predict(X)\n", "\n", " def predict_rate(self, X):\n", " return np.exp(self._log_pred(X))\n", "\n", " def risk(self, X):\n", " z = (np.log(BAD_RATE) - self._log_pred(X)) / self.sigma\n", " return np.round(100 * (1 - stats.norm.cdf(z)), 1)\n", "\n", "\n", "def grade(score):\n", " return np.where(score >= 70, \"๊ฒฝ๋ณด\", np.where(score >= 40, \"์ฃผ์˜\", \"์ •์ƒ\"))\n", "\n", "\n", "d, X = make_features(make_lots(1200, seed=42))\n", "tr, te = (d[\"month\"] < 18).values, (d[\"month\"] >= 18).values # ์•ž 18๊ฐœ์›” ํ•™์Šต, ๋’ค 6๊ฐœ์›” hold-out\n", "y = d[\"confirmed_rate\"].values\n", "\n", "base = make_pipeline(StandardScaler(), Ridge(1.0)).fit(X[tr], np.log(y[tr]))\n", "pb = np.exp(base.predict(X[te]))\n", "eng = RiskEngine().fit(X[tr], y[tr])\n", "pe = eng.predict_rate(X[te])\n", "\n", "res = pd.DataFrame([{\"๋ชจ๋ธ\": name, \"MAE(%p)\": round(mean_absolute_error(y[te], p) * 100, 3), \"RMSE(%p)\": round(np.sqrt(mean_squared_error(y[te], p)) * 100, 3),\n", " \"R2\": round(r2_score(y[te], p), 3), \"MAPE\": round(float(np.mean(np.abs(y[te] - p) / y[te])), 3)}\n", " for name, p in [(\"์—…์ฒด ํ‰๊ท (๋‹จ์ˆœ)\", d[\"vendor_hist_rate\"].values[te]), (\"๊ธฐ์ค€ ๋ชจ๋ธ(Ridge)\", pb), (\"์•™์ƒ๋ธ” ์—”์ง„\", pe)]])\n", "print(\"[Hold-out(์ตœ๊ทผ 6๊ฐœ์›”) ์„ฑ๋Šฅ ๋น„๊ต]\")\n", "print(res.to_string(index=False))\n", "mae_imp = 1 - res.loc[2, \"MAE(%p)\"] / res.loc[1, \"MAE(%p)\"]\n", "rmse_imp = 1 - res.loc[2, \"RMSE(%p)\"] / res.loc[1, \"RMSE(%p)\"]\n", "print(f\"๊ธฐ์ค€ ๋ชจ๋ธ ๋Œ€๋น„ MAE {mae_imp:.1%}, RMSE {rmse_imp:.1%} ๊ฐœ์„  (๋ชฉํ‘œ 20% ์ด์ƒ: {'๋‹ฌ์„ฑ' if max(mae_imp, rmse_imp) >= 0.2 else '๋ฏธ๋‹ฌ'}), \"\n", " f\"R2 {res.loc[2, 'R2']:.3f} (๋ชฉํ‘œ 0.85 ์ด์ƒ: {'๋‹ฌ์„ฑ' if res.loc[2, 'R2'] >= 0.85 else '๋ฏธ๋‹ฌ'})\")\n", "\n", "# ์กฐ๊ธฐ๊ฒฝ๋ณด: ์œ„ํ—˜ ์ง€์ˆ˜์™€ ๋“ฑ๊ธ‰\n", "h = d[te].copy()\n", "h[\"risk\"] = eng.risk(X[te]); h[\"grade\"] = grade(h[\"risk\"])\n", "h[\"base_accept\"] = h[\"defects\"] <= h[\"accept_c\"]\n", "print(\"\\n[์œ„ํ—˜ ๋“ฑ๊ธ‰๋ณ„ ์‹ค์ œ ๋ถˆ๋Ÿ‰ ๋กœํŠธ ๋น„์œจ]\")\n", "print(h.groupby(\"grade\").agg(๋กœํŠธ์ˆ˜=(\"lot_id\", \"size\"), ์‹ค์ œ๋ถˆ๋Ÿ‰=(\"bad_lot\", \"sum\"), ๋ถˆ๋Ÿ‰๋น„์œจ=(\"bad_lot\", \"mean\")).reindex([\"๊ฒฝ๋ณด\", \"์ฃผ์˜\", \"์ •์ƒ\"]).round(3).to_string())\n", "flag = h[\"grade\"] != \"์ •์ƒ\"\n", "rec = (flag & (h[\"bad_lot\"] == 1)).sum() / h[\"bad_lot\"].sum()\n", "missed_by_sampling = h[(h[\"base_accept\"]) & (h[\"bad_lot\"] == 1)]\n", "print(f\"\\n๊ฒฝ๋ณด+์ฃผ์˜ ๋กœํŠธ๊ฐ€ ์‹ค์ œ ๋ถˆ๋Ÿ‰ ๋กœํŠธ๋ฅผ ์žก์•„๋‚ธ ๋น„์œจ {rec:.1%}\")\n", "print(f\"์ƒ˜ํ”Œ๋ง ํŒ์ •์—์„œ ์˜คํ•ฉ๊ฒฉ๋œ ๋ถˆ๋Ÿ‰ ๋กœํŠธ {len(missed_by_sampling)}๊ฑด ์ค‘ ์กฐ๊ธฐ๊ฒฝ๋ณด๊ฐ€ ์žก์€ ๊ฑด {(missed_by_sampling['grade'] != '์ •์ƒ').sum()}๊ฑด -> ์ •๋ฐ€ ์žฌ๊ฒ€์‚ฌ ๋Œ€์ƒ์œผ๋กœ ๋„˜๊ธด๋‹ค\")\n", "\n", "# ์•ˆ์ •์„ฑ: ๊ฐ™์€ ์ž…๋ ฅ ๋ฐ˜๋ณต ์‚ฐ์ถœ ์ผ๊ด€์„ฑ, ์•™์ƒ๋ธ” ๋‘ ๋ชจ๋ธ ๊ฐ„ ํŽธ์ฐจ\n", "again = RiskEngine().fit(X[tr], y[tr]).risk(X[te])\n", "print(f\"์œ„ํ—˜ ์ง€์ˆ˜ ์žฌ์‚ฐ์ถœ ์ผ๊ด€์„ฑ {np.mean(again == h['risk'].values):.0%}, GBM-RF ์˜ˆ์ธก ๋ถˆ์ผ์น˜(๋กœ๊ทธ ํ‘œ์ค€ํŽธ์ฐจ) {np.std(eng.gbm.predict(X[te]) - eng.rf.predict(X[te])):.3f}\")\n", "print(\"\\n[์œ„ํ—˜ ์ง€์ˆ˜ ์ƒ์œ„ 5๊ฐœ ๋กœํŠธ]\")\n", "print(h.sort_values(\"risk\", ascending=False).head(5)[[\"lot_id\", \"vendor\", \"fsc\", \"spec_class\", \"delay_days\", \"risk\", \"grade\", \"confirmed_rate\"]].to_string(index=False))\n", "\n", "fig, ax = plt.subplots(1, 2, figsize=(12, 4))\n", "ax[0].scatter(y[te], pb, s=8, alpha=0.5, label=\"Ridge baseline\"); ax[0].scatter(y[te], pe, s=8, alpha=0.6, label=\"ensemble\")\n", "ax[0].plot([0, 0.4], [0, 0.4], \"k--\"); ax[0].set_xlabel(\"confirmed rate\"); ax[0].set_ylabel(\"predicted\"); ax[0].legend(); ax[0].set_title(\"Hold-out prediction\")\n", "for g, c in [(0, \"tab:blue\"), (1, \"tab:red\")]:\n", " ax[1].hist(h[h[\"bad_lot\"] == g][\"risk\"], bins=20, alpha=0.6, color=c, label=[\"good lot\", \"bad lot\"][g])\n", "ax[1].axvline(40, ls=\"--\", c=\"orange\"); ax[1].axvline(70, ls=\"--\", c=\"red\"); ax[1].legend(); ax[1].set_title(\"Quality risk score (0-100)\")\n", "plt.tight_layout(); plt.show()\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "
\n", "๐Ÿ’ก [์ฐธ๊ณ ] ๋ฏธ์…˜ 05 ์˜ˆ์ƒ ์‹คํ–‰ ๊ฒฐ๊ณผ (Output)\n", "\n", "```text\n", "[Hold-out(์ตœ๊ทผ 6๊ฐœ์›”) ์„ฑ๋Šฅ ๋น„๊ต]\n", " ๋ชจ๋ธ MAE(%p) RMSE(%p) R2 MAPE\n", " ์—…์ฒด ํ‰๊ท (๋‹จ์ˆœ) 2.734 4.835 0.182 0.881\n", "๊ธฐ์ค€ ๋ชจ๋ธ(Ridge) 0.988 2.367 0.804 0.200\n", " ์•™์ƒ๋ธ” ์—”์ง„ 0.756 1.296 0.941 0.188\n", "๊ธฐ์ค€ ๋ชจ๋ธ ๋Œ€๋น„ MAE 23.5%, RMSE 45.2% ๊ฐœ์„  (๋ชฉํ‘œ 20% ์ด์ƒ: ๋‹ฌ์„ฑ), R2 0.941 (๋ชฉํ‘œ 0.85 ์ด์ƒ: ๋‹ฌ์„ฑ)\n", "\n", "[์œ„ํ—˜ ๋“ฑ๊ธ‰๋ณ„ ์‹ค์ œ ๋ถˆ๋Ÿ‰ ๋กœํŠธ ๋น„์œจ]\n", " ๋กœํŠธ์ˆ˜ ์‹ค์ œ๋ถˆ๋Ÿ‰ ๋ถˆ๋Ÿ‰๋น„์œจ\n", "grade \n", "๊ฒฝ๋ณด 61 58 0.951\n", "์ฃผ์˜ 16 9 0.562\n", "์ •์ƒ 227 5 0.022\n", "\n", "๊ฒฝ๋ณด+์ฃผ์˜ ๋กœํŠธ๊ฐ€ ์‹ค์ œ ๋ถˆ๋Ÿ‰ ๋กœํŠธ๋ฅผ ์žก์•„๋‚ธ ๋น„์œจ 93.1%\n", "์ƒ˜ํ”Œ๋ง ํŒ์ •์—์„œ ์˜คํ•ฉ๊ฒฉ๋œ ๋ถˆ๋Ÿ‰ ๋กœํŠธ 15๊ฑด ์ค‘ ์กฐ๊ธฐ๊ฒฝ๋ณด๊ฐ€ ์žก์€ ๊ฑด 10๊ฑด -> ์ •๋ฐ€ ์žฌ๊ฒ€์‚ฌ ๋Œ€์ƒ์œผ๋กœ ๋„˜๊ธด๋‹ค\n", "์œ„ํ—˜ ์ง€์ˆ˜ ์žฌ์‚ฐ์ถœ ์ผ๊ด€์„ฑ 100%, GBM-RF ์˜ˆ์ธก ๋ถˆ์ผ์น˜(๋กœ๊ทธ ํ‘œ์ค€ํŽธ์ฐจ) 0.177\n", "\n", "[์œ„ํ—˜ ์ง€์ˆ˜ ์ƒ์œ„ 5๊ฐœ ๋กœํŠธ]\n", " lot_id vendor fsc spec_class delay_days risk grade confirmed_rate\n", "LOT42-00958 V09 5935 precision -4 100.0 ๊ฒฝ๋ณด 0.1037\n", "LOT42-00469 V10 5935 precision -3 100.0 ๊ฒฝ๋ณด 0.3054\n", "LOT42-01133 V11 2530 precision 6 100.0 ๊ฒฝ๋ณด 0.3937\n", "LOT42-00827 V12 2530 precision -1 100.0 ๊ฒฝ๋ณด 0.1025\n", "LOT42-00303 V11 2530 precision -2 100.0 ๊ฒฝ๋ณด 0.1144\n", "```\n", "
\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "---\n", "\n", "## ๐ŸŽฏ ๋ฏธ์…˜ 06. ์ˆ˜์ž… ๊ฒ€์‚ฌ ๋ถˆ๋Ÿ‰ ์ถ”์„ธ ๋ฐ ๊ณ ์œ„ํ—˜ ๋‚ฉํ’ˆ์—…์ฒด ๋ชจ๋‹ˆํ„ฐ๋ง ํ’ˆ์งˆ ๋Œ€์‹œ๋ณด๋“œ ๊ตฌํ˜„\n", "**์†Œ์š”์‹œ๊ฐ„**: ์•ฝ 3.0์‹œ๊ฐ„\n", "\n", "### ๐Ÿ“‹ ๋ฏธ์…˜ ๋ฐฐ๊ฒฝ ๋ฐ ์ˆ˜ํ–‰ ์ง€์นจ\n", "#### ๋ฏธ์…˜ ๋ฐฐ๊ฒฝ ๋ฐ ๋‹น์œ„์„ฑ\n", "\n", "์ˆ˜์ž… ๊ฒ€์‚ฌ ๋ถˆ๋Ÿ‰ ์ถ”์„ธ ๋ฐ ๊ณ ์œ„ํ—˜ ๋‚ฉํ’ˆ์—…์ฒด ๋ชจ๋‹ˆํ„ฐ๋ง ๋Œ€์‹œ๋ณด๋“œ๋ฅผ ๊ตฌํ˜„ํ•œ๋‹ค. ์‹ค์ œ ์ „์ˆ  ํ˜„์žฅ์—์„œ๋Š” ๋‹จ์ˆœ ํƒ์ง€์— ๊ทธ์น˜์ง€ ์•Š๊ณ  ์‹ ์†ํ•œ ์œ„ํ˜‘ ํŒ๋‹จ๊ณผ ์‹ ๋ขฐ๋„ ๋†’์€ ์˜์‚ฌ๊ฒฐ์ •์ด ํ•„์ˆ˜์ ์ด๋ฏ€๋กœ, **์ˆ˜์ž… ๊ฒ€์‚ฌ ๋ถˆ๋Ÿ‰ ์ถ”์„ธ์™€ ๊ณ ์œ„ํ—˜ ๋‚ฉํ’ˆ์—…์ฒด๋ฅผ ๋ชจ๋‹ˆํ„ฐ๋งํ•˜๋Š” ํ’ˆ์งˆ ๋Œ€์‹œ๋ณด๋“œ๋ฅผ ๋งŒ๋“ค๊ณ , ๊ฒฝ๋ณด ๋กœํŠธ๋งˆ๋‹ค ๊ธฐ์—ฌ๋„ ์ƒ์œ„ 3๊ฐœ ์ธ์ž๋ฅผ ์„ค๋ช…ํ•œ๋‹ค.** ๊ณผ์—…์„ ์™„์ˆ˜ํ•˜์—ฌ ์˜คํŒ์„ ๋ฐฉ์ง€ํ•˜๊ณ  ์ž‘์ „ ๋Œ€์‘๋ ฅ์„ ๊ทน๋Œ€ํ™”ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.\n", "\n", "์ด ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด ๋ณธ ๋ฏธ์…˜์—์„œ๋Š” **์›”๋ณ„ ํ‘œ๋ณธ ๋ถˆ๋Ÿ‰๋ฅ ๊ณผ ๊ด€๋ฆฌํ•œ๊ณ„, ์—…์ฒด๋ณ„ ํ‰๊ท  ์œ„ํ—˜ ์ง€์ˆ˜์™€ ์ถ”์„ธ(์•…ํ™”ยท์œ ์ง€), ์œ„ํ—˜ ์ง€์ˆ˜ ๋ถ„ํฌ, ๋Œ€ํ‘œ ๋กœํŠธ์˜ ๊ธฐ์—ฌ๋„ ์›Œํ„ฐํด์„ 2x2 ๋Œ€์‹œ๋ณด๋“œ๋กœ ๊ทธ๋ฆฐ๋‹ค ๊ณผ์ •์„ ์ง„ํ–‰ํ•˜๋Š”** ์‹ค์Šต์„ ์ฒด๊ณ„์ ์œผ๋กœ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค.\n", "\n", "ํ•™์Šต์ž๋Š” ์‹ค์Šต๊ฐ€์ด๋“œ์— ์ œ์‹œ๋œ ์ด 4๊ฐœ ์„ธ๋ถ€ ํ™œ๋™(๊ธฐ๋ณธ ๊ธฐ๋Šฅ ๊ตฌํ˜„ โž” ๊ฒฐ๊ณผ ์ง€ํ‘œ ๊ฒ€์ฆ โž” ํŒŒ๋ผ๋ฏธํ„ฐ ํŠœ๋‹ ๋ฐ ๋„์ „๊ณผ์ œ)์„ ์ˆœ์ฐจ์ ์œผ๋กœ ์ง„ํ–‰ํ•˜๋ฉฐ ํ•ต์‹ฌ ํŒ์ • ๊ธฐ์ค€์„ ๋‹ฌ์„ฑํ•ฉ๋‹ˆ๋‹ค.\n", "\n", "**์ด๋ฒˆ ๊ฒฐ๊ณผ๋ฌผ:** `src/explainability/qlt02_xai_explainer.py ๋ฐ XAI ๋ฆฌํฌํŠธ` ํ˜•ํƒœ์˜ ๊ฒฐ๊ณผ๋ฌผ์„ ์™„์„ฑํ•œ๋‹ค.\n", "\n", "### ๐Ÿ” ํ‰๊ฐ€ ๋ฐ ๊ฒ€์ฆ ๊ธฐ์ค€ (PASS ์กฐ๊ฑด)\n", "#### ๋ฏธ์…˜ ํŒ์ •\n", "\n", "**ํ‰๊ฐ€ ๊ตฌ๋ถ„:** ํ•„์ˆ˜\n", "\n", "**ํ•ต์‹ฌ ํ‰๊ฐ€์˜์—ญ:** ๊ตฐ์ˆ˜ ์žฅ๊ต/๊ตฐ๋ฌด์›์ด ์‹ ๋ขฐํ•˜๊ณ  ์กฐ์น˜ํ•  ์ˆ˜ ์žˆ๋Š” ์„ค๋ช…์˜ ๋ช…ํ™•์„ฑ\n", "\n", "#### ๋ฏธ์…˜ PASS ์กฐ๊ฑด\n", "\n", "- ์ •๋Ÿ‰ ๊ธฐ์ค€: ์ฃผ์š” ์˜ˆ์ธก ๊ฑด๋‹น ๊ธฐ์—ฌ๋„ ์ƒ์œ„ 3๊ฐœ ์ธ์ž ์ž๋™ ์‚ฐ์ถœ๋ฅ  100%\n", "\n", "\n", "- ์ง„ํ–‰ ๊ธฐ์ค€: SHAP summary plot ๋ฐ waterfall plot ์ƒ์„ฑ ํŒŒ์ดํ”„๋ผ์ธ ์™„์„ฑ\n", "\n", "\n", "- ๊ฒ€์ฆ ๋ฐฉ๋ฒ•: ์‹ค๋ฌด์ž ๋Œ€์ƒ ์„ค๋ช… ๊ทผ๊ฑฐ ์œ ์šฉ์„ฑ ํ‰๊ฐ€ ๊ฒ€์ฆ\n", "\n", "\n", "#### ์ฑ„์ ์ž ํŒ์ • ๋ฉ”๋ชจ\n", "\n", "> ๋‹จ์ˆœ ์ฝ”๋“œ ์‹คํ–‰ ์—ฌ๋ถ€๋ฟ๋งŒ ์•„๋‹ˆ๋ผ ์ถœ๋ ฅ๋œ ์ˆ˜์น˜์™€ ๊ทผ๊ฑฐ ์ž๋ฃŒ๊ฐ€ ๊ตฐ ์‹ค๋ฌด ์š”๊ตฌ์‚ฌํ•ญ์— ๋ถ€ํ•ฉํ•˜๋Š”์ง€ ๋ฉด๋ฐ€ํžˆ ํ™•์ธํ•œ๋‹ค.\n", "\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "\"\"\"์ˆ˜๋ น๊ฒ€์‚ฌ ๋ถˆ๋Ÿ‰ ์ถ”์„ธ์™€ ๊ณ ์œ„ํ—˜ ๋‚ฉํ’ˆ์—…์ฒด๋ฅผ ๋ชจ๋‹ˆํ„ฐ๋งํ•˜๋Š” ํ’ˆ์งˆ ๋Œ€์‹œ๋ณด๋“œ๋ฅผ ๋งŒ๋“ค๊ณ , ๊ฒฝ๋ณด ๋กœํŠธ๋งˆ๋‹ค 7๊ฐœ ์š”์ธ๊ตฐ์˜ ์ •ํ™•ํ•œ ์„€ํ”Œ๋ฆฌ ๊ธฐ์—ฌ๋„๋ฅผ ๊ณ„์‚ฐํ•ด ๊ธฐ์—ฌ๋„ ์ƒ์œ„ 3๊ฐœ ์ธ์ž์™€ ์ž์—ฐ์–ด ์„ค๋ช…ยท์กฐ์น˜ ์ œ์•ˆ์„ ์ž๋™ ์ƒ์„ฑํ•œ๋‹ค.\"\"\"\n", "import itertools\n", "import math\n", "import numpy as np\n", "import pandas as pd\n", "import matplotlib.pyplot as plt\n", "from scipy import stats\n", "from sklearn.ensemble import GradientBoostingRegressor\n", "\n", "# ---------------------------------------------------------------------------\n", "# [๊ต์œก์šฉ๊ฐ€์ƒ์ž๋ฃŒ] ๊ตฐ์ˆ˜ํ’ˆ ์ˆ˜๋ น๊ฒ€์‚ฌ ๋กœํŠธ ๋ฐ์ดํ„ฐ ์ƒ์„ฑ๊ธฐ (์‹ค์ œ ์—…์ฒดยทํ’ˆ๋ชฉยท๊ณ„์•ฝ ์ •๋ณด ์—†์Œ)\n", "# ๊ตฌ๋งค์š”๊ตฌ(๊ทœ๊ฒฉยท๊ธด๊ธ‰ ์—ฌ๋ถ€) -> ๊ณ„์•ฝ(๋‚ฉ๊ธฐ) -> ๋‚ฉํ’ˆ -> ์ˆ˜๋ น๊ฒ€์‚ฌ(ํ‘œ๋ณธ) -> ํ™•์ • ๋ถˆ๋Ÿ‰๋ฅ (์ „์ˆ˜๊ฒ€์‚ฌยท์‚ฌ์šฉ๋ถ€๋Œ€ ํ•˜์ž ๋ฐ˜์˜)\n", "# ---------------------------------------------------------------------------\n", "FSC = {\"5305\": \"screw/bolt\", \"5330\": \"gasket\", \"5935\": \"connector\", \"6135\": \"battery\", \"2530\": \"brake part\", \"8415\": \"clothing\"}\n", "VENDORS = [f\"V{i:02d}\" for i in range(1, 13)]\n", "VENDOR_Q = dict(zip(VENDORS, [-0.6, -0.4, -0.3, -0.2, -0.1, 0.0, 0.1, 0.2, 0.3, 0.5, 0.8, 1.1])) # ์—…์ฒด ๊ณ ์œ  ํ’ˆ์งˆ ์ˆ˜์ค€(์ˆจ์€ ๊ฐ’)\n", "FSC_Q = {\"5305\": -0.3, \"5330\": 0.0, \"5935\": 0.25, \"6135\": 0.1, \"2530\": 0.35, \"8415\": -0.2}\n", "SAMPLE_PLAN = [(500, 50, 3), (1200, 80, 5), (3200, 125, 7), (10 ** 9, 200, 10)] # (๋กœํŠธ ํฌ๊ธฐ ์ƒํ•œ, ํ‘œ๋ณธ n, ํ•ฉ๊ฒฉํŒ์ •์ˆ˜ c) ๋‹จ์ˆœํ™” ๊ธฐ์ค€\n", "BAD_RATE = 0.05 # ํ™•์ • ๋ถˆ๋Ÿ‰๋ฅ  5% ์ดˆ๊ณผ ๋กœํŠธ = ๋ถˆํ•ฉ๊ฒฉ ๋Œ€์ƒ\n", "\n", "\n", "def sample_plan(lot_size):\n", " for up, n, c in SAMPLE_PLAN:\n", " if lot_size <= up:\n", " return n, c\n", "\n", "\n", "def true_rate(d, rng=None):\n", " \"\"\"์ˆจ์€ ๋ถˆ๋Ÿ‰ ๋ฐœ์ƒ ๊ตฌ์กฐ: ์—…์ฒด ์ˆ˜์ค€ + ํ’ˆ๋ชฉ๊ตฐ + ์ •๋ฐ€ ๊ทœ๊ฒฉ + ๊ธด๊ธ‰ ๊ตฌ๋งค + ๊ทœ๊ฒฉ ๋ณ€๊ฒฝยท๋ถˆ์ผ์น˜ + ๋‚ฉ๊ธฐ ์ง€์—ฐ + (์ •๋ฐ€ ๊ทœ๊ฒฉ x ์ด‰๋ฐ• ๋‚ฉ๊ธฐ) ์ƒํ˜ธ์ž‘์šฉ.\"\"\"\n", " short = (d[\"lead_days\"] < 45).astype(float)\n", " z = (-4.35 + d[\"vendor\"].map(VENDOR_Q) + d[\"fsc\"].map(FSC_Q) + 0.55 * (d[\"spec_class\"] == \"precision\")\n", " + 0.35 * d[\"pr_urgent\"] + 0.6 * d[\"spec_changed\"] + 0.8 * d[\"spec_mismatch\"]\n", " + 0.9 * np.log1p(np.maximum(d[\"delay_days\"], 0) / 7) + 0.9 * short * (d[\"spec_class\"] == \"precision\")\n", " + 0.5 * (d[\"month\"] >= 18) * (d[\"vendor\"] == \"V10\")) # V10 ํ›„๋ฐ˜๊ธฐ ํ’ˆ์งˆ ์ €ํ•˜(๋“œ๋ฆฌํ”„ํŠธ)\n", " if rng is not None:\n", " z = z + rng.normal(0, 0.12, len(d))\n", " return np.clip(np.exp(z), 0.002, 0.35)\n", "\n", "\n", "def make_lots(n=1200, seed=42, months=24):\n", " rng = np.random.default_rng(seed)\n", " d = pd.DataFrame({\"lot_id\": [f\"LOT{seed % 100:02d}-{i:05d}\" for i in range(n)]})\n", " d[\"month\"] = rng.integers(0, months, n)\n", " d[\"receipt_date\"] = pd.Timestamp(\"2024-01-01\") + pd.to_timedelta(d[\"month\"] * 30 + rng.integers(0, 30, n), unit=\"D\")\n", " d = d.sort_values(\"receipt_date\").reset_index(drop=True)\n", " d[\"month\"] = ((d[\"receipt_date\"] - pd.Timestamp(\"2024-01-01\")).dt.days // 30).clip(0, months - 1)\n", " d[\"vendor\"] = rng.choice(VENDORS, n, p=np.r_[np.full(6, 0.1), np.full(6, 0.4 / 6)])\n", " d[\"fsc\"] = rng.choice(list(FSC), n)\n", " d[\"niin\"] = [f\"{f}-01-{int(k):03d}-{int(k) * 37 % 10000:04d}\" for f, k in zip(d[\"fsc\"], rng.integers(100, 110, n))]\n", " d[\"spec_class\"] = np.where(rng.random(n) < np.where(d[\"fsc\"].isin([\"5935\", \"2530\"]), 0.6, 0.25), \"precision\", \"general\")\n", " d[\"tolerance_mm\"] = np.where(d[\"spec_class\"] == \"precision\", rng.uniform(0.05, 0.1, n), rng.uniform(0.2, 0.5, n)).round(3)\n", " d[\"spec_rev\"] = rng.integers(1, 6, n)\n", " d[\"pr_urgent\"] = (rng.random(n) < 0.2).astype(int)\n", " d[\"spec_changed\"] = (rng.random(n) < 0.12).astype(int)\n", " d[\"spec_mismatch\"] = (rng.random(n) < 0.08).astype(int)\n", " d[\"pr_qty\"] = rng.integers(200, 5000, n)\n", " d[\"lot_size\"] = (d[\"pr_qty\"] * rng.uniform(0.5, 1.0, n)).astype(int)\n", " d[\"lead_days\"] = np.where(d[\"pr_urgent\"] == 1, rng.integers(20, 60, n), rng.integers(40, 150, n))\n", " d[\"delay_days\"] = np.round(rng.gamma(1.2, 6, n) * rng.binomial(1, 0.45, n) - rng.integers(0, 5, n)).astype(int)\n", " d[\"unit_cost\"] = rng.choice([2_000, 15_000, 40_000, 120_000], n, p=[0.35, 0.3, 0.25, 0.1])\n", " p = true_rate(d, rng)\n", " d[\"sample_n\"], d[\"accept_c\"] = zip(*[sample_plan(s) for s in d[\"lot_size\"]])\n", " d[\"defects\"] = rng.binomial(d[\"sample_n\"], p)\n", " share = np.where(d[\"spec_class\"] == \"precision\", 0.55, 0.3)\n", " d[\"def_dim\"] = rng.binomial(d[\"defects\"], share)\n", " d[\"def_spec\"] = rng.binomial(d[\"defects\"] - d[\"def_dim\"], 0.6)\n", " d[\"def_visual\"] = d[\"defects\"] - d[\"def_dim\"] - d[\"def_spec\"]\n", " d[\"dim_dev_mm\"] = (d[\"tolerance_mm\"] * (0.3 + 2.0 * p) * rng.lognormal(0, 0.3, n)).round(4)\n", " d[\"confirmed_rate\"] = np.clip(p * rng.lognormal(0, 0.05, n), 0.001, 0.4).round(4) # ์ „์ˆ˜๊ฒ€์‚ฌยทํ•˜์ž ๋ฐ˜์˜ ํ™•์ • ๋ถˆ๋Ÿ‰๋ฅ \n", " d[\"field_claims\"] = rng.poisson(d[\"lot_size\"] * p * 0.03)\n", " d[\"inspector\"] = rng.choice([\"INS-1\", \"INS-2\", \"INS-3\", \"INS-4\"], n)\n", " d[\"emergency_release\"] = ((d[\"pr_urgent\"] == 1) & (rng.random(n) < 0.3)).astype(int) # ๊ธด๊ธ‰ ๋ถˆ์ถœ(๊ฒ€์‚ฌ ์ „ ์„ ๋ถˆ์ถœ) ์˜ˆ์™ธ\n", " d[\"bad_lot\"] = (d[\"confirmed_rate\"] > BAD_RATE).astype(int)\n", " return d\n", "# ---------------------------------------------------------------------------\n", "\n", "# ---- ํŒŒ์ƒ ํ”ผ์ฒ˜ ๊ณตํ†ต ํ•จ์ˆ˜ (๋ฏธ์…˜ 4 ์ดํ›„ ๊ณตํ†ต) ----\n", "def add_history(d):\n", " \"\"\"์—…์ฒด๋ณ„ ๊ณผ๊ฑฐ ํ™•์ • ๋ถˆ๋Ÿ‰๋ฅ (ํ˜„์žฌ ๋กœํŠธ ์ด์ „ ๋กœํŠธ๋งŒ ์‚ฌ์šฉ: ์ •๋ณด ๋ˆ„์ˆ˜ ๋ฐฉ์ง€)๊ณผ ์ตœ๊ทผ 5๋กœํŠธ ์ถ”์„ธ.\"\"\"\n", " d = d.sort_values(\"receipt_date\").reset_index(drop=True)\n", " g = d.groupby(\"vendor\")[\"confirmed_rate\"]\n", " d[\"vendor_hist_rate\"] = g.transform(lambda s: s.shift().expanding().mean()).fillna(d[\"confirmed_rate\"].mean())\n", " d[\"vendor_recent_rate\"] = g.transform(lambda s: s.shift().rolling(5, min_periods=1).mean()).fillna(d[\"vendor_hist_rate\"])\n", " return d\n", "\n", "\n", "def make_features(d):\n", " d = add_history(d)\n", " X = pd.DataFrame(index=d.index)\n", " X[\"obs_rate\"] = np.log((d[\"defects\"] + 0.5) / d[\"sample_n\"]) # 1 ํ‘œ๋ณธ ๋ถˆ๋Ÿ‰๋ฅ \n", " X[\"log_lot\"] = np.log(d[\"lot_size\"]) # 2 ๋กœํŠธ ๊ทœ๋ชจ(๋กœ๊ทธ)\n", " X[\"delay_pos\"] = d[\"delay_days\"].clip(lower=0) # 3 ๋‚ฉ๊ธฐ ์ง€์—ฐ์ผ\n", " X[\"delay_ratio\"] = X[\"delay_pos\"] / d[\"lead_days\"] # 4 ์ง€์—ฐ/๊ณ„์•ฝ๊ธฐ๊ฐ„ ๋น„\n", " X[\"short_lead\"] = (d[\"lead_days\"] < 45).astype(int) # 5 ์ด‰๋ฐ• ๋‚ฉ๊ธฐ\n", " X[\"precision\"] = (d[\"spec_class\"] == \"precision\").astype(int) # 6 ์ •๋ฐ€ ๊ทœ๊ฒฉ\n", " X[\"prec_x_short\"] = X[\"precision\"] * X[\"short_lead\"] # 7 ์ •๋ฐ€ x ์ด‰๋ฐ•(์ƒํ˜ธ์ž‘์šฉ)\n", " X[\"spec_risk\"] = d[\"spec_changed\"] + d[\"spec_mismatch\"] # 8 ๊ทœ๊ฒฉ ๋ณ€๊ฒฝยท๋ถˆ์ผ์น˜ ํ•ฉ\n", " X[\"dim_ratio\"] = np.log(d[\"dim_dev_mm\"] / d[\"tolerance_mm\"]) # 9 ์น˜์ˆ˜ ํŽธ์ฐจ/๊ณต์ฐจ\n", " X[\"vendor_hist\"] = np.log(d[\"vendor_hist_rate\"]) # 10 ์—…์ฒด ๊ณผ๊ฑฐ ๋ถˆ๋Ÿ‰๋ฅ \n", " X[\"vendor_trend\"] = d[\"vendor_recent_rate\"] - d[\"vendor_hist_rate\"] # 11 ์—…์ฒด ์ตœ๊ทผ ์ถ”์„ธ\n", " X[\"urgent\"] = d[\"pr_urgent\"] # 12 ๊ธด๊ธ‰ ๊ตฌ๋งค์š”๊ตฌ\n", " X[\"fill_ratio\"] = d[\"lot_size\"] / d[\"pr_qty\"] # 13 ๊ตฌ๋งค์š”๊ตฌ ๋Œ€๋น„ ๋‚ฉํ’ˆ ๋น„์œจ\n", " X[\"dim_share\"] = d[\"def_dim\"] / d[\"defects\"].clip(lower=1) # 14 ์น˜์ˆ˜ ์˜ค์ฐจ ๋น„์ค‘\n", " for f in FSC: # 15~ ํ’ˆ๋ชฉ๊ตฐ ์›-ํ•ซ\n", " X[f\"fsc_{f}\"] = (d[\"fsc\"] == f).astype(int)\n", " return d, X\n", "\n", "GROUPS = {\"์—…์ฒด ์ด๋ ฅ\": [\"vendor_hist\", \"vendor_trend\"], \"๊ทœ๊ฒฉ ๋ณ€๊ฒฝยท๋ถˆ์ผ์น˜\": [\"spec_risk\"], \"์ •๋ฐ€ ๊ทœ๊ฒฉ\": [\"precision\"],\n", " \"๋‚ฉ๊ธฐ ์ง€์—ฐยท์ด‰๋ฐ•\": [\"delay_pos\", \"delay_ratio\", \"short_lead\", \"prec_x_short\"],\n", " \"ํ‘œ๋ณธ๊ฒ€์‚ฌ ๊ฒฐ๊ณผ\": [\"obs_rate\", \"dim_ratio\", \"dim_share\"], \"๊ตฌ๋งค์š”๊ตฌ ์กฐ๊ฑด\": [\"urgent\", \"fill_ratio\", \"log_lot\"],\n", " \"ํ’ˆ๋ชฉ๊ตฐ\": [f\"fsc_{f}\" for f in FSC]}\n", "ACTION = {\"์—…์ฒด ์ด๋ ฅ\": \"์—…์ฒด ํ’ˆ์งˆ ์ด๋ ฅ ์žฌ์ ๊ฒ€ยท์‹œ์ •์กฐ์น˜ ์š”๊ตฌ\", \"๊ทœ๊ฒฉ ๋ณ€๊ฒฝยท๋ถˆ์ผ์น˜\": \"๊ตฌ๋งค์š”๊ตฌ ๊ทœ๊ฒฉ์„œ์™€ ๋‚ฉํ’ˆ ๊ทœ๊ฒฉ ๋Œ€์กฐ\",\n", " \"์ •๋ฐ€ ๊ทœ๊ฒฉ\": \"์ •๋ฐ€ ์ธก์ • ์žฅ๋น„๋กœ ์น˜์ˆ˜ ์žฌ์ธก์ •\", \"๋‚ฉ๊ธฐ ์ง€์—ฐยท์ด‰๋ฐ•\": \"๋‚ฉ๊ธฐ ์—ฌ์œ  ํ™•๋ณดยท์ง€์—ฐ ์‚ฌ์œ  ํ™•์ธ\",\n", " \"ํ‘œ๋ณธ๊ฒ€์‚ฌ ๊ฒฐ๊ณผ\": \"ํ‘œ๋ณธ ํฌ๊ธฐ ํ™•๋Œ€ ์žฌ๊ฒ€์‚ฌ\", \"๊ตฌ๋งค์š”๊ตฌ ์กฐ๊ฑด\": \"๊ธด๊ธ‰ ๊ตฌ๋งค ๊ฑด ์‚ฌํ›„ ๊ฒ€์‚ฌ ๊ฐ•ํ™”\", \"ํ’ˆ๋ชฉ๊ตฐ\": \"ํ’ˆ๋ชฉ๊ตฐ๋ณ„ ๊ฒ€์‚ฌ ๊ธฐ์ค€ ํ™•์ธ\"}\n", "\n", "d, X = make_features(make_lots(1200, seed=42))\n", "tr, te = (d[\"month\"] < 18).values, (d[\"month\"] >= 18).values\n", "y = np.log(d[\"confirmed_rate\"].values)\n", "gbm = GradientBoostingRegressor(n_estimators=300, max_depth=2, learning_rate=0.08, subsample=0.8, random_state=0).fit(X.values[tr], y[tr])\n", "sigma = float(np.std(y[tr] - gbm.predict(X.values[tr])) * 1.5)\n", "h = d[te].copy()\n", "h[\"pred\"] = np.exp(gbm.predict(X.values[te]))\n", "h[\"risk\"] = np.round(100 * (1 - stats.norm.cdf((np.log(BAD_RATE) - np.log(h[\"pred\"])) / sigma)), 1)\n", "h[\"grade\"] = np.where(h[\"risk\"] >= 70, \"๊ฒฝ๋ณด\", np.where(h[\"risk\"] >= 40, \"์ฃผ์˜\", \"์ •์ƒ\"))\n", "\n", "# ---------------- 1) ๋Œ€์‹œ๋ณด๋“œ ์š”์•ฝ ํ‘œ ----------------\n", "print(\"[ํ’ˆ์งˆ ๋Œ€์‹œ๋ณด๋“œ] ์ตœ๊ทผ 6๊ฐœ์›” ์ˆ˜๋ น๊ฒ€์‚ฌ ํ˜„ํ™ฉ [๊ต์œก์šฉ๊ฐ€์ƒ์ž๋ฃŒ]\")\n", "m = d.groupby(\"month\").agg(sample_rate=(\"defects\", \"sum\"), n=(\"sample_n\", \"sum\"), conf=(\"confirmed_rate\", \"mean\"))\n", "m[\"sample_rate\"] = m[\"sample_rate\"] / m[\"n\"]\n", "p0 = m.loc[:17, \"sample_rate\"].mean()\n", "ucl = p0 + 3 * np.sqrt(p0 * (1 - p0) / m[\"n\"])\n", "print(f\"์›”๋ณ„ ํ‘œ๋ณธ ๋ถˆ๋Ÿ‰๋ฅ  ๊ด€๋ฆฌํ•œ๊ณ„ ์ดˆ๊ณผ ์›”: {list(m.index[m['sample_rate'] > ucl])} (๊ธฐ์ค€ {p0:.2%})\")\n", "vend = h.groupby(\"vendor\").agg(๋กœํŠธ=(\"lot_id\", \"size\"), ํ‰๊ท ์œ„ํ—˜=(\"risk\", \"mean\"), ๊ฒฝ๋ณด=(\"grade\", lambda s: (s == \"๊ฒฝ๋ณด\").sum()),\n", " ์‹ค์ œ๋ถˆ๋Ÿ‰๋ฅ =(\"bad_lot\", \"mean\"), ์ตœ๊ทผ์ถ”์„ธ=(\"vendor_recent_rate\", \"mean\"), ๊ณผ๊ฑฐ=(\"vendor_hist_rate\", \"mean\"))\n", "vend[\"์ถ”์„ธ\"] = np.where(vend[\"์ตœ๊ทผ์ถ”์„ธ\"] > vend[\"๊ณผ๊ฑฐ\"] * 1.2, \"์•…ํ™”\", \"์œ ์ง€\")\n", "vend[\"์ƒํƒœ\"] = np.where(vend[\"ํ‰๊ท ์œ„ํ—˜\"] >= 50, \"๊ณ ์œ„ํ—˜\", np.where(vend[\"ํ‰๊ท ์œ„ํ—˜\"] >= 25, \"๊ด€์ฐฐ\", \"์ •์ƒ\"))\n", "print(vend.sort_values(\"ํ‰๊ท ์œ„ํ—˜\", ascending=False).head(6)[[\"๋กœํŠธ\", \"ํ‰๊ท ์œ„ํ—˜\", \"๊ฒฝ๋ณด\", \"์‹ค์ œ๋ถˆ๋Ÿ‰๋ฅ \", \"์ถ”์„ธ\", \"์ƒํƒœ\"]].round(2).to_string())\n", "\n", "# ---------------- 2) ์ •ํ™•ํ•œ ์„€ํ”Œ๋ฆฌ ๊ธฐ์—ฌ๋„(์š”์ธ๊ตฐ 7๊ฐœ, 2^7 ์—ฐํ•ฉ) ----------------\n", "cols = {g: [X.columns.get_loc(c) for c in cs] for g, cs in GROUPS.items()}\n", "names = list(GROUPS)\n", "bg = X[tr].sample(40, random_state=1).values.astype(float) # ๋ฐฐ๊ฒฝ ๋ฐ์ดํ„ฐ(ํ•™์Šต ๊ตฌ๊ฐ„ ํ‘œ๋ณธ)\n", "base_val = gbm.predict(bg).mean()\n", "coal = list(itertools.product([0, 1], repeat=len(names)))\n", "\n", "\n", "def shapley(x):\n", " \"\"\"์—ฐํ•ฉ S๋งˆ๋‹ค S์— ์†ํ•œ ์š”์ธ๊ตฐ์€ x ๊ฐ’, ๋‚˜๋จธ์ง€๋Š” ๋ฐฐ๊ฒฝ ๊ฐ’์œผ๋กœ ๋ฐ”๊ฟ” ํ‰๊ท  ์˜ˆ์ธก v(S)๋ฅผ ๊ตฌํ•˜๊ณ  ์„€ํ”Œ๋ฆฌ ๊ณต์‹์œผ๋กœ ํ•ฉ์‚ฐ.\"\"\"\n", " batch = np.repeat(bg[None], len(coal), axis=0)\n", " for k, s in enumerate(coal):\n", " for g, on in zip(names, s):\n", " if on:\n", " batch[k][:, cols[g]] = x[cols[g]]\n", " v = dict(zip(coal, gbm.predict(batch.reshape(-1, bg.shape[1])).reshape(len(coal), -1).mean(1)))\n", " n, phi = len(names), np.zeros(len(names))\n", " for s in coal:\n", " for i in range(n):\n", " if not s[i]:\n", " w = math.factorial(sum(s)) * math.factorial(n - sum(s) - 1) / math.factorial(n)\n", " phi[i] += w * (v[s[:i] + (1,) + s[i + 1:]] - v[s])\n", " return phi, v[tuple([1] * n)]\n", "\n", "\n", "alert = h[h[\"grade\"] == \"๊ฒฝ๋ณด\"]\n", "rows = []\n", "for idx in alert.index:\n", " phi, full = shapley(X.loc[idx].values)\n", " top = np.argsort(-phi)[:3]\n", " rows.append({\"lot_id\": h.loc[idx, \"lot_id\"], \"risk\": h.loc[idx, \"risk\"], **{names[i]: phi[i] for i in range(len(names))},\n", " \"top3\": [names[i] for i in top], \"top3_phi\": [phi[i] for i in top], \"add_err\": abs(base_val + phi.sum() - full)})\n", "ex = pd.DataFrame(rows)\n", "done = (ex[\"top3\"].map(len) == 3).mean()\n", "print(f\"\\n๊ฒฝ๋ณด ๋กœํŠธ {len(ex)}๊ฑด ์„ค๋ช… ์ƒ์„ฑ: ์ƒ์œ„ 3๊ฐœ ์ธ์ž ์ž๋™ ์‚ฐ์ถœ๋ฅ  {done:.0%} (๋ชฉํ‘œ 100%: {'๋‹ฌ์„ฑ' if done == 1 else '๋ฏธ๋‹ฌ'}), \"\n", " f\"๊ฐ€๋ฒ•์„ฑ ์˜ค์ฐจ ์ตœ๋Œ€ {ex['add_err'].max():.1e}\")\n", "print(\"[์š”์ธ๊ตฐ๋ณ„ ํ‰๊ท  ๊ธฐ์—ฌ๋„(๋กœ๊ทธ ๋ถˆ๋Ÿ‰๋ฅ , ๊ฒฝ๋ณด ๋กœํŠธ)]\")\n", "print(ex[names].mean().sort_values(ascending=False).round(3).to_string())\n", "\n", "print(\"\\n[์ž๋™ ์„ค๋ช… ์˜ˆ์‹œ]\")\n", "for r in ex.sort_values(\"risk\", ascending=False).head(3).itertuples():\n", " parts = \", \".join(f\"{g}(x{np.exp(p):.2f})\" for g, p in zip(r.top3, r.top3_phi))\n", " print(f\" {r.lot_id} ์œ„ํ—˜ {r.risk:.0f}: {parts} ์ˆœ์œผ๋กœ ๋ถˆ๋Ÿ‰๋ฅ  ์˜ˆ์ธก์„ ๋†’์˜€๋‹ค -> ์กฐ์น˜: {ACTION[r.top3[0]]}\")\n", "\n", "# ์„ค๋ช… ํƒ€๋‹น์„ฑ ์ ๊ฒ€: ๊ทœ๊ฒฉ ๋ถˆ์ผ์น˜ ๋กœํŠธ์—์„œ '๊ทœ๊ฒฉ' ์š”์ธ์ด ์–‘(+)์œผ๋กœ ๋‚˜์˜ค๋Š”๊ฐ€\n", "mm = alert[\"spec_mismatch\"].values == 1\n", "if mm.any():\n", " print(f\"ํƒ€๋‹น์„ฑ ์ ๊ฒ€: ๊ทœ๊ฒฉ ๋ถˆ์ผ์น˜ ๊ฒฝ๋ณด ๋กœํŠธ {mm.sum()}๊ฑด ์ค‘ '๊ทœ๊ฒฉ ๋ณ€๊ฒฝยท๋ถˆ์ผ์น˜' ๊ธฐ์—ฌ ์–‘์ˆ˜ {np.mean(ex.loc[mm, '๊ทœ๊ฒฉ ๋ณ€๊ฒฝยท๋ถˆ์ผ์น˜'] > 0):.0%}\")\n", "\n", "fig, ax = plt.subplots(2, 2, figsize=(13, 8))\n", "ax[0, 0].plot(m.index, m[\"sample_rate\"], \"o-\", label=\"sample defect rate\"); ax[0, 0].plot(m.index, ucl, \"r--\", label=\"UCL (3 sigma)\")\n", "ax[0, 0].axvspan(17.5, 23.5, color=\"gray\", alpha=0.1); ax[0, 0].legend(); ax[0, 0].set_title(\"Monthly inspection defect trend\"); ax[0, 0].set_xlabel(\"month\")\n", "vs = vend.sort_values(\"ํ‰๊ท ์œ„ํ—˜\")\n", "ax[0, 1].barh(vs.index, vs[\"ํ‰๊ท ์œ„ํ—˜\"], color=[\"tab:red\" if s == \"๊ณ ์œ„ํ—˜\" else \"tab:orange\" if s == \"๊ด€์ฐฐ\" else \"tab:green\" for s in vs[\"์ƒํƒœ\"]])\n", "ax[0, 1].set_title(\"Vendor mean risk score (last 6 months)\")\n", "ax[1, 0].hist(h[\"risk\"], bins=25, color=\"tab:blue\"); ax[1, 0].axvline(40, c=\"orange\", ls=\"--\"); ax[1, 0].axvline(70, c=\"red\", ls=\"--\")\n", "ax[1, 0].set_title(\"Lot risk score distribution\")\n", "r0 = ex.sort_values(\"risk\", ascending=False).iloc[0]\n", "vals = [r0[g] for g in names]; order = np.argsort(vals)[::-1]\n", "cum = base_val\n", "eng = [\"vendor history\", \"spec change\", \"precision spec\", \"delivery delay\", \"sample result\", \"PR condition\", \"item group\"]\n", "for k, i in enumerate(order):\n", " ax[1, 1].barh(k, vals[i], left=cum, color=\"tab:red\" if vals[i] > 0 else \"tab:blue\"); cum += vals[i]\n", "ax[1, 1].set_yticks(range(len(order))); ax[1, 1].set_yticklabels([eng[i] for i in order]); ax[1, 1].invert_yaxis()\n", "ax[1, 1].set_xlabel(\"log defect rate\"); ax[1, 1].set_title(f\"Waterfall: {r0['lot_id']}\")\n", "plt.suptitle(\"QLT02 incoming inspection quality dashboard (synthetic)\"); plt.tight_layout(); plt.show()\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "
\n", "๐Ÿ’ก [์ฐธ๊ณ ] ๋ฏธ์…˜ 06 ์˜ˆ์ƒ ์‹คํ–‰ ๊ฒฐ๊ณผ (Output)\n", "\n", "```text\n", "[ํ’ˆ์งˆ ๋Œ€์‹œ๋ณด๋“œ] ์ตœ๊ทผ 6๊ฐœ์›” ์ˆ˜๋ น๊ฒ€์‚ฌ ํ˜„ํ™ฉ [๊ต์œก์šฉ๊ฐ€์ƒ์ž๋ฃŒ]\n", "์›”๋ณ„ ํ‘œ๋ณธ ๋ถˆ๋Ÿ‰๋ฅ  ๊ด€๋ฆฌํ•œ๊ณ„ ์ดˆ๊ณผ ์›”: [1, 11, 13, 18] (๊ธฐ์ค€ 4.09%)\n", " ๋กœํŠธ ํ‰๊ท ์œ„ํ—˜ ๊ฒฝ๋ณด ์‹ค์ œ๋ถˆ๋Ÿ‰๋ฅ  ์ถ”์„ธ ์ƒํƒœ\n", "vendor \n", "V12 19 72.55 12 0.79 ์œ ์ง€ ๊ณ ์œ„ํ—˜\n", "V10 16 55.41 8 0.69 ์•…ํ™” ๊ณ ์œ„ํ—˜\n", "V11 22 49.74 8 0.59 ์•…ํ™” ๊ด€์ฐฐ\n", "V07 14 28.34 3 0.21 ์œ ์ง€ ๊ด€์ฐฐ\n", "V09 21 26.50 5 0.29 ์œ ์ง€ ๊ด€์ฐฐ\n", "V06 31 21.46 5 0.19 ์œ ์ง€ ์ •์ƒ\n", "\n", "๊ฒฝ๋ณด ๋กœํŠธ 59๊ฑด ์„ค๋ช… ์ƒ์„ฑ: ์ƒ์œ„ 3๊ฐœ ์ธ์ž ์ž๋™ ์‚ฐ์ถœ๋ฅ  100% (๋ชฉํ‘œ 100%: ๋‹ฌ์„ฑ), ๊ฐ€๋ฒ•์„ฑ ์˜ค์ฐจ ์ตœ๋Œ€ 6.7e-16\n", "[์š”์ธ๊ตฐ๋ณ„ ํ‰๊ท  ๊ธฐ์—ฌ๋„(๋กœ๊ทธ ๋ถˆ๋Ÿ‰๋ฅ , ๊ฒฝ๋ณด ๋กœํŠธ)]\n", "ํ‘œ๋ณธ๊ฒ€์‚ฌ ๊ฒฐ๊ณผ 0.615\n", "๋‚ฉ๊ธฐ ์ง€์—ฐยท์ด‰๋ฐ• 0.216\n", "์ •๋ฐ€ ๊ทœ๊ฒฉ 0.165\n", "์—…์ฒด ์ด๋ ฅ 0.116\n", "ํ’ˆ๋ชฉ๊ตฐ 0.053\n", "๊ตฌ๋งค์š”๊ตฌ ์กฐ๊ฑด 0.049\n", "๊ทœ๊ฒฉ ๋ณ€๊ฒฝยท๋ถˆ์ผ์น˜ 0.038\n", "\n", "[์ž๋™ ์„ค๋ช… ์˜ˆ์‹œ]\n", " LOT42-00172 ์œ„ํ—˜ 100: ํ‘œ๋ณธ๊ฒ€์‚ฌ ๊ฒฐ๊ณผ(x3.45), ๋‚ฉ๊ธฐ ์ง€์—ฐยท์ด‰๋ฐ•(x2.22), ์ •๋ฐ€ ๊ทœ๊ฒฉ(x1.33) ์ˆœ์œผ๋กœ ๋ถˆ๋Ÿ‰๋ฅ  ์˜ˆ์ธก์„ ๋†’์˜€๋‹ค -> ์กฐ์น˜: ํ‘œ๋ณธ ํฌ๊ธฐ ํ™•๋Œ€ ์žฌ๊ฒ€์‚ฌ\n", " LOT42-01133 ์œ„ํ—˜ 100: ํ‘œ๋ณธ๊ฒ€์‚ฌ ๊ฒฐ๊ณผ(x3.37), ๋‚ฉ๊ธฐ ์ง€์—ฐยท์ด‰๋ฐ•(x1.84), ์ •๋ฐ€ ๊ทœ๊ฒฉ(x1.28) ์ˆœ์œผ๋กœ ๋ถˆ๋Ÿ‰๋ฅ  ์˜ˆ์ธก์„ ๋†’์˜€๋‹ค -> ์กฐ์น˜: ํ‘œ๋ณธ ํฌ๊ธฐ ํ™•๋Œ€ ์žฌ๊ฒ€์‚ฌ\n", " LOT42-01072 ์œ„ํ—˜ 100: ํ‘œ๋ณธ๊ฒ€์‚ฌ ๊ฒฐ๊ณผ(x2.72), ์—…์ฒด ์ด๋ ฅ(x1.62), ์ •๋ฐ€ ๊ทœ๊ฒฉ(x1.26) ์ˆœ์œผ๋กœ ๋ถˆ๋Ÿ‰๋ฅ  ์˜ˆ์ธก์„ ๋†’์˜€๋‹ค -> ์กฐ์น˜: ํ‘œ๋ณธ ํฌ๊ธฐ ํ™•๋Œ€ ์žฌ๊ฒ€์‚ฌ\n", "ํƒ€๋‹น์„ฑ ์ ๊ฒ€: ๊ทœ๊ฒฉ ๋ถˆ์ผ์น˜ ๊ฒฝ๋ณด ๋กœํŠธ 10๊ฑด ์ค‘ '๊ทœ๊ฒฉ ๋ณ€๊ฒฝยท๋ถˆ์ผ์น˜' ๊ธฐ์—ฌ ์–‘์ˆ˜ 100%\n", "```\n", "
\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "---\n", "\n", "## ๐ŸŽฏ ๋ฏธ์…˜ 07. ํ•ฉ๋ถ€ ํŒ์ • ๊ฒฝ๊ณ„์„  ํ’ˆ๋ชฉ์— ๋Œ€ํ•œ ํ’ˆ์งˆ ๊ฒ€์‚ฌ๊ด€ ์ •๋ฐ€ ์žฌ๊ฒ€์‚ฌ(HITL) ์›Œํฌํ”Œ๋กœ์šฐ ์„ค๊ณ„\n", "**์†Œ์š”์‹œ๊ฐ„**: ์•ฝ 3.5์‹œ๊ฐ„\n", "\n", "### ๐Ÿ“‹ ๋ฏธ์…˜ ๋ฐฐ๊ฒฝ ๋ฐ ์ˆ˜ํ–‰ ์ง€์นจ\n", "#### ๋ฏธ์…˜ ๋ฐฐ๊ฒฝ ๋ฐ ๋‹น์œ„์„ฑ\n", "\n", "ํ•ฉ๋ถ€ ํŒ์ • ๊ฒฝ๊ณ„์„  ํ’ˆ๋ชฉ์— ๋Œ€ํ•œ ๊ฒ€์‚ฌ๊ด€ ์ •๋ฐ€ ์žฌ๊ฒ€์‚ฌ(HITL) ์›Œํฌํ”Œ๋กœ์šฐ๋ฅผ ์„ค๊ณ„ํ•œ๋‹ค. ์‹ค์ œ ์ „์ˆ  ํ˜„์žฅ์—์„œ๋Š” ๋‹จ์ˆœ ํƒ์ง€์— ๊ทธ์น˜์ง€ ์•Š๊ณ  ์‹ ์†ํ•œ ์œ„ํ˜‘ ํŒ๋‹จ๊ณผ ์‹ ๋ขฐ๋„ ๋†’์€ ์˜์‚ฌ๊ฒฐ์ •์ด ํ•„์ˆ˜์ ์ด๋ฏ€๋กœ, **ํ•ฉ๋ถ€ ํŒ์ • ๊ฒฝ๊ณ„์„ ์— ์žˆ๋Š” ๋กœํŠธ๋ฅผ ์ž๋™ ํ™•์ •ํ•˜์ง€ ์•Š๊ณ  ํ’ˆ์งˆ ๊ฒ€์‚ฌ๊ด€ ์ •๋ฐ€ ์žฌ๊ฒ€์‚ฌ๋กœ ๋„˜๊ธฐ๋Š” ์ธ๊ฐ„ ๊ฐœ์ž…(HITL) ์›Œํฌํ”Œ๋กœ์šฐ๋ฅผ ์„ค๊ณ„ํ•˜๊ณ , ํ•œ์ •๋œ ๊ฒ€์‚ฌ ์‹œ๊ฐ„์„ ์ตœ์  ๋ฐฐ๋ถ„ํ•œ๋‹ค.** ๊ณผ์—…์„ ์™„์ˆ˜ํ•˜์—ฌ ์˜คํŒ์„ ๋ฐฉ์ง€ํ•˜๊ณ  ์ž‘์ „ ๋Œ€์‘๋ ฅ์„ ๊ทน๋Œ€ํ™”ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.\n", "\n", "์ด ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด ๋ณธ ๋ฏธ์…˜์—์„œ๋Š” **ํ‘œ๋ณธ ๋ถˆ๋Ÿ‰ ์ˆ˜๊ฐ€ ํ•ฉ๊ฒฉํŒ์ •์ˆ˜ ๊ทผ์ฒ˜์ด๊ฑฐ๋‚˜ ์œ„ํ—˜ ํ™•๋ฅ ์ด ์ค‘๊ฐ„์ธ ๋กœํŠธ๋ฅผ ์žฌ๊ฒ€์‚ฌ ๋Œ€๊ธฐ์—ด์— ๋“ฑ๋ก ์ˆ˜ํ–‰ํ•˜๊ณ  ๊ฒ€์ฆํ•˜๋Š”** ์‹ค์Šต์„ ์ฒด๊ณ„์ ์œผ๋กœ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค.\n", "\n", "ํ•™์Šต์ž๋Š” ์‹ค์Šต๊ฐ€์ด๋“œ์— ์ œ์‹œ๋œ ์ด 4๊ฐœ ์„ธ๋ถ€ ํ™œ๋™(๊ธฐ๋ณธ ๊ธฐ๋Šฅ ๊ตฌํ˜„ โž” ๊ฒฐ๊ณผ ์ง€ํ‘œ ๊ฒ€์ฆ โž” ํŒŒ๋ผ๋ฏธํ„ฐ ํŠœ๋‹ ๋ฐ ๋„์ „๊ณผ์ œ)์„ ์ˆœ์ฐจ์ ์œผ๋กœ ์ง„ํ–‰ํ•˜๋ฉฐ ํ•ต์‹ฌ ํŒ์ • ๊ธฐ์ค€์„ ๋‹ฌ์„ฑํ•ฉ๋‹ˆ๋‹ค.\n", "\n", "**์ด๋ฒˆ ๊ฒฐ๊ณผ๋ฌผ:** `src/optimization/qlt02_simulator.py` ํ˜•ํƒœ์˜ ๊ฒฐ๊ณผ๋ฌผ์„ ์™„์„ฑํ•œ๋‹ค.\n", "\n", "### ๐Ÿ” ํ‰๊ฐ€ ๋ฐ ๊ฒ€์ฆ ๊ธฐ์ค€ (PASS ์กฐ๊ฑด)\n", "#### ๋ฏธ์…˜ ํŒ์ •\n", "\n", "**ํ‰๊ฐ€ ๊ตฌ๋ถ„:** ํ•„์ˆ˜\n", "\n", "**ํ•ต์‹ฌ ํ‰๊ฐ€์˜์—ญ:** ํ˜„์‹ค์  ๊ตฐ์ˆ˜ ์ œ์•ฝ์กฐ๊ฑด ๋ฐ˜์˜๋„ ๋ฐ ์‹œ๋‚˜๋ฆฌ์˜ค๋ณ„ ๋Œ€์•ˆ ์ œ์‹œ ์ ์ ˆ์„ฑ\n", "\n", "#### ๋ฏธ์…˜ PASS ์กฐ๊ฑด\n", "\n", "- ์ •๋Ÿ‰ ๊ธฐ์ค€: ์ตœ์ ํ™” ์ ์šฉ ์‹œ ๋น„์šฉ 15% ์ ˆ๊ฐ ๋˜๋Š” ์žฌ๊ณ  ํšŒ์ „์œจ 20% ํ–ฅ์ƒ\n", "\n", "\n", "- ์ง„ํ–‰ ๊ธฐ์ค€: ์„ ํ˜•/์ •์ˆ˜ ๊ณ„ํš๋ฒ•(LP/MIP) ์ตœ์ ํ™” ํ•ด ๋„์ถœ ๋ฐ ์‹œ๋ฎฌ๋ ˆ์ด์…˜ ์™„๋ฃŒ\n", "\n", "\n", "- ๊ฒ€์ฆ ๋ฐฉ๋ฒ•: ๊ณผ๊ฑฐ ์‹ค์  ๋ฐ์ดํ„ฐ ๋Œ€๋น„ ์ตœ์ ํ™” ๊ฒฐ๊ณผ ๋น„๊ต ๋ฐฑํ…Œ์ŠคํŒ…\n", "\n", "\n", "#### ์ฑ„์ ์ž ํŒ์ • ๋ฉ”๋ชจ\n", "\n", "> ๋‹จ์ˆœ ์ฝ”๋“œ ์‹คํ–‰ ์—ฌ๋ถ€๋ฟ๋งŒ ์•„๋‹ˆ๋ผ ์ถœ๋ ฅ๋œ ์ˆ˜์น˜์™€ ๊ทผ๊ฑฐ ์ž๋ฃŒ๊ฐ€ ๊ตฐ ์‹ค๋ฌด ์š”๊ตฌ์‚ฌํ•ญ์— ๋ถ€ํ•ฉํ•˜๋Š”์ง€ ๋ฉด๋ฐ€ํžˆ ํ™•์ธํ•œ๋‹ค.\n", "\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "\"\"\"์ƒ˜ํ”Œ๋ง ํŒ์ •๊ณผ ์œ„ํ—˜ ์ง€์ˆ˜๊ฐ€ ๊ฒฝ๊ณ„์„ ์— ์žˆ๋Š” ๋กœํŠธ๋ฅผ ์ž๋™ ํ™•์ •ํ•˜์ง€ ์•Š๊ณ  ๊ฒ€์‚ฌ๊ด€ ์ •๋ฐ€ ์žฌ๊ฒ€์‚ฌ ๋Œ€๊ธฐ์—ด๋กœ ๋ณด๋‚ด๋Š” ์ธ๊ฐ„ ๊ฐœ์ž…(HITL) ์›Œํฌํ”Œ๋กœ์šฐ๋ฅผ ์„ค๊ณ„ํ•˜๊ณ , ํ•œ์ •๋œ ๊ฒ€์‚ฌ๊ด€ ์‹œ๊ฐ„์„ ์ •์ˆ˜๊ณ„ํš(MIP)์œผ๋กœ ๋ฐฐ๋ถ„ํ•ด ํ˜„ํ–‰ ์„ ์ฐฉ์ˆœ ๋ฐฉ์‹ ๋Œ€๋น„ ํ’ˆ์งˆ ๋น„์šฉ ์ ˆ๊ฐ๊ณผ ์ˆ˜์š” ๊ธ‰์ฆ ๋ฏผ๊ฐ๋„๋ฅผ ์‹œ๋ฎฌ๋ ˆ์ด์…˜ํ•œ๋‹ค.\"\"\"\n", "import hashlib\n", "import numpy as np\n", "import pandas as pd\n", "import matplotlib.pyplot as plt\n", "from scipy import stats\n", "from scipy.optimize import milp, LinearConstraint, Bounds\n", "from sklearn.ensemble import GradientBoostingRegressor\n", "\n", "# ---------------------------------------------------------------------------\n", "# [๊ต์œก์šฉ๊ฐ€์ƒ์ž๋ฃŒ] ๊ตฐ์ˆ˜ํ’ˆ ์ˆ˜๋ น๊ฒ€์‚ฌ ๋กœํŠธ ๋ฐ์ดํ„ฐ ์ƒ์„ฑ๊ธฐ (์‹ค์ œ ์—…์ฒดยทํ’ˆ๋ชฉยท๊ณ„์•ฝ ์ •๋ณด ์—†์Œ)\n", "# ๊ตฌ๋งค์š”๊ตฌ(๊ทœ๊ฒฉยท๊ธด๊ธ‰ ์—ฌ๋ถ€) -> ๊ณ„์•ฝ(๋‚ฉ๊ธฐ) -> ๋‚ฉํ’ˆ -> ์ˆ˜๋ น๊ฒ€์‚ฌ(ํ‘œ๋ณธ) -> ํ™•์ • ๋ถˆ๋Ÿ‰๋ฅ (์ „์ˆ˜๊ฒ€์‚ฌยท์‚ฌ์šฉ๋ถ€๋Œ€ ํ•˜์ž ๋ฐ˜์˜)\n", "# ---------------------------------------------------------------------------\n", "FSC = {\"5305\": \"screw/bolt\", \"5330\": \"gasket\", \"5935\": \"connector\", \"6135\": \"battery\", \"2530\": \"brake part\", \"8415\": \"clothing\"}\n", "VENDORS = [f\"V{i:02d}\" for i in range(1, 13)]\n", "VENDOR_Q = dict(zip(VENDORS, [-0.6, -0.4, -0.3, -0.2, -0.1, 0.0, 0.1, 0.2, 0.3, 0.5, 0.8, 1.1])) # ์—…์ฒด ๊ณ ์œ  ํ’ˆ์งˆ ์ˆ˜์ค€(์ˆจ์€ ๊ฐ’)\n", "FSC_Q = {\"5305\": -0.3, \"5330\": 0.0, \"5935\": 0.25, \"6135\": 0.1, \"2530\": 0.35, \"8415\": -0.2}\n", "SAMPLE_PLAN = [(500, 50, 3), (1200, 80, 5), (3200, 125, 7), (10 ** 9, 200, 10)] # (๋กœํŠธ ํฌ๊ธฐ ์ƒํ•œ, ํ‘œ๋ณธ n, ํ•ฉ๊ฒฉํŒ์ •์ˆ˜ c) ๋‹จ์ˆœํ™” ๊ธฐ์ค€\n", "BAD_RATE = 0.05 # ํ™•์ • ๋ถˆ๋Ÿ‰๋ฅ  5% ์ดˆ๊ณผ ๋กœํŠธ = ๋ถˆํ•ฉ๊ฒฉ ๋Œ€์ƒ\n", "\n", "\n", "def sample_plan(lot_size):\n", " for up, n, c in SAMPLE_PLAN:\n", " if lot_size <= up:\n", " return n, c\n", "\n", "\n", "def true_rate(d, rng=None):\n", " \"\"\"์ˆจ์€ ๋ถˆ๋Ÿ‰ ๋ฐœ์ƒ ๊ตฌ์กฐ: ์—…์ฒด ์ˆ˜์ค€ + ํ’ˆ๋ชฉ๊ตฐ + ์ •๋ฐ€ ๊ทœ๊ฒฉ + ๊ธด๊ธ‰ ๊ตฌ๋งค + ๊ทœ๊ฒฉ ๋ณ€๊ฒฝยท๋ถˆ์ผ์น˜ + ๋‚ฉ๊ธฐ ์ง€์—ฐ + (์ •๋ฐ€ ๊ทœ๊ฒฉ x ์ด‰๋ฐ• ๋‚ฉ๊ธฐ) ์ƒํ˜ธ์ž‘์šฉ.\"\"\"\n", " short = (d[\"lead_days\"] < 45).astype(float)\n", " z = (-4.35 + d[\"vendor\"].map(VENDOR_Q) + d[\"fsc\"].map(FSC_Q) + 0.55 * (d[\"spec_class\"] == \"precision\")\n", " + 0.35 * d[\"pr_urgent\"] + 0.6 * d[\"spec_changed\"] + 0.8 * d[\"spec_mismatch\"]\n", " + 0.9 * np.log1p(np.maximum(d[\"delay_days\"], 0) / 7) + 0.9 * short * (d[\"spec_class\"] == \"precision\")\n", " + 0.5 * (d[\"month\"] >= 18) * (d[\"vendor\"] == \"V10\")) # V10 ํ›„๋ฐ˜๊ธฐ ํ’ˆ์งˆ ์ €ํ•˜(๋“œ๋ฆฌํ”„ํŠธ)\n", " if rng is not None:\n", " z = z + rng.normal(0, 0.12, len(d))\n", " return np.clip(np.exp(z), 0.002, 0.35)\n", "\n", "\n", "def make_lots(n=1200, seed=42, months=24):\n", " rng = np.random.default_rng(seed)\n", " d = pd.DataFrame({\"lot_id\": [f\"LOT{seed % 100:02d}-{i:05d}\" for i in range(n)]})\n", " d[\"month\"] = rng.integers(0, months, n)\n", " d[\"receipt_date\"] = pd.Timestamp(\"2024-01-01\") + pd.to_timedelta(d[\"month\"] * 30 + rng.integers(0, 30, n), unit=\"D\")\n", " d = d.sort_values(\"receipt_date\").reset_index(drop=True)\n", " d[\"month\"] = ((d[\"receipt_date\"] - pd.Timestamp(\"2024-01-01\")).dt.days // 30).clip(0, months - 1)\n", " d[\"vendor\"] = rng.choice(VENDORS, n, p=np.r_[np.full(6, 0.1), np.full(6, 0.4 / 6)])\n", " d[\"fsc\"] = rng.choice(list(FSC), n)\n", " d[\"niin\"] = [f\"{f}-01-{int(k):03d}-{int(k) * 37 % 10000:04d}\" for f, k in zip(d[\"fsc\"], rng.integers(100, 110, n))]\n", " d[\"spec_class\"] = np.where(rng.random(n) < np.where(d[\"fsc\"].isin([\"5935\", \"2530\"]), 0.6, 0.25), \"precision\", \"general\")\n", " d[\"tolerance_mm\"] = np.where(d[\"spec_class\"] == \"precision\", rng.uniform(0.05, 0.1, n), rng.uniform(0.2, 0.5, n)).round(3)\n", " d[\"spec_rev\"] = rng.integers(1, 6, n)\n", " d[\"pr_urgent\"] = (rng.random(n) < 0.2).astype(int)\n", " d[\"spec_changed\"] = (rng.random(n) < 0.12).astype(int)\n", " d[\"spec_mismatch\"] = (rng.random(n) < 0.08).astype(int)\n", " d[\"pr_qty\"] = rng.integers(200, 5000, n)\n", " d[\"lot_size\"] = (d[\"pr_qty\"] * rng.uniform(0.5, 1.0, n)).astype(int)\n", " d[\"lead_days\"] = np.where(d[\"pr_urgent\"] == 1, rng.integers(20, 60, n), rng.integers(40, 150, n))\n", " d[\"delay_days\"] = np.round(rng.gamma(1.2, 6, n) * rng.binomial(1, 0.45, n) - rng.integers(0, 5, n)).astype(int)\n", " d[\"unit_cost\"] = rng.choice([2_000, 15_000, 40_000, 120_000], n, p=[0.35, 0.3, 0.25, 0.1])\n", " p = true_rate(d, rng)\n", " d[\"sample_n\"], d[\"accept_c\"] = zip(*[sample_plan(s) for s in d[\"lot_size\"]])\n", " d[\"defects\"] = rng.binomial(d[\"sample_n\"], p)\n", " share = np.where(d[\"spec_class\"] == \"precision\", 0.55, 0.3)\n", " d[\"def_dim\"] = rng.binomial(d[\"defects\"], share)\n", " d[\"def_spec\"] = rng.binomial(d[\"defects\"] - d[\"def_dim\"], 0.6)\n", " d[\"def_visual\"] = d[\"defects\"] - d[\"def_dim\"] - d[\"def_spec\"]\n", " d[\"dim_dev_mm\"] = (d[\"tolerance_mm\"] * (0.3 + 2.0 * p) * rng.lognormal(0, 0.3, n)).round(4)\n", " d[\"confirmed_rate\"] = np.clip(p * rng.lognormal(0, 0.05, n), 0.001, 0.4).round(4) # ์ „์ˆ˜๊ฒ€์‚ฌยทํ•˜์ž ๋ฐ˜์˜ ํ™•์ • ๋ถˆ๋Ÿ‰๋ฅ \n", " d[\"field_claims\"] = rng.poisson(d[\"lot_size\"] * p * 0.03)\n", " d[\"inspector\"] = rng.choice([\"INS-1\", \"INS-2\", \"INS-3\", \"INS-4\"], n)\n", " d[\"emergency_release\"] = ((d[\"pr_urgent\"] == 1) & (rng.random(n) < 0.3)).astype(int) # ๊ธด๊ธ‰ ๋ถˆ์ถœ(๊ฒ€์‚ฌ ์ „ ์„ ๋ถˆ์ถœ) ์˜ˆ์™ธ\n", " d[\"bad_lot\"] = (d[\"confirmed_rate\"] > BAD_RATE).astype(int)\n", " return d\n", "# ---------------------------------------------------------------------------\n", "\n", "# ---- ํŒŒ์ƒ ํ”ผ์ฒ˜ ๊ณตํ†ต ํ•จ์ˆ˜ (๋ฏธ์…˜ 4 ์ดํ›„ ๊ณตํ†ต) ----\n", "def add_history(d):\n", " \"\"\"์—…์ฒด๋ณ„ ๊ณผ๊ฑฐ ํ™•์ • ๋ถˆ๋Ÿ‰๋ฅ (ํ˜„์žฌ ๋กœํŠธ ์ด์ „ ๋กœํŠธ๋งŒ ์‚ฌ์šฉ: ์ •๋ณด ๋ˆ„์ˆ˜ ๋ฐฉ์ง€)๊ณผ ์ตœ๊ทผ 5๋กœํŠธ ์ถ”์„ธ.\"\"\"\n", " d = d.sort_values(\"receipt_date\").reset_index(drop=True)\n", " g = d.groupby(\"vendor\")[\"confirmed_rate\"]\n", " d[\"vendor_hist_rate\"] = g.transform(lambda s: s.shift().expanding().mean()).fillna(d[\"confirmed_rate\"].mean())\n", " d[\"vendor_recent_rate\"] = g.transform(lambda s: s.shift().rolling(5, min_periods=1).mean()).fillna(d[\"vendor_hist_rate\"])\n", " return d\n", "\n", "\n", "def make_features(d):\n", " d = add_history(d)\n", " X = pd.DataFrame(index=d.index)\n", " X[\"obs_rate\"] = np.log((d[\"defects\"] + 0.5) / d[\"sample_n\"]) # 1 ํ‘œ๋ณธ ๋ถˆ๋Ÿ‰๋ฅ \n", " X[\"log_lot\"] = np.log(d[\"lot_size\"]) # 2 ๋กœํŠธ ๊ทœ๋ชจ(๋กœ๊ทธ)\n", " X[\"delay_pos\"] = d[\"delay_days\"].clip(lower=0) # 3 ๋‚ฉ๊ธฐ ์ง€์—ฐ์ผ\n", " X[\"delay_ratio\"] = X[\"delay_pos\"] / d[\"lead_days\"] # 4 ์ง€์—ฐ/๊ณ„์•ฝ๊ธฐ๊ฐ„ ๋น„\n", " X[\"short_lead\"] = (d[\"lead_days\"] < 45).astype(int) # 5 ์ด‰๋ฐ• ๋‚ฉ๊ธฐ\n", " X[\"precision\"] = (d[\"spec_class\"] == \"precision\").astype(int) # 6 ์ •๋ฐ€ ๊ทœ๊ฒฉ\n", " X[\"prec_x_short\"] = X[\"precision\"] * X[\"short_lead\"] # 7 ์ •๋ฐ€ x ์ด‰๋ฐ•(์ƒํ˜ธ์ž‘์šฉ)\n", " X[\"spec_risk\"] = d[\"spec_changed\"] + d[\"spec_mismatch\"] # 8 ๊ทœ๊ฒฉ ๋ณ€๊ฒฝยท๋ถˆ์ผ์น˜ ํ•ฉ\n", " X[\"dim_ratio\"] = np.log(d[\"dim_dev_mm\"] / d[\"tolerance_mm\"]) # 9 ์น˜์ˆ˜ ํŽธ์ฐจ/๊ณต์ฐจ\n", " X[\"vendor_hist\"] = np.log(d[\"vendor_hist_rate\"]) # 10 ์—…์ฒด ๊ณผ๊ฑฐ ๋ถˆ๋Ÿ‰๋ฅ \n", " X[\"vendor_trend\"] = d[\"vendor_recent_rate\"] - d[\"vendor_hist_rate\"] # 11 ์—…์ฒด ์ตœ๊ทผ ์ถ”์„ธ\n", " X[\"urgent\"] = d[\"pr_urgent\"] # 12 ๊ธด๊ธ‰ ๊ตฌ๋งค์š”๊ตฌ\n", " X[\"fill_ratio\"] = d[\"lot_size\"] / d[\"pr_qty\"] # 13 ๊ตฌ๋งค์š”๊ตฌ ๋Œ€๋น„ ๋‚ฉํ’ˆ ๋น„์œจ\n", " X[\"dim_share\"] = d[\"def_dim\"] / d[\"defects\"].clip(lower=1) # 14 ์น˜์ˆ˜ ์˜ค์ฐจ ๋น„์ค‘\n", " for f in FSC: # 15~ ํ’ˆ๋ชฉ๊ตฐ ์›-ํ•ซ\n", " X[f\"fsc_{f}\"] = (d[\"fsc\"] == f).astype(int)\n", " return d, X\n", "\n", "# ๊ต์œก์šฉ ๊ฐ€์ • ๋‹จ๊ฐ€(์›): ์‹ค์ œ ๊ณ„์•ฝยท์˜ˆ์‚ฐ ์ž๋ฃŒ ์•„๋‹˜\n", "HOUR_COST = 50_000 # ๊ฒ€์‚ฌ๊ด€ 1์‹œ๊ฐ„ ๋น„์šฉ\n", "MISS_FACTOR = 3.0 # ๋ถˆ๋Ÿ‰ํ’ˆ์ด ๋ถ€๋Œ€๋กœ ๋ถˆ์ถœ๋  ๋•Œ ๋‹จ๊ฐ€ ๋Œ€๋น„ ์ฒ˜๋ฆฌ๋น„(๊ต์ฒดยท์žฌ์ˆ˜์†กยท์ •๋น„ ์†์‹ค) ๋ฐฐ์ˆ˜\n", "REJECT_COST_RATE = 0.03 # ์–‘ํ’ˆ ๋กœํŠธ๋ฅผ ์ž˜๋ชป ๋ฐ˜ํ’ˆํ•  ๋•Œ ๋กœํŠธ ๊ธˆ์•ก ๋Œ€๋น„ ์ง€์—ฐยท์žฌ๋‚ฉํ’ˆ ๋น„์šฉ\n", "CAP_HOURS = 24 # ์›” ์ •๋ฐ€ ์žฌ๊ฒ€์‚ฌ ๊ฐ€์šฉ ์‹œ๊ฐ„(๊ฒ€์‚ฌ๊ด€ 2๋ช… x 12์‹œ๊ฐ„)\n", "\n", "d, X = make_features(make_lots(1200, seed=42))\n", "tr, te = (d[\"month\"] < 18).values, (d[\"month\"] >= 18).values\n", "y = np.log(d[\"confirmed_rate\"].values)\n", "gbm = GradientBoostingRegressor(n_estimators=300, max_depth=2, learning_rate=0.08, subsample=0.8, random_state=0).fit(X.values[tr], y[tr])\n", "sigma = float(np.std(y[tr] - gbm.predict(X.values[tr])) * 1.5)\n", "h = d[te].copy()\n", "h[\"p_bad\"] = 1 - stats.norm.cdf((np.log(BAD_RATE) - gbm.predict(X.values[te])) / sigma)\n", "h[\"accept\"] = h[\"defects\"] <= h[\"accept_c\"]\n", "\n", "\n", "def costs(df):\n", " value = df[\"lot_size\"] * df[\"unit_cost\"]\n", " df[\"c_fa\"] = df[\"lot_size\"] * df[\"confirmed_rate\"] * df[\"unit_cost\"] * MISS_FACTOR # ์˜คํ•ฉ๊ฒฉ ์‹œ ์‹ค์ œ ๋น„์šฉ\n", " df[\"c_fr\"] = value * REJECT_COST_RATE # ์˜ค๋ถˆํ•ฉ๊ฒฉ ์‹œ ์‹ค์ œ ๋น„์šฉ\n", " df[\"hours\"] = np.where(df[\"spec_class\"] == \"precision\", 4.0, 2.0) + df[\"lot_size\"] / 2000\n", " # ์žฌ๊ฒ€์‚ฌ ์ „ ๊ธฐ๋Œ€ ๋น„์šฉ(๋ชจ๋ธ ์ถ”์ •): ํ•ฉ๊ฒฉ ํŒ์ •์ด๋ฉด P(๋ถˆ๋Ÿ‰)x์˜คํ•ฉ๊ฒฉ๋น„, ๋ถˆํ•ฉ๊ฒฉ ํŒ์ •์ด๋ฉด (1-P)x์˜ค๋ถˆํ•ฉ๊ฒฉ๋น„\n", " exp_fa = df[\"p_bad\"] * df[\"lot_size\"] * np.exp(np.log(BAD_RATE) + 0.7) * df[\"unit_cost\"] * MISS_FACTOR\n", " df[\"benefit\"] = np.where(df[\"accept\"], exp_fa, (1 - df[\"p_bad\"]) * df[\"c_fr\"]) - df[\"hours\"] * HOUR_COST\n", " df[\"realized_no\"] = np.where(df[\"accept\"], df[\"bad_lot\"] * df[\"c_fa\"], (1 - df[\"bad_lot\"]) * df[\"c_fr\"])\n", " return df\n", "\n", "\n", "# ---------------- 1) ๊ฒฝ๊ณ„์„  ๋กœํŠธ ์„ ๋ณ„ -> ์žฌ๊ฒ€์‚ฌ ๋Œ€๊ธฐ์—ด(์ž๋™ ํ™•์ • ๊ธˆ์ง€) ----------------\n", "h[\"margin\"] = h[\"defects\"] - h[\"accept_c\"]\n", "h[\"border\"] = (h[\"margin\"].abs() <= 1) | h[\"p_bad\"].between(0.3, 0.8) | (h[\"accept\"] & (h[\"p_bad\"] > 0.5))\n", "q = costs(h[h[\"border\"]].copy())\n", "print(f\"[1] ์ตœ๊ทผ 6๊ฐœ์›” ๋กœํŠธ {len(h)}๊ฑด ์ค‘ ๊ฒฝ๊ณ„์„  {len(q)}๊ฑด -> ๊ฒ€์‚ฌ๊ด€ ์žฌ๊ฒ€์‚ฌ ๋Œ€๊ธฐ์—ด ๋“ฑ๋ก, ์ž๋™ ํ™•์ • 0๊ฑด [๊ต์œก์šฉ๊ฐ€์ƒ์ž๋ฃŒ]\")\n", "print(f\" ์„ ๋ณ„ ๊ธฐ์ค€: |ํ‘œ๋ณธ๋ถˆ๋Ÿ‰-ํ•ฉ๊ฒฉํŒ์ •์ˆ˜|<=1, ์œ„ํ—˜ํ™•๋ฅ  30~80%, ํ•ฉ๊ฒฉ ํŒ์ •์ธ๋ฐ ์œ„ํ—˜ํ™•๋ฅ >50%\")\n", "\n", "\n", "def select(df, policy, cap=CAP_HOURS):\n", " \"\"\"์›”๋ณ„ ๊ฐ€์šฉ ์‹œ๊ฐ„ ์•ˆ์—์„œ ์žฌ๊ฒ€์‚ฌ ๋Œ€์ƒ ์„ ํƒ. fifo=์ž…๊ณ ์ˆœ, mip=๊ธฐ๋Œ€ ์ˆœํŽธ์ต ์ตœ๋Œ€ํ™” 0-1 ์ •์ˆ˜๊ณ„ํš.\"\"\"\n", " pick = np.zeros(len(df), bool)\n", " for mth, g in df.groupby(\"month\"):\n", " idx = np.where(df[\"month\"].values == mth)[0]\n", " if policy == \"fifo\":\n", " used = 0\n", " for i in idx[np.argsort(df[\"receipt_date\"].values[idx])]:\n", " if used + df[\"hours\"].values[i] <= cap:\n", " pick[i] = True; used += df[\"hours\"].values[i]\n", " else:\n", " b, w = df[\"benefit\"].values[idx], df[\"hours\"].values[idx]\n", " res = milp(c=-b, constraints=[LinearConstraint(w[None, :], 0, cap)], integrality=np.ones(len(idx)), bounds=Bounds(0, 1))\n", " pick[idx] = res.x > 0.5\n", " return pick\n", "\n", "\n", "def total_cost(df, pick):\n", " \"\"\"์‹ค์ œ(ํ™•์ •) ๊ฒฐ๊ณผ๋กœ ๊ณ„์‚ฐ: ์žฌ๊ฒ€์‚ฌ ๋กœํŠธ๋Š” ๊ฒ€์‚ฌ๋น„๋งŒ(์ •๋ฐ€ ์žฌ๊ฒ€์‚ฌ๋กœ ์ •๋‹ต ํŒ์ •), ๋‚˜๋จธ์ง€๋Š” ์ƒ˜ํ”Œ๋ง ํŒ์ • ์˜ค๋ฅ˜ ๋น„์šฉ.\"\"\"\n", " return float(np.sum(np.where(pick, df[\"hours\"] * HOUR_COST, df[\"realized_no\"])))\n", "\n", "\n", "res = {}\n", "for pol in (\"none\", \"fifo\", \"mip\"):\n", " pick = np.zeros(len(q), bool) if pol == \"none\" else select(q, pol)\n", " res[pol] = {\"์žฌ๊ฒ€์‚ฌ ๋กœํŠธ\": int(pick.sum()), \"์‚ฌ์šฉ ์‹œ๊ฐ„\": round(float(q[\"hours\"][pick].sum()), 1), \"์ด ํ’ˆ์งˆ๋น„์šฉ(๋ฐฑ๋งŒ์›)\": round(total_cost(q, pick) / 1e6, 1),\n", " \"๋†“์นœ ๋ถˆ๋Ÿ‰ ๋กœํŠธ\": int(((~pick) & q[\"accept\"].values & (q[\"bad_lot\"].values == 1)).sum())}\n", " if pol == \"mip\":\n", " q[\"pick\"] = pick\n", "tab = pd.DataFrame(res).T.astype({\"์žฌ๊ฒ€์‚ฌ ๋กœํŠธ\": int, \"๋†“์นœ ๋ถˆ๋Ÿ‰ ๋กœํŠธ\": int}).rename(index={\"none\": \"์žฌ๊ฒ€์‚ฌ ์—†์Œ\", \"fifo\": \"ํ˜„ํ–‰(์„ ์ฐฉ์ˆœ)\", \"mip\": \"์ตœ์ ํ™”(MIP)\"})\n", "print(\"\\n[2] ์žฌ๊ฒ€์‚ฌ ๋ฐฐ๋ถ„ ๋ฐฉ์‹ ๋น„๊ต (์›” ๊ฐ€์šฉ 24์‹œ๊ฐ„)\")\n", "print(tab.to_string())\n", "cut = 1 - res[\"mip\"][\"์ด ํ’ˆ์งˆ๋น„์šฉ(๋ฐฑ๋งŒ์›)\"] / res[\"fifo\"][\"์ด ํ’ˆ์งˆ๋น„์šฉ(๋ฐฑ๋งŒ์›)\"]\n", "print(f\"ํ˜„ํ–‰ ๋Œ€๋น„ ํ’ˆ์งˆ๋น„์šฉ {cut:.1%} ์ ˆ๊ฐ (๋ชฉํ‘œ 15% ์ด์ƒ: {'๋‹ฌ์„ฑ' if cut >= 0.15 else '๋ฏธ๋‹ฌ'})\")\n", "\n", "# ---------------- 3) ๊ฒ€์‚ฌ๊ด€ ๊ฒฐ์ • ๊ธฐ๋ก(HITL) ----------------\n", "log, prev, n_block = [], \"GENESIS\", 0\n", "INSP = [\"INS-1\", \"INS-2\", \"INS-3\", \"INS-4\"]\n", "for i, r in enumerate(q[q[\"pick\"]].head(12).itertuples()):\n", " who = INSP[(INSP.index(r.inspector) + 1 + i) % 4]\n", " if who == r.inspector: # ์ตœ์ดˆ ๊ฒ€์‚ฌ์ž๋Š” ์žฌ๊ฒ€์‚ฌ ๋ถˆ๊ฐ€(์ดํ•ด์ถฉ๋Œ)\n", " n_block += 1; who = INSP[(INSP.index(who) + 1) % 4]\n", " verdict = \"๋ถˆํ•ฉ๊ฒฉ\" if r.bad_lot else \"ํ•ฉ๊ฒฉ\"\n", " reason = f\"์ „์ˆ˜ ์ธก์ • ํ™•์ •๋ถˆ๋Ÿ‰๋ฅ  {r.confirmed_rate:.1%}, ์น˜์ˆ˜ํŽธ์ฐจ {r.dim_dev_mm}mm\"\n", " rec = f\"{r.lot_id}|{who}|{verdict}|{reason}|{prev}\"\n", " prev = hashlib.sha256(rec.encode()).hexdigest()[:12]\n", " log.append({\"๋กœํŠธ\": r.lot_id, \"์ƒ˜ํ”Œ๋งํŒ์ •\": \"ํ•ฉ๊ฒฉ\" if r.accept else \"๋ถˆํ•ฉ๊ฒฉ\", \"์œ„ํ—˜ํ™•๋ฅ \": round(r.p_bad, 2), \"์žฌ๊ฒ€์‚ฌ๊ด€\": who, \"์ตœ์ข…\": verdict, \"๊ทผ๊ฑฐ\": reason, \"hash\": prev})\n", "L = pd.DataFrame(log)\n", "flip = (L[\"์ƒ˜ํ”Œ๋งํŒ์ •\"] != L[\"์ตœ์ข…\"]).sum()\n", "print(f\"\\n[3] ๊ฒ€์‚ฌ๊ด€ ์žฌ๊ฒ€์‚ฌ ๊ธฐ๋ก ์˜ˆ์‹œ(12๊ฑด ์ค‘ ํŒ์ • ๋ฒˆ๋ณต {flip}๊ฑด, ๊ทผ๊ฑฐ ๋ฏธ๊ธฐ์žฌ ๊ฒฐ์ • 0๊ฑด, ์ตœ์ดˆ ๊ฒ€์‚ฌ์ž ๋ฐฐ์ • ์ฐจ๋‹จ {n_block}๊ฑด)\")\n", "print(L.head(5)[[\"๋กœํŠธ\", \"์ƒ˜ํ”Œ๋งํŒ์ •\", \"์œ„ํ—˜ํ™•๋ฅ \", \"์žฌ๊ฒ€์‚ฌ๊ด€\", \"์ตœ์ข…\", \"๊ทผ๊ฑฐ\"]].to_string(index=False))\n", "\n", "# ---------------- 4) ๋ฏผ๊ฐ๋„: ์ „์‹œ ์ „ํ™˜ ๋“ฑ ๋Œ๋ฐœ ์ˆ˜์š” ----------------\n", "print(\"\\n[4] ๋ฏผ๊ฐ๋„ ๋ถ„์„ (๊ฒฝ๊ณ„์„  ๋กœํŠธ ์ฆ๊ฐ€, ๊ฒ€์‚ฌ ๊ฐ€์šฉ ์‹œ๊ฐ„ ๋ณ€ํ™”)\")\n", "rows = []\n", "for surge, capf in [(1.0, 1.0), (1.5, 1.0), (2.0, 1.0), (2.0, 0.7)]:\n", " qq = q.sample(frac=surge, replace=surge > 1, random_state=7).reset_index(drop=True)\n", " cf, cm = [total_cost(qq, select(qq, p, CAP_HOURS * capf)) for p in (\"fifo\", \"mip\")]\n", " rows.append({\"๋ฌผ๋Ÿ‰ ๋ฐฐ์ˆ˜\": surge, \"๊ฐ€์šฉ์‹œ๊ฐ„\": f\"{capf:.0%}\", \"์„ ์ฐฉ์ˆœ(๋ฐฑ๋งŒ์›)\": round(cf / 1e6, 1), \"MIP(๋ฐฑ๋งŒ์›)\": round(cm / 1e6, 1), \"์ ˆ๊ฐ์•ก(๋ฐฑ๋งŒ์›)\": round((cf - cm) / 1e6, 1), \"์ ˆ๊ฐ๋ฅ \": f\"{1 - cm / cf:.1%}\"})\n", "sens = pd.DataFrame(rows)\n", "print(sens.to_string(index=False))\n", "print(\"๋ฌผ๋Ÿ‰์ด ๋Š˜๋ฉด ์ ˆ๊ฐ๋ฅ ์€ ๋‚ฎ์•„์ง€์ง€๋งŒ ์ ˆ๊ฐ์•ก์€ ์ปค์ง„๋‹ค. ๊ฐ€์šฉ ์‹œ๊ฐ„์ด ๋ถ€์กฑํ• ์ˆ˜๋ก ๊ธฐ๋Œ€ ์†์‹ค์ด ํฐ ๋กœํŠธ๋ถ€ํ„ฐ ์žฌ๊ฒ€์‚ฌํ•ด์•ผ ํ•œ๋‹ค.\")\n", "print(\"์ตœ์ข… ํ•ฉ๋ถ€ ํŒ์ •์€ ๊ฒ€์‚ฌ๊ด€์ด ๋‚ด๋ฆฌ๊ณ , ๋ชจ๋ธ์€ ์žฌ๊ฒ€์‚ฌ ์ˆœ์„œ๋งŒ ์ œ์•ˆํ•œ๋‹ค.\")\n", "\n", "fig, ax = plt.subplots(1, 3, figsize=(15, 4))\n", "ax[0].bar([\"none\", \"FIFO\", \"MIP\"], tab[\"์ด ํ’ˆ์งˆ๋น„์šฉ(๋ฐฑ๋งŒ์›)\"], color=[\"gray\", \"tab:orange\", \"tab:green\"]); ax[0].set_ylabel(\"quality cost (M KRW)\"); ax[0].set_title(\"Re-inspection allocation\")\n", "ax[1].scatter(q[\"hours\"], q[\"benefit\"] / 1e6, c=q[\"pick\"].map({True: \"tab:green\", False: \"lightgray\"}), s=14)\n", "ax[1].set_xlabel(\"re-inspection hours\"); ax[1].set_ylabel(\"expected net benefit (M KRW)\"); ax[1].set_title(\"MIP selection (green = selected)\")\n", "ax[2].plot(range(len(sens)), [float(s[:-1]) for s in sens[\"์ ˆ๊ฐ๋ฅ \"]], \"o-\"); ax[2].set_xticks(range(len(sens)))\n", "ax[2].set_xticklabels([f\"x{a} / {b}\" for a, b in zip(sens[\"๋ฌผ๋Ÿ‰ ๋ฐฐ์ˆ˜\"], sens[\"๊ฐ€์šฉ์‹œ๊ฐ„\"])]); ax[2].set_ylabel(\"saving vs FIFO (%)\"); ax[2].set_title(\"Sensitivity: surge / capacity\")\n", "plt.tight_layout(); plt.show()\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "
\n", "๐Ÿ’ก [์ฐธ๊ณ ] ๋ฏธ์…˜ 07 ์˜ˆ์ƒ ์‹คํ–‰ ๊ฒฐ๊ณผ (Output)\n", "\n", "```text\n", "[1] ์ตœ๊ทผ 6๊ฐœ์›” ๋กœํŠธ 304๊ฑด ์ค‘ ๊ฒฝ๊ณ„์„  57๊ฑด -> ๊ฒ€์‚ฌ๊ด€ ์žฌ๊ฒ€์‚ฌ ๋Œ€๊ธฐ์—ด ๋“ฑ๋ก, ์ž๋™ ํ™•์ • 0๊ฑด [๊ต์œก์šฉ๊ฐ€์ƒ์ž๋ฃŒ]\n", " ์„ ๋ณ„ ๊ธฐ์ค€: |ํ‘œ๋ณธ๋ถˆ๋Ÿ‰-ํ•ฉ๊ฒฉํŒ์ •์ˆ˜|<=1, ์œ„ํ—˜ํ™•๋ฅ  30~80%, ํ•ฉ๊ฒฉ ํŒ์ •์ธ๋ฐ ์œ„ํ—˜ํ™•๋ฅ >50%\n", "\n", "[2] ์žฌ๊ฒ€์‚ฌ ๋ฐฐ๋ถ„ ๋ฐฉ์‹ ๋น„๊ต (์›” ๊ฐ€์šฉ 24์‹œ๊ฐ„)\n", " ์žฌ๊ฒ€์‚ฌ ๋กœํŠธ ์‚ฌ์šฉ ์‹œ๊ฐ„ ์ด ํ’ˆ์งˆ๋น„์šฉ(๋ฐฑ๋งŒ์›) ๋†“์นœ ๋ถˆ๋Ÿ‰ ๋กœํŠธ\n", "์žฌ๊ฒ€์‚ฌ ์—†์Œ 0 0.0 57.5 13\n", "ํ˜„ํ–‰(์„ ์ฐฉ์ˆœ) 35 137.7 25.7 5\n", "์ตœ์ ํ™”(MIP) 32 121.3 8.8 3\n", "ํ˜„ํ–‰ ๋Œ€๋น„ ํ’ˆ์งˆ๋น„์šฉ 65.8% ์ ˆ๊ฐ (๋ชฉํ‘œ 15% ์ด์ƒ: ๋‹ฌ์„ฑ)\n", "\n", "[3] ๊ฒ€์‚ฌ๊ด€ ์žฌ๊ฒ€์‚ฌ ๊ธฐ๋ก ์˜ˆ์‹œ(12๊ฑด ์ค‘ ํŒ์ • ๋ฒˆ๋ณต 6๊ฑด, ๊ทผ๊ฑฐ ๋ฏธ๊ธฐ์žฌ ๊ฒฐ์ • 0๊ฑด, ์ตœ์ดˆ ๊ฒ€์‚ฌ์ž ๋ฐฐ์ • ์ฐจ๋‹จ 3๊ฑด)\n", " ๋กœํŠธ ์ƒ˜ํ”Œ๋งํŒ์ • ์œ„ํ—˜ํ™•๋ฅ  ์žฌ๊ฒ€์‚ฌ๊ด€ ์ตœ์ข… ๊ทผ๊ฑฐ\n", "LOT42-00127 ํ•ฉ๊ฒฉ 0.89 INS-2 ๋ถˆํ•ฉ๊ฒฉ ์ „์ˆ˜ ์ธก์ • ํ™•์ •๋ถˆ๋Ÿ‰๋ฅ  5.7%, ์น˜์ˆ˜ํŽธ์ฐจ 0.0523mm\n", "LOT42-00597 ํ•ฉ๊ฒฉ 0.11 INS-2 ํ•ฉ๊ฒฉ ์ „์ˆ˜ ์ธก์ • ํ™•์ •๋ถˆ๋Ÿ‰๋ฅ  3.3%, ์น˜์ˆ˜ํŽธ์ฐจ 0.1563mm\n", "LOT42-00159 ํ•ฉ๊ฒฉ 0.18 INS-1 ํ•ฉ๊ฒฉ ์ „์ˆ˜ ์ธก์ • ํ™•์ •๋ถˆ๋Ÿ‰๋ฅ  2.8%, ์น˜์ˆ˜ํŽธ์ฐจ 0.1883mm\n", "LOT42-01097 ํ•ฉ๊ฒฉ 0.08 INS-2 ํ•ฉ๊ฒฉ ์ „์ˆ˜ ์ธก์ • ํ™•์ •๋ถˆ๋Ÿ‰๋ฅ  2.6%, ์น˜์ˆ˜ํŽธ์ฐจ 0.0287mm\n", "LOT42-00246 ํ•ฉ๊ฒฉ 0.33 INS-2 ๋ถˆํ•ฉ๊ฒฉ ์ „์ˆ˜ ์ธก์ • ํ™•์ •๋ถˆ๋Ÿ‰๋ฅ  6.7%, ์น˜์ˆ˜ํŽธ์ฐจ 0.21mm\n", "\n", "[4] ๋ฏผ๊ฐ๋„ ๋ถ„์„ (๊ฒฝ๊ณ„์„  ๋กœํŠธ ์ฆ๊ฐ€, ๊ฒ€์‚ฌ ๊ฐ€์šฉ ์‹œ๊ฐ„ ๋ณ€ํ™”)\n", " ๋ฌผ๋Ÿ‰ ๋ฐฐ์ˆ˜ ๊ฐ€์šฉ์‹œ๊ฐ„ ์„ ์ฐฉ์ˆœ(๋ฐฑ๋งŒ์›) MIP(๋ฐฑ๋งŒ์›) ์ ˆ๊ฐ์•ก(๋ฐฑ๋งŒ์›) ์ ˆ๊ฐ๋ฅ \n", " 1.0 100% 25.7 8.8 16.9 65.9%\n", " 1.5 100% 92.1 51.7 40.5 43.9%\n", " 2.0 100% 123.6 77.9 45.7 36.9%\n", " 2.0 70% 145.9 100.5 45.4 31.1%\n", "๋ฌผ๋Ÿ‰์ด ๋Š˜๋ฉด ์ ˆ๊ฐ๋ฅ ์€ ๋‚ฎ์•„์ง€์ง€๋งŒ ์ ˆ๊ฐ์•ก์€ ์ปค์ง„๋‹ค. ๊ฐ€์šฉ ์‹œ๊ฐ„์ด ๋ถ€์กฑํ• ์ˆ˜๋ก ๊ธฐ๋Œ€ ์†์‹ค์ด ํฐ ๋กœํŠธ๋ถ€ํ„ฐ ์žฌ๊ฒ€์‚ฌํ•ด์•ผ ํ•œ๋‹ค.\n", "์ตœ์ข… ํ•ฉ๋ถ€ ํŒ์ •์€ ๊ฒ€์‚ฌ๊ด€์ด ๋‚ด๋ฆฌ๊ณ , ๋ชจ๋ธ์€ ์žฌ๊ฒ€์‚ฌ ์ˆœ์„œ๋งŒ ์ œ์•ˆํ•œ๋‹ค.\n", "```\n", "
\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "---\n", "\n", "## ๐ŸŽฏ ๋ฏธ์…˜ 08. ๋ถˆ๋Ÿ‰ ๋ถ€ํ’ˆ ๋Œ€๋Ÿ‰ ๋‚ฉํ’ˆ ๊ฐ€์ƒ ์ฃผ์ž… ์‹œ ์กฐ๊ธฐ๊ฒฝ๋ณด ์‹ ์†์„ฑ ์ŠคํŠธ๋ ˆ์Šค ํ…Œ์ŠคํŠธ\n", "**์†Œ์š”์‹œ๊ฐ„**: ์•ฝ 3.0์‹œ๊ฐ„\n", "\n", "### ๐Ÿ“‹ ๋ฏธ์…˜ ๋ฐฐ๊ฒฝ ๋ฐ ์ˆ˜ํ–‰ ์ง€์นจ\n", "#### ๋ฏธ์…˜ ๋ฐฐ๊ฒฝ ๋ฐ ๋‹น์œ„์„ฑ\n", "\n", "๋ถˆ๋Ÿ‰ ๋ถ€ํ’ˆ ๋Œ€๋Ÿ‰ ๋‚ฉํ’ˆ ๊ฐ€์ƒ ์ฃผ์ž… ์‹œ ์กฐ๊ธฐ๊ฒฝ๋ณด ์ŠคํŠธ๋ ˆ์Šค ํ…Œ์ŠคํŠธ๋ฅผ ์ˆ˜ํ–‰ํ•œ๋‹ค. ์‹ค์ œ ์ „์ˆ  ํ˜„์žฅ์—์„œ๋Š” ๋‹จ์ˆœ ํƒ์ง€์— ๊ทธ์น˜์ง€ ์•Š๊ณ  ์‹ ์†ํ•œ ์œ„ํ˜‘ ํŒ๋‹จ๊ณผ ์‹ ๋ขฐ๋„ ๋†’์€ ์˜์‚ฌ๊ฒฐ์ •์ด ํ•„์ˆ˜์ ์ด๋ฏ€๋กœ, **ํ‰์†Œ ํ’ˆ์งˆ์ด ์ข‹๋˜ ์—…์ฒด๊ฐ€ ๋ถˆ๋Ÿ‰ ๋ถ€ํ’ˆ์„ ๋Œ€๋Ÿ‰ ๋‚ฉํ’ˆํ•˜๋Š” ์ƒํ™ฉ์„ ๊ฐ€์ƒ์œผ๋กœ ์ฃผ์ž…ํ•ด ์กฐ๊ธฐ๊ฒฝ๋ณด์˜ ์ •ํ™•๋„์™€ ์‹ ์†์„ฑ, ๋Œ€์‹œ๋ณด๋“œ ๊ฐฑ์‹  ์„ฑ๋Šฅ์„ ์‹œํ—˜ํ•œ๋‹ค.** ๊ณผ์—…์„ ์™„์ˆ˜ํ•˜์—ฌ ์˜คํŒ์„ ๋ฐฉ์ง€ํ•˜๊ณ  ์ž‘์ „ ๋Œ€์‘๋ ฅ์„ ๊ทน๋Œ€ํ™”ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.\n", "\n", "์ด ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด ๋ณธ ๋ฏธ์…˜์—์„œ๋Š” **์šฐ์ˆ˜ ์—…์ฒด V03์— ๋ถˆ๋Ÿ‰ ๋กœํŠธ ์œ„์ฃผ์˜ ๋Œ€๋Ÿ‰ ๋‚ฉํ’ˆ 60๋กœํŠธ์™€ ์†์ƒ ๋ ˆ์ฝ”๋“œ 12๊ฑด ์ฃผ์ž…ํ•˜์—ฌ ์ฒ˜๋ฆฌ ์ง€์—ฐ ๋ฐ ๋ณต์›๋ ฅ์„ ์‹œํ—˜ํ•˜๋Š”** ์‹ค์Šต์„ ์ฒด๊ณ„์ ์œผ๋กœ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค.\n", "\n", "ํ•™์Šต์ž๋Š” ์‹ค์Šต๊ฐ€์ด๋“œ์— ์ œ์‹œ๋œ ์ด 4๊ฐœ ์„ธ๋ถ€ ํ™œ๋™(๊ธฐ๋ณธ ๊ธฐ๋Šฅ ๊ตฌํ˜„ โž” ๊ฒฐ๊ณผ ์ง€ํ‘œ ๊ฒ€์ฆ โž” ํŒŒ๋ผ๋ฏธํ„ฐ ํŠœ๋‹ ๋ฐ ๋„์ „๊ณผ์ œ)์„ ์ˆœ์ฐจ์ ์œผ๋กœ ์ง„ํ–‰ํ•˜๋ฉฐ ํ•ต์‹ฌ ํŒ์ • ๊ธฐ์ค€์„ ๋‹ฌ์„ฑํ•ฉ๋‹ˆ๋‹ค.\n", "\n", "**์ด๋ฒˆ ๊ฒฐ๊ณผ๋ฌผ:** `src/dashboard/qlt02_app.py (Streamlit/Dash ๋“ฑ)` ํ˜•ํƒœ์˜ ๊ฒฐ๊ณผ๋ฌผ์„ ์™„์„ฑํ•œ๋‹ค.\n", "\n", "### ๐Ÿ” ํ‰๊ฐ€ ๋ฐ ๊ฒ€์ฆ ๊ธฐ์ค€ (PASS ์กฐ๊ฑด)\n", "#### ๋ฏธ์…˜ ํŒ์ •\n", "\n", "**ํ‰๊ฐ€ ๊ตฌ๋ถ„:** ํ•„์ˆ˜\n", "\n", "**ํ•ต์‹ฌ ํ‰๊ฐ€์˜์—ญ:** ์˜์‚ฌ๊ฒฐ์ • ์ง€์›์— ์ตœ์ ํ™”๋œ ์‹œ๊ฐ์  ์œ„๊ณ„ ๋ฐ ์‚ฌ์šฉ์ž ๊ฒฝํ—˜(UX)\n", "\n", "#### ๋ฏธ์…˜ PASS ์กฐ๊ฑด\n", "\n", "- ์ •๋Ÿ‰ ๊ธฐ์ค€: ๋Œ€์‹œ๋ณด๋“œ ๋กœ๋”ฉ ์‹œ๊ฐ„ 2์ดˆ ์ด๋‚ด, ๊ฒฝ๋ณด ๋ฐœ๋ น ์ •ํ™•๋„ 95% ์ด์ƒ\n", "\n", "\n", "- ์ง„ํ–‰ ๊ธฐ์ค€: ์›น ๊ธฐ๋ฐ˜ ๋ถ„์„ ๋Œ€์‹œ๋ณด๋“œ ๊ตฌ๋™ ๋ฐ ์‹ค์‹œ๊ฐ„ ํ•„ํ„ฐ๋ง ๊ธฐ๋Šฅ ๊ฒ€์ฆ\n", "\n", "\n", "- ๊ฒ€์ฆ ๋ฐฉ๋ฒ•: ์‹ค์ œ ๊ตฐ์ˆ˜ ๋ฐ์ดํ„ฐ ์—ฐ๋™ ํ™”๋ฉด ํ…Œ์ŠคํŠธ ๋ฐ UI ๋™์ž‘ ๊ฒ€์ฆ\n", "\n", "\n", "#### ์ฑ„์ ์ž ํŒ์ • ๋ฉ”๋ชจ\n", "\n", "> ๋‹จ์ˆœ ์ฝ”๋“œ ์‹คํ–‰ ์—ฌ๋ถ€๋ฟ๋งŒ ์•„๋‹ˆ๋ผ ์ถœ๋ ฅ๋œ ์ˆ˜์น˜์™€ ๊ทผ๊ฑฐ ์ž๋ฃŒ๊ฐ€ ๊ตฐ ์‹ค๋ฌด ์š”๊ตฌ์‚ฌํ•ญ์— ๋ถ€ํ•ฉํ•˜๋Š”์ง€ ๋ฉด๋ฐ€ํžˆ ํ™•์ธํ•œ๋‹ค.\n", "\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "\"\"\"ํ‰์†Œ ํ’ˆ์งˆ์ด ์ข‹๋˜ ๊ฐ€์ƒ ์—…์ฒด๊ฐ€ ๋ถˆ๋Ÿ‰ ๋กœํŠธ๋ฅผ ๋Œ€๋Ÿ‰ ๋‚ฉํ’ˆํ•˜๋Š” ์ƒํ™ฉ๊ณผ ์†์ƒ ๋ ˆ์ฝ”๋“œ๋ฅผ ์ฃผ์ž…ํ•ด, ๋กœํŠธ ๊ฒฝ๋ณด ์ •ํ™•๋„ยท์—…์ฒด ๋‹จ์œ„ ๋ˆ„์ ํ•ฉ(CUSUM) ๊ฒฝ๋ณด์˜ ์‹ ์†์„ฑยท๋Œ€์‹œ๋ณด๋“œ ๊ฐฑ์‹  ์‹œ๊ฐ„(๋ฌผ๋Ÿ‰ 1~50๋ฐฐ)์„ ์ธก์ •ํ•˜๋Š” ์กฐ๊ธฐ๊ฒฝ๋ณด ์ŠคํŠธ๋ ˆ์Šค ํ…Œ์ŠคํŠธ๋ฅผ ์ˆ˜ํ–‰ํ•œ๋‹ค.\"\"\"\n", "import time\n", "import numpy as np\n", "import pandas as pd\n", "import matplotlib.pyplot as plt\n", "from scipy import stats\n", "from sklearn.ensemble import GradientBoostingRegressor\n", "\n", "# ---------------------------------------------------------------------------\n", "# [๊ต์œก์šฉ๊ฐ€์ƒ์ž๋ฃŒ] ๊ตฐ์ˆ˜ํ’ˆ ์ˆ˜๋ น๊ฒ€์‚ฌ ๋กœํŠธ ๋ฐ์ดํ„ฐ ์ƒ์„ฑ๊ธฐ (์‹ค์ œ ์—…์ฒดยทํ’ˆ๋ชฉยท๊ณ„์•ฝ ์ •๋ณด ์—†์Œ)\n", "# ๊ตฌ๋งค์š”๊ตฌ(๊ทœ๊ฒฉยท๊ธด๊ธ‰ ์—ฌ๋ถ€) -> ๊ณ„์•ฝ(๋‚ฉ๊ธฐ) -> ๋‚ฉํ’ˆ -> ์ˆ˜๋ น๊ฒ€์‚ฌ(ํ‘œ๋ณธ) -> ํ™•์ • ๋ถˆ๋Ÿ‰๋ฅ (์ „์ˆ˜๊ฒ€์‚ฌยท์‚ฌ์šฉ๋ถ€๋Œ€ ํ•˜์ž ๋ฐ˜์˜)\n", "# ---------------------------------------------------------------------------\n", "FSC = {\"5305\": \"screw/bolt\", \"5330\": \"gasket\", \"5935\": \"connector\", \"6135\": \"battery\", \"2530\": \"brake part\", \"8415\": \"clothing\"}\n", "VENDORS = [f\"V{i:02d}\" for i in range(1, 13)]\n", "VENDOR_Q = dict(zip(VENDORS, [-0.6, -0.4, -0.3, -0.2, -0.1, 0.0, 0.1, 0.2, 0.3, 0.5, 0.8, 1.1])) # ์—…์ฒด ๊ณ ์œ  ํ’ˆ์งˆ ์ˆ˜์ค€(์ˆจ์€ ๊ฐ’)\n", "FSC_Q = {\"5305\": -0.3, \"5330\": 0.0, \"5935\": 0.25, \"6135\": 0.1, \"2530\": 0.35, \"8415\": -0.2}\n", "SAMPLE_PLAN = [(500, 50, 3), (1200, 80, 5), (3200, 125, 7), (10 ** 9, 200, 10)] # (๋กœํŠธ ํฌ๊ธฐ ์ƒํ•œ, ํ‘œ๋ณธ n, ํ•ฉ๊ฒฉํŒ์ •์ˆ˜ c) ๋‹จ์ˆœํ™” ๊ธฐ์ค€\n", "BAD_RATE = 0.05 # ํ™•์ • ๋ถˆ๋Ÿ‰๋ฅ  5% ์ดˆ๊ณผ ๋กœํŠธ = ๋ถˆํ•ฉ๊ฒฉ ๋Œ€์ƒ\n", "\n", "\n", "def sample_plan(lot_size):\n", " for up, n, c in SAMPLE_PLAN:\n", " if lot_size <= up:\n", " return n, c\n", "\n", "\n", "def true_rate(d, rng=None):\n", " \"\"\"์ˆจ์€ ๋ถˆ๋Ÿ‰ ๋ฐœ์ƒ ๊ตฌ์กฐ: ์—…์ฒด ์ˆ˜์ค€ + ํ’ˆ๋ชฉ๊ตฐ + ์ •๋ฐ€ ๊ทœ๊ฒฉ + ๊ธด๊ธ‰ ๊ตฌ๋งค + ๊ทœ๊ฒฉ ๋ณ€๊ฒฝยท๋ถˆ์ผ์น˜ + ๋‚ฉ๊ธฐ ์ง€์—ฐ + (์ •๋ฐ€ ๊ทœ๊ฒฉ x ์ด‰๋ฐ• ๋‚ฉ๊ธฐ) ์ƒํ˜ธ์ž‘์šฉ.\"\"\"\n", " short = (d[\"lead_days\"] < 45).astype(float)\n", " z = (-4.35 + d[\"vendor\"].map(VENDOR_Q) + d[\"fsc\"].map(FSC_Q) + 0.55 * (d[\"spec_class\"] == \"precision\")\n", " + 0.35 * d[\"pr_urgent\"] + 0.6 * d[\"spec_changed\"] + 0.8 * d[\"spec_mismatch\"]\n", " + 0.9 * np.log1p(np.maximum(d[\"delay_days\"], 0) / 7) + 0.9 * short * (d[\"spec_class\"] == \"precision\")\n", " + 0.5 * (d[\"month\"] >= 18) * (d[\"vendor\"] == \"V10\")) # V10 ํ›„๋ฐ˜๊ธฐ ํ’ˆ์งˆ ์ €ํ•˜(๋“œ๋ฆฌํ”„ํŠธ)\n", " if rng is not None:\n", " z = z + rng.normal(0, 0.12, len(d))\n", " return np.clip(np.exp(z), 0.002, 0.35)\n", "\n", "\n", "def make_lots(n=1200, seed=42, months=24):\n", " rng = np.random.default_rng(seed)\n", " d = pd.DataFrame({\"lot_id\": [f\"LOT{seed % 100:02d}-{i:05d}\" for i in range(n)]})\n", " d[\"month\"] = rng.integers(0, months, n)\n", " d[\"receipt_date\"] = pd.Timestamp(\"2024-01-01\") + pd.to_timedelta(d[\"month\"] * 30 + rng.integers(0, 30, n), unit=\"D\")\n", " d = d.sort_values(\"receipt_date\").reset_index(drop=True)\n", " d[\"month\"] = ((d[\"receipt_date\"] - pd.Timestamp(\"2024-01-01\")).dt.days // 30).clip(0, months - 1)\n", " d[\"vendor\"] = rng.choice(VENDORS, n, p=np.r_[np.full(6, 0.1), np.full(6, 0.4 / 6)])\n", " d[\"fsc\"] = rng.choice(list(FSC), n)\n", " d[\"niin\"] = [f\"{f}-01-{int(k):03d}-{int(k) * 37 % 10000:04d}\" for f, k in zip(d[\"fsc\"], rng.integers(100, 110, n))]\n", " d[\"spec_class\"] = np.where(rng.random(n) < np.where(d[\"fsc\"].isin([\"5935\", \"2530\"]), 0.6, 0.25), \"precision\", \"general\")\n", " d[\"tolerance_mm\"] = np.where(d[\"spec_class\"] == \"precision\", rng.uniform(0.05, 0.1, n), rng.uniform(0.2, 0.5, n)).round(3)\n", " d[\"spec_rev\"] = rng.integers(1, 6, n)\n", " d[\"pr_urgent\"] = (rng.random(n) < 0.2).astype(int)\n", " d[\"spec_changed\"] = (rng.random(n) < 0.12).astype(int)\n", " d[\"spec_mismatch\"] = (rng.random(n) < 0.08).astype(int)\n", " d[\"pr_qty\"] = rng.integers(200, 5000, n)\n", " d[\"lot_size\"] = (d[\"pr_qty\"] * rng.uniform(0.5, 1.0, n)).astype(int)\n", " d[\"lead_days\"] = np.where(d[\"pr_urgent\"] == 1, rng.integers(20, 60, n), rng.integers(40, 150, n))\n", " d[\"delay_days\"] = np.round(rng.gamma(1.2, 6, n) * rng.binomial(1, 0.45, n) - rng.integers(0, 5, n)).astype(int)\n", " d[\"unit_cost\"] = rng.choice([2_000, 15_000, 40_000, 120_000], n, p=[0.35, 0.3, 0.25, 0.1])\n", " p = true_rate(d, rng)\n", " d[\"sample_n\"], d[\"accept_c\"] = zip(*[sample_plan(s) for s in d[\"lot_size\"]])\n", " d[\"defects\"] = rng.binomial(d[\"sample_n\"], p)\n", " share = np.where(d[\"spec_class\"] == \"precision\", 0.55, 0.3)\n", " d[\"def_dim\"] = rng.binomial(d[\"defects\"], share)\n", " d[\"def_spec\"] = rng.binomial(d[\"defects\"] - d[\"def_dim\"], 0.6)\n", " d[\"def_visual\"] = d[\"defects\"] - d[\"def_dim\"] - d[\"def_spec\"]\n", " d[\"dim_dev_mm\"] = (d[\"tolerance_mm\"] * (0.3 + 2.0 * p) * rng.lognormal(0, 0.3, n)).round(4)\n", " d[\"confirmed_rate\"] = np.clip(p * rng.lognormal(0, 0.05, n), 0.001, 0.4).round(4) # ์ „์ˆ˜๊ฒ€์‚ฌยทํ•˜์ž ๋ฐ˜์˜ ํ™•์ • ๋ถˆ๋Ÿ‰๋ฅ \n", " d[\"field_claims\"] = rng.poisson(d[\"lot_size\"] * p * 0.03)\n", " d[\"inspector\"] = rng.choice([\"INS-1\", \"INS-2\", \"INS-3\", \"INS-4\"], n)\n", " d[\"emergency_release\"] = ((d[\"pr_urgent\"] == 1) & (rng.random(n) < 0.3)).astype(int) # ๊ธด๊ธ‰ ๋ถˆ์ถœ(๊ฒ€์‚ฌ ์ „ ์„ ๋ถˆ์ถœ) ์˜ˆ์™ธ\n", " d[\"bad_lot\"] = (d[\"confirmed_rate\"] > BAD_RATE).astype(int)\n", " return d\n", "# ---------------------------------------------------------------------------\n", "\n", "# ---- ํŒŒ์ƒ ํ”ผ์ฒ˜ ๊ณตํ†ต ํ•จ์ˆ˜ (๋ฏธ์…˜ 4 ์ดํ›„ ๊ณตํ†ต) ----\n", "def add_history(d):\n", " \"\"\"์—…์ฒด๋ณ„ ๊ณผ๊ฑฐ ํ™•์ • ๋ถˆ๋Ÿ‰๋ฅ (ํ˜„์žฌ ๋กœํŠธ ์ด์ „ ๋กœํŠธ๋งŒ ์‚ฌ์šฉ: ์ •๋ณด ๋ˆ„์ˆ˜ ๋ฐฉ์ง€)๊ณผ ์ตœ๊ทผ 5๋กœํŠธ ์ถ”์„ธ.\"\"\"\n", " d = d.sort_values(\"receipt_date\").reset_index(drop=True)\n", " g = d.groupby(\"vendor\")[\"confirmed_rate\"]\n", " d[\"vendor_hist_rate\"] = g.transform(lambda s: s.shift().expanding().mean()).fillna(d[\"confirmed_rate\"].mean())\n", " d[\"vendor_recent_rate\"] = g.transform(lambda s: s.shift().rolling(5, min_periods=1).mean()).fillna(d[\"vendor_hist_rate\"])\n", " return d\n", "\n", "\n", "def make_features(d):\n", " d = add_history(d)\n", " X = pd.DataFrame(index=d.index)\n", " X[\"obs_rate\"] = np.log((d[\"defects\"] + 0.5) / d[\"sample_n\"]) # 1 ํ‘œ๋ณธ ๋ถˆ๋Ÿ‰๋ฅ \n", " X[\"log_lot\"] = np.log(d[\"lot_size\"]) # 2 ๋กœํŠธ ๊ทœ๋ชจ(๋กœ๊ทธ)\n", " X[\"delay_pos\"] = d[\"delay_days\"].clip(lower=0) # 3 ๋‚ฉ๊ธฐ ์ง€์—ฐ์ผ\n", " X[\"delay_ratio\"] = X[\"delay_pos\"] / d[\"lead_days\"] # 4 ์ง€์—ฐ/๊ณ„์•ฝ๊ธฐ๊ฐ„ ๋น„\n", " X[\"short_lead\"] = (d[\"lead_days\"] < 45).astype(int) # 5 ์ด‰๋ฐ• ๋‚ฉ๊ธฐ\n", " X[\"precision\"] = (d[\"spec_class\"] == \"precision\").astype(int) # 6 ์ •๋ฐ€ ๊ทœ๊ฒฉ\n", " X[\"prec_x_short\"] = X[\"precision\"] * X[\"short_lead\"] # 7 ์ •๋ฐ€ x ์ด‰๋ฐ•(์ƒํ˜ธ์ž‘์šฉ)\n", " X[\"spec_risk\"] = d[\"spec_changed\"] + d[\"spec_mismatch\"] # 8 ๊ทœ๊ฒฉ ๋ณ€๊ฒฝยท๋ถˆ์ผ์น˜ ํ•ฉ\n", " X[\"dim_ratio\"] = np.log(d[\"dim_dev_mm\"] / d[\"tolerance_mm\"]) # 9 ์น˜์ˆ˜ ํŽธ์ฐจ/๊ณต์ฐจ\n", " X[\"vendor_hist\"] = np.log(d[\"vendor_hist_rate\"]) # 10 ์—…์ฒด ๊ณผ๊ฑฐ ๋ถˆ๋Ÿ‰๋ฅ \n", " X[\"vendor_trend\"] = d[\"vendor_recent_rate\"] - d[\"vendor_hist_rate\"] # 11 ์—…์ฒด ์ตœ๊ทผ ์ถ”์„ธ\n", " X[\"urgent\"] = d[\"pr_urgent\"] # 12 ๊ธด๊ธ‰ ๊ตฌ๋งค์š”๊ตฌ\n", " X[\"fill_ratio\"] = d[\"lot_size\"] / d[\"pr_qty\"] # 13 ๊ตฌ๋งค์š”๊ตฌ ๋Œ€๋น„ ๋‚ฉํ’ˆ ๋น„์œจ\n", " X[\"dim_share\"] = d[\"def_dim\"] / d[\"defects\"].clip(lower=1) # 14 ์น˜์ˆ˜ ์˜ค์ฐจ ๋น„์ค‘\n", " for f in FSC: # 15~ ํ’ˆ๋ชฉ๊ตฐ ์›-ํ•ซ\n", " X[f\"fsc_{f}\"] = (d[\"fsc\"] == f).astype(int)\n", " return d, X\n", "\n", "base = make_lots(1200, seed=42)\n", "# ---------------- 1) ๋Œ€๋Ÿ‰ ๋ถˆ๋Ÿ‰ ๋‚ฉํ’ˆ ์ฃผ์ž… ----------------\n", "rng = np.random.default_rng(88)\n", "inj = make_lots(60, seed=88)\n", "inj[\"vendor\"] = \"V03\" # ํ‰์†Œ ์šฐ์ˆ˜ ์—…์ฒด(์ˆจ์€ ํ’ˆ์งˆ -0.3)\n", "inj[\"receipt_date\"] = pd.Timestamp(\"2025-09-01\") + pd.to_timedelta(np.sort(rng.integers(0, 25, 60)), unit=\"D\")\n", "inj[\"month\"] = 20\n", "inj[\"lot_size\"] = (inj[\"lot_size\"] * 2).astype(int) # ๋Œ€๋Ÿ‰ ๋‚ฉํ’ˆ\n", "p = np.where(rng.random(60) < 0.8, rng.uniform(0.08, 0.25, 60), rng.uniform(0.01, 0.03, 60)) # 80%๋Š” ๋ถˆ๋Ÿ‰ ๋กœํŠธ\n", "inj[\"sample_n\"], inj[\"accept_c\"] = zip(*[sample_plan(s) for s in inj[\"lot_size\"]])\n", "inj[\"defects\"] = rng.binomial(inj[\"sample_n\"], p)\n", "inj[\"def_dim\"] = rng.binomial(inj[\"defects\"], 0.5); inj[\"def_spec\"] = inj[\"defects\"] - inj[\"def_dim\"]; inj[\"def_visual\"] = 0\n", "inj[\"dim_dev_mm\"] = (inj[\"tolerance_mm\"] * (0.3 + 2.0 * p) * rng.lognormal(0, 0.3, 60)).round(4)\n", "inj[\"confirmed_rate\"] = p.round(4); inj[\"bad_lot\"] = (p > BAD_RATE).astype(int)\n", "inj[\"lot_id\"] = [f\"INJ-{i:03d}\" for i in range(60)]\n", "inj[\"injected\"] = 1\n", "base[\"injected\"] = 0\n", "allx = pd.concat([base, inj], ignore_index=True)\n", "\n", "# ์†์ƒ ๋ ˆ์ฝ”๋“œ ์ฃผ์ž…(์ธก์ •์น˜ ๊ฒฐ์ธก, ์Œ์ˆ˜ ๋ถˆ๋Ÿ‰ ์ˆ˜, ํ‘œ๋ณธ 0) -> ๊ฒฉ๋ฆฌ\n", "bad_idx = rng.choice(len(allx), 12, replace=False)\n", "allx.loc[bad_idx[:4], \"dim_dev_mm\"] = np.nan\n", "allx.loc[bad_idx[4:8], \"defects\"] = -1\n", "allx.loc[bad_idx[8:], \"sample_n\"] = 0\n", "\n", "\n", "def quarantine(df):\n", " ok = df[\"dim_dev_mm\"].notna() & (df[\"defects\"] >= 0) & (df[\"sample_n\"] > 0) & (df[\"defects\"] <= df[\"sample_n\"])\n", " return df[ok].copy(), df[~ok].copy()\n", "\n", "\n", "clean, quar = quarantine(allx)\n", "print(f\"[1] ์ฃผ์ž…: V03 ์—…์ฒด ๋Œ€๋Ÿ‰ ๋‚ฉํ’ˆ 60๋กœํŠธ(๋ถˆ๋Ÿ‰ {inj['bad_lot'].sum()}๋กœํŠธ) + ์†์ƒ ๋ ˆ์ฝ”๋“œ {len(bad_idx)}๊ฑด [๊ต์œก์šฉ๊ฐ€์ƒ์ž๋ฃŒ]\")\n", "print(f\" ์†์ƒ ๋ ˆ์ฝ”๋“œ {len(quar)}๊ฑด ๊ฒฉ๋ฆฌ(์žฌ์ž…๋ ฅ ์š”์ฒญ), ํŒŒ์ดํ”„๋ผ์ธ ์ค‘๋‹จ 0๊ฑด\")\n", "\n", "d, X = make_features(clean)\n", "tr = (d[\"month\"] < 18).values\n", "y = np.log(d[\"confirmed_rate\"].values)\n", "gbm = GradientBoostingRegressor(n_estimators=300, max_depth=2, learning_rate=0.08, subsample=0.8, random_state=0).fit(X.values[tr], y[tr])\n", "sigma = float(np.std(y[tr] - gbm.predict(X.values[tr])) * 1.5)\n", "\n", "\n", "def score(Xv):\n", " return 100 * (1 - stats.norm.cdf((np.log(BAD_RATE) - gbm.predict(Xv)) / sigma))\n", "\n", "\n", "# ---------------- 2) ๋กœํŠธ ๊ฒฝ๋ณด ์ •ํ™•๋„ ----------------\n", "win = (d[\"month\"] >= 18).values\n", "w = d[win].copy(); w[\"risk\"] = score(X.values[win])\n", "w[\"alert\"] = (w[\"risk\"] >= 70) | (w[\"defects\"] > w[\"accept_c\"]) # ์œ„ํ—˜ ์ง€์ˆ˜ ๊ฒฝ๋ณด ๋˜๋Š” ์ƒ˜ํ”Œ๋ง ๋ถˆํ•ฉ๊ฒฉ\n", "acc = (w[\"alert\"] == (w[\"bad_lot\"] == 1)).mean()\n", "wi = w[w[\"injected\"] == 1]\n", "tp, fp = int((w[\"alert\"] & (w[\"bad_lot\"] == 1)).sum()), int((w[\"alert\"] & (w[\"bad_lot\"] == 0)).sum())\n", "print(f\"\\n[2] ์ŠคํŠธ๋ ˆ์Šค ๊ตฌ๊ฐ„ ๋กœํŠธ {len(w)}๊ฑด: ๊ฒฝ๋ณด ๋ฐœ๋ น ์ •ํ™•๋„ {acc:.1%} (๋ชฉํ‘œ 95% ์ด์ƒ: {'๋‹ฌ์„ฑ' if acc >= 0.95 else '๋ฏธ๋‹ฌ'})\")\n", "print(f\" ์ฃผ์ž… ๋ถˆ๋Ÿ‰ ๋กœํŠธ ํƒ์ง€ {int((wi['alert'] & (wi['bad_lot'] == 1)).sum())}/{int(wi['bad_lot'].sum())}, ์ „์ฒด ์ •ํƒ {tp}๊ฑด, ์˜ค๊ฒฝ๋ณด {fp}๊ฑด\")\n", "acc_i = (wi[\"alert\"] == (wi[\"bad_lot\"] == 1)).mean()\n", "print(f\" ๋Œ€๋Ÿ‰ ๋‚ฉํ’ˆ ์ฃผ์ž… ๋กœํŠธ {len(wi)}๊ฑด ๊ฒฝ๋ณด ์ •ํ™•๋„ {acc_i:.1%} (๋ชฉํ‘œ 95% ์ด์ƒ: {'๋‹ฌ์„ฑ' if acc_i >= 0.95 else '๋ฏธ๋‹ฌ'})\")\n", "print(f\" ์ƒ˜ํ”Œ๋ง ํŒ์ •๋งŒ ์‚ฌ์šฉ ์‹œ ์ •ํ™•๋„ {((w['defects'] > w['accept_c']) == (w['bad_lot'] == 1)).mean():.1%}\")\n", "\n", "# ---------------- 3) ์—…์ฒด ๋‹จ์œ„ CUSUM ๊ฒฝ๋ณด ์‹ ์†์„ฑ ----------------\n", "v3 = d[d[\"vendor\"] == \"V03\"].sort_values(\"receipt_date\")\n", "ref = v3[v3[\"month\"] < 18]\n", "p0 = (ref[\"defects\"].sum() / ref[\"sample_n\"].sum())\n", "first = v3[v3[\"injected\"] == 1][\"receipt_date\"].min()\n", "k, hlim, s, alarm, false_alarm = p0 * 2, 0.15, 0.0, None, 0\n", "trace = []\n", "for r in v3.itertuples():\n", " s = max(0.0, s + r.defects / r.sample_n - k) # ๊ธฐ์ค€์˜ 2๋ฐฐ๋ฅผ ๋„˜๋Š” ํ‘œ๋ณธ ๋ถˆ๋Ÿ‰๋ฅ ๋งŒ ๋ˆ„์ \n", " trace.append(s)\n", " if s > hlim and r.receipt_date < first:\n", " false_alarm += 1; s = 0.0 # ํ‰์‹œ ์˜ค๊ฒฝ๋ณด: ๊ธฐ๋ก ํ›„ ์ดˆ๊ธฐํ™”\n", " if alarm is None and s > hlim and r.injected == 1:\n", " alarm = r\n", "n_before = int((v3[v3[\"injected\"] == 1][\"receipt_date\"] <= alarm.receipt_date).sum()) if alarm is not None else None\n", "print(f\"\\n[3] V03 ์—…์ฒด CUSUM(๊ธฐ์ค€ ํ‘œ๋ณธ๋ถˆ๋Ÿ‰๋ฅ  {p0:.2%}): ๋Œ€๋Ÿ‰ ๋‚ฉํ’ˆ ์‹œ์ž‘ ํ›„ {n_before}๋ฒˆ์งธ ๋กœํŠธ์—์„œ ์—…์ฒด ๊ฒฝ๋ณด ๋ฐœ๋ น\"\n", " f\" ({(alarm.receipt_date - first).days}์ผ ๊ฒฝ๊ณผ) -> ํ•ด๋‹น ์—…์ฒด ํ›„์† ๋กœํŠธ ์ „์ˆ˜๊ฒ€์‚ฌ ์ „ํ™˜ ๊ฑด์˜\")\n", "print(f\" ์ฃผ์ž… ์ด์ „ V03 ๋กœํŠธ {int((v3['receipt_date'] < first).sum())}๊ฑด ์ค‘ ์—…์ฒด ์˜ค๊ฒฝ๋ณด {false_alarm}๊ฑด\")\n", "\n", "# ---------------- 4) ๋Œ€์‹œ๋ณด๋“œ ๊ฐฑ์‹  ์‹œ๊ฐ„ ----------------\n", "def refresh(df, Xv):\n", " t0 = time.perf_counter()\n", " rk = score(Xv)\n", " vend = pd.DataFrame({\"vendor\": df[\"vendor\"].values, \"risk\": rk, \"bad\": df[\"bad_lot\"].values}).groupby(\"vendor\").agg([\"mean\", \"size\"])\n", " trend = df.groupby(\"month\")[\"defects\"].sum() / df.groupby(\"month\")[\"sample_n\"].sum()\n", " top = np.argsort(-rk)[:20]\n", " return time.perf_counter() - t0\n", "\n", "\n", "print(\"\\n[4] ๋Œ€์‹œ๋ณด๋“œ ๊ฐฑ์‹  ์‹œ๊ฐ„(์œ„ํ—˜ ์‚ฐ์ถœ+์—…์ฒด ์ง‘๊ณ„+์ถ”์„ธ+์ƒ์œ„ 20๊ฑด)\")\n", "rows = []\n", "for mult in (1, 10, 50):\n", " big = pd.concat([d] * mult, ignore_index=True); Xb = np.tile(X.values, (mult, 1))\n", " tt = np.median([refresh(big, Xb) for _ in range(3)])\n", " rows.append({\"๋ฌผ๋Ÿ‰ ๋ฐฐ์ˆ˜\": mult, \"๋กœํŠธ ์ˆ˜\": len(big), \"๊ฐฑ์‹  ์‹œ๊ฐ„(์ดˆ)\": round(tt, 3), \"2์ดˆ ์ด๋‚ด\": \"๋‹ฌ์„ฑ\" if tt <= 2 else \"๋ฏธ๋‹ฌ\"})\n", "print(pd.DataFrame(rows).to_string(index=False))\n", "\n", "fig, ax = plt.subplots(1, 3, figsize=(15, 4))\n", "ax[0].scatter(w[\"risk\"], w[\"confirmed_rate\"], c=np.where(w[\"injected\"] == 1, \"tab:red\", \"tab:blue\"), s=10)\n", "ax[0].axvline(70, ls=\"--\", c=\"k\"); ax[0].axhline(BAD_RATE, ls=\":\", c=\"k\"); ax[0].set_xlabel(\"risk score\"); ax[0].set_ylabel(\"confirmed rate\")\n", "ax[0].set_title(\"Lot alerts (red = injected surge)\")\n", "ax[1].plot(range(len(trace)), trace); ax[1].axhline(hlim, c=\"r\", ls=\"--\"); ax[1].set_title(\"Vendor V03 CUSUM\"); ax[1].set_xlabel(\"lot sequence\")\n", "rr = pd.DataFrame(rows); ax[2].plot(rr[\"๋กœํŠธ ์ˆ˜\"], rr[\"๊ฐฑ์‹  ์‹œ๊ฐ„(์ดˆ)\"], \"o-\"); ax[2].axhline(2, c=\"r\", ls=\"--\"); ax[2].set_xscale(\"log\")\n", "ax[2].set_xlabel(\"lots\"); ax[2].set_ylabel(\"seconds\"); ax[2].set_title(\"Dashboard refresh time\")\n", "plt.tight_layout(); plt.show()\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "
\n", "๐Ÿ’ก [์ฐธ๊ณ ] ๋ฏธ์…˜ 08 ์˜ˆ์ƒ ์‹คํ–‰ ๊ฒฐ๊ณผ (Output)\n", "\n", "```text\n", "[1] ์ฃผ์ž…: V03 ์—…์ฒด ๋Œ€๋Ÿ‰ ๋‚ฉํ’ˆ 60๋กœํŠธ(๋ถˆ๋Ÿ‰ 54๋กœํŠธ) + ์†์ƒ ๋ ˆ์ฝ”๋“œ 12๊ฑด [๊ต์œก์šฉ๊ฐ€์ƒ์ž๋ฃŒ]\n", " ์†์ƒ ๋ ˆ์ฝ”๋“œ 12๊ฑด ๊ฒฉ๋ฆฌ(์žฌ์ž…๋ ฅ ์š”์ฒญ), ํŒŒ์ดํ”„๋ผ์ธ ์ค‘๋‹จ 0๊ฑด\n", "\n", "[2] ์ŠคํŠธ๋ ˆ์Šค ๊ตฌ๊ฐ„ ๋กœํŠธ 360๊ฑด: ๊ฒฝ๋ณด ๋ฐœ๋ น ์ •ํ™•๋„ 94.2% (๋ชฉํ‘œ 95% ์ด์ƒ: ๋ฏธ๋‹ฌ)\n", " ์ฃผ์ž… ๋ถˆ๋Ÿ‰ ๋กœํŠธ ํƒ์ง€ 54/54, ์ „์ฒด ์ •ํƒ 117๊ฑด, ์˜ค๊ฒฝ๋ณด 12๊ฑด\n", " ๋Œ€๋Ÿ‰ ๋‚ฉํ’ˆ ์ฃผ์ž… ๋กœํŠธ 60๊ฑด ๊ฒฝ๋ณด ์ •ํ™•๋„ 98.3% (๋ชฉํ‘œ 95% ์ด์ƒ: ๋‹ฌ์„ฑ)\n", " ์ƒ˜ํ”Œ๋ง ํŒ์ •๋งŒ ์‚ฌ์šฉ ์‹œ ์ •ํ™•๋„ 93.1%\n", "\n", "[3] V03 ์—…์ฒด CUSUM(๊ธฐ์ค€ ํ‘œ๋ณธ๋ถˆ๋Ÿ‰๋ฅ  2.40%): ๋Œ€๋Ÿ‰ ๋‚ฉํ’ˆ ์‹œ์ž‘ ํ›„ 6๋ฒˆ์งธ ๋กœํŠธ์—์„œ ์—…์ฒด ๊ฒฝ๋ณด ๋ฐœ๋ น (1์ผ ๊ฒฝ๊ณผ) -> ํ•ด๋‹น ์—…์ฒด ํ›„์† ๋กœํŠธ ์ „์ˆ˜๊ฒ€์‚ฌ ์ „ํ™˜ ๊ฑด์˜\n", " ์ฃผ์ž… ์ด์ „ V03 ๋กœํŠธ 107๊ฑด ์ค‘ ์—…์ฒด ์˜ค๊ฒฝ๋ณด 1๊ฑด\n", "\n", "[4] ๋Œ€์‹œ๋ณด๋“œ ๊ฐฑ์‹  ์‹œ๊ฐ„(์œ„ํ—˜ ์‚ฐ์ถœ+์—…์ฒด ์ง‘๊ณ„+์ถ”์„ธ+์ƒ์œ„ 20๊ฑด)\n", " ๋ฌผ๋Ÿ‰ ๋ฐฐ์ˆ˜ ๋กœํŠธ ์ˆ˜ ๊ฐฑ์‹  ์‹œ๊ฐ„(์ดˆ) 2์ดˆ ์ด๋‚ด\n", " 1 1248 0.012 ๋‹ฌ์„ฑ\n", " 10 12480 0.044 ๋‹ฌ์„ฑ\n", " 50 62400 0.172 ๋‹ฌ์„ฑ\n", "```\n", "
\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "---\n", "\n", "## ๐ŸŽฏ ๋ฏธ์…˜ 09. AAR ๊ธฐ๋ฐ˜ ๋ถˆ๋Ÿ‰ ์˜คํŒ์ • ์‚ฌ๋ก€ ๋ถ„์„ ๋ฐ ๊ฒ€์‚ฌ ์ƒ˜ํ”Œ๋ง ํŒŒ๋ผ๋ฏธํ„ฐ ์žฌ๋ณด์ •\n", "**์†Œ์š”์‹œ๊ฐ„**: ์•ฝ 3.0์‹œ๊ฐ„\n", "\n", "### ๐Ÿ“‹ ๋ฏธ์…˜ ๋ฐฐ๊ฒฝ ๋ฐ ์ˆ˜ํ–‰ ์ง€์นจ\n", "#### ๋ฏธ์…˜ ๋ฐฐ๊ฒฝ ๋ฐ ๋‹น์œ„์„ฑ\n", "\n", "AAR ๊ธฐ๋ฐ˜ ๋ถˆ๋Ÿ‰ ์˜คํŒ์ • ์‚ฌ๋ก€๋ฅผ ๋ถ„์„ํ•˜๊ณ  ์ƒ˜ํ”Œ๋ง ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ์žฌ๋ณด์ •ํ•œ๋‹ค. ์‹ค์ œ ์ „์ˆ  ํ˜„์žฅ์—์„œ๋Š” ๋‹จ์ˆœ ํƒ์ง€์— ๊ทธ์น˜์ง€ ์•Š๊ณ  ์‹ ์†ํ•œ ์œ„ํ˜‘ ํŒ๋‹จ๊ณผ ์‹ ๋ขฐ๋„ ๋†’์€ ์˜์‚ฌ๊ฒฐ์ •์ด ํ•„์ˆ˜์ ์ด๋ฏ€๋กœ, **๊ณ ์ • ์ƒ˜ํ”Œ๋ง ๊ธฐ์ค€์˜ ๋ถˆ๋Ÿ‰ ์˜คํŒ์ • ์‚ฌ๋ก€๋ฅผ ์‚ฌํ›„๊ฒ€ํ† (AAR)๋กœ ๋ถ„์„ํ•˜๊ณ , ์œ„ํ—˜ ๋“ฑ๊ธ‰๋ณ„ ์ƒ˜ํ”Œ๋ง ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ์žฌ๋ณด์ •ํ•ด ์ •๋Ÿ‰ ํšจ๊ณผ๋ฅผ ์‚ฐ์ถœํ•œ๋‹ค.** ๊ณผ์—…์„ ์™„์ˆ˜ํ•˜์—ฌ ์˜คํŒ์„ ๋ฐฉ์ง€ํ•˜๊ณ  ์ž‘์ „ ๋Œ€์‘๋ ฅ์„ ๊ทน๋Œ€ํ™”ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.\n", "\n", "์ด ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด ๋ณธ ๋ฏธ์…˜์—์„œ๋Š” **ํ•™์Šต ๊ตฌ๊ฐ„์˜ ์˜คํ•ฉ๊ฒฉยท์˜ค๋ถˆํ•ฉ๊ฒฉ ์‚ฌ๋ก€๋ฅผ ๊ฒฝ๊ณ„์„  ๋ถˆ๋Ÿ‰๋ฅ , ์—…์ฒด ์ด๋ ฅ ๋ฏธ๋ฐ˜์˜, ์ •๋ฐ€ ๊ทœ๊ฒฉ ๊ธฐ์ค€ ๋™์ผ ๋“ฑ ์›์ธ์œผ๋กœ ๋ถ„๋ฅ˜ ์ˆ˜ํ–‰ํ•˜๊ณ  ๊ฒ€์ฆํ•˜๋Š”** ์‹ค์Šต์„ ์ฒด๊ณ„์ ์œผ๋กœ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค.\n", "\n", "ํ•™์Šต์ž๋Š” ์‹ค์Šต๊ฐ€์ด๋“œ์— ์ œ์‹œ๋œ ์ด 4๊ฐœ ์„ธ๋ถ€ ํ™œ๋™(๊ธฐ๋ณธ ๊ธฐ๋Šฅ ๊ตฌํ˜„ โž” ๊ฒฐ๊ณผ ์ง€ํ‘œ ๊ฒ€์ฆ โž” ํŒŒ๋ผ๋ฏธํ„ฐ ํŠœ๋‹ ๋ฐ ๋„์ „๊ณผ์ œ)์„ ์ˆœ์ฐจ์ ์œผ๋กœ ์ง„ํ–‰ํ•˜๋ฉฐ ํ•ต์‹ฌ ํŒ์ • ๊ธฐ์ค€์„ ๋‹ฌ์„ฑํ•ฉ๋‹ˆ๋‹ค.\n", "\n", "**์ด๋ฒˆ ๊ฒฐ๊ณผ๋ฌผ:** `reports/qlt02_roi_analysis.md` ํ˜•ํƒœ์˜ ๊ฒฐ๊ณผ๋ฌผ์„ ์™„์„ฑํ•œ๋‹ค.\n", "\n", "### ๐Ÿ” ํ‰๊ฐ€ ๋ฐ ๊ฒ€์ฆ ๊ธฐ์ค€ (PASS ์กฐ๊ฑด)\n", "#### ๋ฏธ์…˜ ํŒ์ •\n", "\n", "**ํ‰๊ฐ€ ๊ตฌ๋ถ„:** ํ•„์ˆ˜\n", "\n", "**ํ•ต์‹ฌ ํ‰๊ฐ€์˜์—ญ:** ์ถ”์ • ์‚ฐ์‹์˜ ๊ฐ๊ด€์„ฑ ๋ฐ ๊ตฐ ์žฌ์ •/๊ตฐ์ˆ˜ ๊ธฐํš ๋ถ€์„œ ์„ค๋“๋ ฅ\n", "\n", "#### ๋ฏธ์…˜ PASS ์กฐ๊ฑด\n", "\n", "- ์ •๋Ÿ‰ ๊ธฐ์ค€: ๋น„์šฉ ์ ˆ๊ฐ์•ก, ์‹œ๊ฐ„ ๋‹จ์ถ•๋ฅ  ๋“ฑ 3๋Œ€ ์ •๋Ÿ‰ ํšจ๊ณผ ์ง€ํ‘œ ์‚ฐ์ถœ\n", "\n", "\n", "- ์ง„ํ–‰ ๊ธฐ์ค€: ROI ๋ถ„์„ ๋ณด๊ณ ์„œ ์™„์„ฑ ๋ฐ ์ฃผ์š” ์ง€ํ‘œ ๋ฏผ๊ฐ๋„ ๋ถ„์„ ์ˆ˜๋ก\n", "\n", "\n", "- ๊ฒ€์ฆ ๋ฐฉ๋ฒ•: ํšŒ๊ณ„/๊ธฐํš ๊ธฐ์ค€ ๋ชจ๋ธ๊ณผ์˜ ๋น„๊ต ๊ฒ€์ฆ\n", "\n", "\n", "#### ์ฑ„์ ์ž ํŒ์ • ๋ฉ”๋ชจ\n", "\n", "> ๋‹จ์ˆœ ์ฝ”๋“œ ์‹คํ–‰ ์—ฌ๋ถ€๋ฟ๋งŒ ์•„๋‹ˆ๋ผ ์ถœ๋ ฅ๋œ ์ˆ˜์น˜์™€ ๊ทผ๊ฑฐ ์ž๋ฃŒ๊ฐ€ ๊ตฐ ์‹ค๋ฌด ์š”๊ตฌ์‚ฌํ•ญ์— ๋ถ€ํ•ฉํ•˜๋Š”์ง€ ๋ฉด๋ฐ€ํžˆ ํ™•์ธํ•œ๋‹ค.\n", "\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "\"\"\"๊ณ ์ • ์ƒ˜ํ”Œ๋ง ๊ธฐ์ค€์˜ ์˜คํŒ์ •(์˜คํ•ฉ๊ฒฉยท์˜ค๋ถˆํ•ฉ๊ฒฉ) ์‚ฌ๋ก€๋ฅผ ์‚ฌํ›„๊ฒ€ํ† (AAR)๋กœ ์›์ธ ๋ถ„๋ฅ˜ํ•˜๊ณ , ์ž…๊ณ  ์ „ ์ •๋ณด๋กœ ๋‚˜๋ˆˆ ์œ„ํ—˜ ๋“ฑ๊ธ‰๋ณ„ ํ‘œ๋ณธ ํฌ๊ธฐยทํ•ฉ๊ฒฉํŒ์ •์ˆ˜๋ฅผ ํ•™์Šต ๊ตฌ๊ฐ„์—์„œ ์žฌ๋ณด์ •ํ•ด ๊ฒ€์ฆ ๊ตฌ๊ฐ„์—์„œ ๋น„์šฉ ์ ˆ๊ฐ์•กยท๊ฒ€์‚ฌ ์‹œ๊ฐ„ ๋‹จ์ถ•๋ฅ ยท๋ถˆ๋Ÿ‰ ์œ ์ถœ ๊ฐ์†Œ์œจ์˜ 3๋Œ€ ํšจ๊ณผ ์ง€ํ‘œ๋ฅผ ์‚ฐ์ถœํ•œ๋‹ค.\"\"\"\n", "import numpy as np\n", "import pandas as pd\n", "import matplotlib.pyplot as plt\n", "from sklearn.linear_model import LogisticRegression\n", "\n", "# ---------------------------------------------------------------------------\n", "# [๊ต์œก์šฉ๊ฐ€์ƒ์ž๋ฃŒ] ๊ตฐ์ˆ˜ํ’ˆ ์ˆ˜๋ น๊ฒ€์‚ฌ ๋กœํŠธ ๋ฐ์ดํ„ฐ ์ƒ์„ฑ๊ธฐ (์‹ค์ œ ์—…์ฒดยทํ’ˆ๋ชฉยท๊ณ„์•ฝ ์ •๋ณด ์—†์Œ)\n", "# ๊ตฌ๋งค์š”๊ตฌ(๊ทœ๊ฒฉยท๊ธด๊ธ‰ ์—ฌ๋ถ€) -> ๊ณ„์•ฝ(๋‚ฉ๊ธฐ) -> ๋‚ฉํ’ˆ -> ์ˆ˜๋ น๊ฒ€์‚ฌ(ํ‘œ๋ณธ) -> ํ™•์ • ๋ถˆ๋Ÿ‰๋ฅ (์ „์ˆ˜๊ฒ€์‚ฌยท์‚ฌ์šฉ๋ถ€๋Œ€ ํ•˜์ž ๋ฐ˜์˜)\n", "# ---------------------------------------------------------------------------\n", "FSC = {\"5305\": \"screw/bolt\", \"5330\": \"gasket\", \"5935\": \"connector\", \"6135\": \"battery\", \"2530\": \"brake part\", \"8415\": \"clothing\"}\n", "VENDORS = [f\"V{i:02d}\" for i in range(1, 13)]\n", "VENDOR_Q = dict(zip(VENDORS, [-0.6, -0.4, -0.3, -0.2, -0.1, 0.0, 0.1, 0.2, 0.3, 0.5, 0.8, 1.1])) # ์—…์ฒด ๊ณ ์œ  ํ’ˆ์งˆ ์ˆ˜์ค€(์ˆจ์€ ๊ฐ’)\n", "FSC_Q = {\"5305\": -0.3, \"5330\": 0.0, \"5935\": 0.25, \"6135\": 0.1, \"2530\": 0.35, \"8415\": -0.2}\n", "SAMPLE_PLAN = [(500, 50, 3), (1200, 80, 5), (3200, 125, 7), (10 ** 9, 200, 10)] # (๋กœํŠธ ํฌ๊ธฐ ์ƒํ•œ, ํ‘œ๋ณธ n, ํ•ฉ๊ฒฉํŒ์ •์ˆ˜ c) ๋‹จ์ˆœํ™” ๊ธฐ์ค€\n", "BAD_RATE = 0.05 # ํ™•์ • ๋ถˆ๋Ÿ‰๋ฅ  5% ์ดˆ๊ณผ ๋กœํŠธ = ๋ถˆํ•ฉ๊ฒฉ ๋Œ€์ƒ\n", "\n", "\n", "def sample_plan(lot_size):\n", " for up, n, c in SAMPLE_PLAN:\n", " if lot_size <= up:\n", " return n, c\n", "\n", "\n", "def true_rate(d, rng=None):\n", " \"\"\"์ˆจ์€ ๋ถˆ๋Ÿ‰ ๋ฐœ์ƒ ๊ตฌ์กฐ: ์—…์ฒด ์ˆ˜์ค€ + ํ’ˆ๋ชฉ๊ตฐ + ์ •๋ฐ€ ๊ทœ๊ฒฉ + ๊ธด๊ธ‰ ๊ตฌ๋งค + ๊ทœ๊ฒฉ ๋ณ€๊ฒฝยท๋ถˆ์ผ์น˜ + ๋‚ฉ๊ธฐ ์ง€์—ฐ + (์ •๋ฐ€ ๊ทœ๊ฒฉ x ์ด‰๋ฐ• ๋‚ฉ๊ธฐ) ์ƒํ˜ธ์ž‘์šฉ.\"\"\"\n", " short = (d[\"lead_days\"] < 45).astype(float)\n", " z = (-4.35 + d[\"vendor\"].map(VENDOR_Q) + d[\"fsc\"].map(FSC_Q) + 0.55 * (d[\"spec_class\"] == \"precision\")\n", " + 0.35 * d[\"pr_urgent\"] + 0.6 * d[\"spec_changed\"] + 0.8 * d[\"spec_mismatch\"]\n", " + 0.9 * np.log1p(np.maximum(d[\"delay_days\"], 0) / 7) + 0.9 * short * (d[\"spec_class\"] == \"precision\")\n", " + 0.5 * (d[\"month\"] >= 18) * (d[\"vendor\"] == \"V10\")) # V10 ํ›„๋ฐ˜๊ธฐ ํ’ˆ์งˆ ์ €ํ•˜(๋“œ๋ฆฌํ”„ํŠธ)\n", " if rng is not None:\n", " z = z + rng.normal(0, 0.12, len(d))\n", " return np.clip(np.exp(z), 0.002, 0.35)\n", "\n", "\n", "def make_lots(n=1200, seed=42, months=24):\n", " rng = np.random.default_rng(seed)\n", " d = pd.DataFrame({\"lot_id\": [f\"LOT{seed % 100:02d}-{i:05d}\" for i in range(n)]})\n", " d[\"month\"] = rng.integers(0, months, n)\n", " d[\"receipt_date\"] = pd.Timestamp(\"2024-01-01\") + pd.to_timedelta(d[\"month\"] * 30 + rng.integers(0, 30, n), unit=\"D\")\n", " d = d.sort_values(\"receipt_date\").reset_index(drop=True)\n", " d[\"month\"] = ((d[\"receipt_date\"] - pd.Timestamp(\"2024-01-01\")).dt.days // 30).clip(0, months - 1)\n", " d[\"vendor\"] = rng.choice(VENDORS, n, p=np.r_[np.full(6, 0.1), np.full(6, 0.4 / 6)])\n", " d[\"fsc\"] = rng.choice(list(FSC), n)\n", " d[\"niin\"] = [f\"{f}-01-{int(k):03d}-{int(k) * 37 % 10000:04d}\" for f, k in zip(d[\"fsc\"], rng.integers(100, 110, n))]\n", " d[\"spec_class\"] = np.where(rng.random(n) < np.where(d[\"fsc\"].isin([\"5935\", \"2530\"]), 0.6, 0.25), \"precision\", \"general\")\n", " d[\"tolerance_mm\"] = np.where(d[\"spec_class\"] == \"precision\", rng.uniform(0.05, 0.1, n), rng.uniform(0.2, 0.5, n)).round(3)\n", " d[\"spec_rev\"] = rng.integers(1, 6, n)\n", " d[\"pr_urgent\"] = (rng.random(n) < 0.2).astype(int)\n", " d[\"spec_changed\"] = (rng.random(n) < 0.12).astype(int)\n", " d[\"spec_mismatch\"] = (rng.random(n) < 0.08).astype(int)\n", " d[\"pr_qty\"] = rng.integers(200, 5000, n)\n", " d[\"lot_size\"] = (d[\"pr_qty\"] * rng.uniform(0.5, 1.0, n)).astype(int)\n", " d[\"lead_days\"] = np.where(d[\"pr_urgent\"] == 1, rng.integers(20, 60, n), rng.integers(40, 150, n))\n", " d[\"delay_days\"] = np.round(rng.gamma(1.2, 6, n) * rng.binomial(1, 0.45, n) - rng.integers(0, 5, n)).astype(int)\n", " d[\"unit_cost\"] = rng.choice([2_000, 15_000, 40_000, 120_000], n, p=[0.35, 0.3, 0.25, 0.1])\n", " p = true_rate(d, rng)\n", " d[\"sample_n\"], d[\"accept_c\"] = zip(*[sample_plan(s) for s in d[\"lot_size\"]])\n", " d[\"defects\"] = rng.binomial(d[\"sample_n\"], p)\n", " share = np.where(d[\"spec_class\"] == \"precision\", 0.55, 0.3)\n", " d[\"def_dim\"] = rng.binomial(d[\"defects\"], share)\n", " d[\"def_spec\"] = rng.binomial(d[\"defects\"] - d[\"def_dim\"], 0.6)\n", " d[\"def_visual\"] = d[\"defects\"] - d[\"def_dim\"] - d[\"def_spec\"]\n", " d[\"dim_dev_mm\"] = (d[\"tolerance_mm\"] * (0.3 + 2.0 * p) * rng.lognormal(0, 0.3, n)).round(4)\n", " d[\"confirmed_rate\"] = np.clip(p * rng.lognormal(0, 0.05, n), 0.001, 0.4).round(4) # ์ „์ˆ˜๊ฒ€์‚ฌยทํ•˜์ž ๋ฐ˜์˜ ํ™•์ • ๋ถˆ๋Ÿ‰๋ฅ \n", " d[\"field_claims\"] = rng.poisson(d[\"lot_size\"] * p * 0.03)\n", " d[\"inspector\"] = rng.choice([\"INS-1\", \"INS-2\", \"INS-3\", \"INS-4\"], n)\n", " d[\"emergency_release\"] = ((d[\"pr_urgent\"] == 1) & (rng.random(n) < 0.3)).astype(int) # ๊ธด๊ธ‰ ๋ถˆ์ถœ(๊ฒ€์‚ฌ ์ „ ์„ ๋ถˆ์ถœ) ์˜ˆ์™ธ\n", " d[\"bad_lot\"] = (d[\"confirmed_rate\"] > BAD_RATE).astype(int)\n", " return d\n", "# ---------------------------------------------------------------------------\n", "\n", "# ---- ํŒŒ์ƒ ํ”ผ์ฒ˜ ๊ณตํ†ต ํ•จ์ˆ˜ (๋ฏธ์…˜ 4 ์ดํ›„ ๊ณตํ†ต) ----\n", "def add_history(d):\n", " \"\"\"์—…์ฒด๋ณ„ ๊ณผ๊ฑฐ ํ™•์ • ๋ถˆ๋Ÿ‰๋ฅ (ํ˜„์žฌ ๋กœํŠธ ์ด์ „ ๋กœํŠธ๋งŒ ์‚ฌ์šฉ: ์ •๋ณด ๋ˆ„์ˆ˜ ๋ฐฉ์ง€)๊ณผ ์ตœ๊ทผ 5๋กœํŠธ ์ถ”์„ธ.\"\"\"\n", " d = d.sort_values(\"receipt_date\").reset_index(drop=True)\n", " g = d.groupby(\"vendor\")[\"confirmed_rate\"]\n", " d[\"vendor_hist_rate\"] = g.transform(lambda s: s.shift().expanding().mean()).fillna(d[\"confirmed_rate\"].mean())\n", " d[\"vendor_recent_rate\"] = g.transform(lambda s: s.shift().rolling(5, min_periods=1).mean()).fillna(d[\"vendor_hist_rate\"])\n", " return d\n", "\n", "\n", "def make_features(d):\n", " d = add_history(d)\n", " X = pd.DataFrame(index=d.index)\n", " X[\"obs_rate\"] = np.log((d[\"defects\"] + 0.5) / d[\"sample_n\"]) # 1 ํ‘œ๋ณธ ๋ถˆ๋Ÿ‰๋ฅ \n", " X[\"log_lot\"] = np.log(d[\"lot_size\"]) # 2 ๋กœํŠธ ๊ทœ๋ชจ(๋กœ๊ทธ)\n", " X[\"delay_pos\"] = d[\"delay_days\"].clip(lower=0) # 3 ๋‚ฉ๊ธฐ ์ง€์—ฐ์ผ\n", " X[\"delay_ratio\"] = X[\"delay_pos\"] / d[\"lead_days\"] # 4 ์ง€์—ฐ/๊ณ„์•ฝ๊ธฐ๊ฐ„ ๋น„\n", " X[\"short_lead\"] = (d[\"lead_days\"] < 45).astype(int) # 5 ์ด‰๋ฐ• ๋‚ฉ๊ธฐ\n", " X[\"precision\"] = (d[\"spec_class\"] == \"precision\").astype(int) # 6 ์ •๋ฐ€ ๊ทœ๊ฒฉ\n", " X[\"prec_x_short\"] = X[\"precision\"] * X[\"short_lead\"] # 7 ์ •๋ฐ€ x ์ด‰๋ฐ•(์ƒํ˜ธ์ž‘์šฉ)\n", " X[\"spec_risk\"] = d[\"spec_changed\"] + d[\"spec_mismatch\"] # 8 ๊ทœ๊ฒฉ ๋ณ€๊ฒฝยท๋ถˆ์ผ์น˜ ํ•ฉ\n", " X[\"dim_ratio\"] = np.log(d[\"dim_dev_mm\"] / d[\"tolerance_mm\"]) # 9 ์น˜์ˆ˜ ํŽธ์ฐจ/๊ณต์ฐจ\n", " X[\"vendor_hist\"] = np.log(d[\"vendor_hist_rate\"]) # 10 ์—…์ฒด ๊ณผ๊ฑฐ ๋ถˆ๋Ÿ‰๋ฅ \n", " X[\"vendor_trend\"] = d[\"vendor_recent_rate\"] - d[\"vendor_hist_rate\"] # 11 ์—…์ฒด ์ตœ๊ทผ ์ถ”์„ธ\n", " X[\"urgent\"] = d[\"pr_urgent\"] # 12 ๊ธด๊ธ‰ ๊ตฌ๋งค์š”๊ตฌ\n", " X[\"fill_ratio\"] = d[\"lot_size\"] / d[\"pr_qty\"] # 13 ๊ตฌ๋งค์š”๊ตฌ ๋Œ€๋น„ ๋‚ฉํ’ˆ ๋น„์œจ\n", " X[\"dim_share\"] = d[\"def_dim\"] / d[\"defects\"].clip(lower=1) # 14 ์น˜์ˆ˜ ์˜ค์ฐจ ๋น„์ค‘\n", " for f in FSC: # 15~ ํ’ˆ๋ชฉ๊ตฐ ์›-ํ•ซ\n", " X[f\"fsc_{f}\"] = (d[\"fsc\"] == f).astype(int)\n", " return d, X\n", "\n", "# ๊ต์œก์šฉ ๊ฐ€์ • ๋‹จ๊ฐ€(์›): ์‹ค์ œ ์˜ˆ์‚ฐยท๊ณ„์•ฝ ์ž๋ฃŒ ์•„๋‹˜\n", "SAMPLE_MIN, MIN_COST = 6, 1_000 # ํ‘œ๋ณธ 1๊ฐœ ๊ฒ€์‚ฌ 6๋ถ„, ๊ฒ€์‚ฌ ์ธ๊ฑด๋น„ ๋ถ„๋‹น 1์ฒœ์›\n", "MISS_FACTOR, REJECT_RATE = 3.0, 0.03 # ์˜คํ•ฉ๊ฒฉ: ๋ถˆ๋Ÿ‰ํ’ˆ ๋‹จ๊ฐ€ x3 ์†์‹ค / ์˜ค๋ถˆํ•ฉ๊ฒฉ: ๋กœํŠธ ๊ธˆ์•ก์˜ 3% ์ง€์—ฐยท์žฌ๋‚ฉํ’ˆ ๋น„์šฉ\n", "\n", "d, _ = make_features(make_lots(1200, seed=42))\n", "tr, te = d[d[\"month\"] < 18].copy(), d[d[\"month\"] >= 18].copy()\n", "\n", "\n", "def evaluate(df, n, c, seed=0):\n", " \"\"\"์ฃผ์–ด์ง„ ํ‘œ๋ณธ ํฌ๊ธฐ n๊ณผ ํ•ฉ๊ฒฉํŒ์ •์ˆ˜ c๋กœ ๋กœํŠธ๋ฅผ ๋‹ค์‹œ ๊ฒ€์‚ฌ(ํ™•์ • ๋ถˆ๋Ÿ‰๋ฅ ๋กœ ์ดํ•ญ ๋ชจ์˜)ํ•ด ํŒ์ •ยท๋น„์šฉ์„ ๊ณ„์‚ฐํ•œ๋‹ค.\"\"\"\n", " rng = np.random.default_rng(seed)\n", " dfc = rng.binomial(n, df[\"confirmed_rate\"].values)\n", " acc = dfc <= c\n", " bad = df[\"bad_lot\"].values == 1\n", " value = (df[\"lot_size\"] * df[\"unit_cost\"]).values\n", " c_insp = n * SAMPLE_MIN * MIN_COST\n", " c_fa = np.where(acc & bad, df[\"lot_size\"].values * df[\"confirmed_rate\"].values * df[\"unit_cost\"].values * MISS_FACTOR, 0)\n", " c_fr = np.where(~acc & ~bad, value * REJECT_RATE, 0)\n", " return {\"acc\": acc, \"FA\": int((acc & bad).sum()), \"FR\": int((~acc & ~bad).sum()), \"๊ฒ€์‚ฌ๋ถ„\": float(np.sum(n * SAMPLE_MIN)),\n", " \"๊ฒ€์‚ฌ๋น„\": float(np.sum(c_insp)), \"์œ ์ถœ์†์‹ค\": float(c_fa.sum()), \"๋ฐ˜ํ’ˆ์†์‹ค\": float(c_fr.sum()), \"์ด๋น„์šฉ\": float(np.sum(c_insp) + c_fa.sum() + c_fr.sum()),\n", " \"์œ ์ถœ ๋ถˆ๋Ÿ‰ํ’ˆ\": float(np.sum(np.where(acc, df[\"lot_size\"].values * df[\"confirmed_rate\"].values, 0)))}\n", "\n", "\n", "# ---------------- 1) AAR: ํ˜„ํ–‰ ๊ณ ์ • ์ƒ˜ํ”Œ๋ง ์˜คํŒ์ • ์›์ธ ๋ถ„๋ฅ˜ ----------------\n", "base_tr = evaluate(tr, tr[\"sample_n\"].values, tr[\"accept_c\"].values, seed=1)\n", "t = tr.assign(acc=base_tr[\"acc\"])\n", "fa = t[t[\"acc\"] & (t[\"bad_lot\"] == 1)].copy()\n", "fr = t[~t[\"acc\"] & (t[\"bad_lot\"] == 0)].copy()\n", "fa[\"์›์ธ\"] = np.select([fa[\"confirmed_rate\"] < 0.08, fa[\"vendor_hist_rate\"] > 0.06, fa[\"spec_class\"] == \"precision\"],\n", " [\"๊ฒฝ๊ณ„์„  ๋ถˆ๋Ÿ‰๋ฅ (5~8%)์„ ํ‘œ๋ณธ์ด ๋ชป ์žก์Œ\", \"๊ณ ์œ„ํ—˜ ์—…์ฒด ์ด๋ ฅ ๋ฏธ๋ฐ˜์˜\", \"์ •๋ฐ€ ๊ทœ๊ฒฉ์ธ๋ฐ ํ‘œ๋ณธยท๊ธฐ์ค€ ๋™์ผ\"], \"ํ‘œ๋ณธ ์šฐ์—ฐ ๋ณ€๋™\")\n", "fr[\"์›์ธ\"] = np.where(fr[\"confirmed_rate\"] > 0.035, \"๋ถˆ๋Ÿ‰๋ฅ  4~5% ๊ฒฝ๊ณ„ ๋กœํŠธ ๊ณผํŒ์ •\", \"ํ‘œ๋ณธ ์šฐ์—ฐ ๋ณ€๋™(์†Œํ˜• ๋กœํŠธ)\")\n", "print(f\"[1] AAR: ํ•™์Šต ๊ตฌ๊ฐ„ {len(t)}๋กœํŠธ ์ค‘ ์˜คํ•ฉ๊ฒฉ {len(fa)}๊ฑด, ์˜ค๋ถˆํ•ฉ๊ฒฉ {len(fr)}๊ฑด [๊ต์œก์šฉ๊ฐ€์ƒ์ž๋ฃŒ]\")\n", "print(\" ์˜คํ•ฉ๊ฒฉ ์›์ธ:\", fa[\"์›์ธ\"].value_counts().to_dict())\n", "print(\" ์˜ค๋ถˆํ•ฉ๊ฒฉ ์›์ธ:\", fr[\"์›์ธ\"].value_counts().to_dict())\n", "print(\" -> ๋ชจ๋“  ๋กœํŠธ์— ๋กœํŠธ ํฌ๊ธฐ๋งŒ์œผ๋กœ ๊ฐ™์€ ๊ธฐ์ค€์„ ์ ์šฉํ•ด, ์œ„ํ—˜์ด ๋†’์€ ๋กœํŠธ๋Š” ํŒ์ •์ด ๋А์Šจํ•˜๊ณ  ๋‚ฎ์€ ๋กœํŠธ๋Š” ๊ณผ์ž‰ ๊ฒ€์‚ฌํ•˜๊ณ  ์žˆ๋‹ค.\")\n", "\n", "# ---------------- 2) ์ž…๊ณ  ์ „ ์ •๋ณด๋กœ ์œ„ํ—˜ ๋“ฑ๊ธ‰ ๋ถ„๋ฅ˜ ----------------\n", "PRE = [\"vendor_hist_rate\", \"delay_days\", \"lead_days\", \"pr_urgent\", \"spec_changed\", \"spec_mismatch\"]\n", "\n", "\n", "def pre_X(df):\n", " x = df[PRE].astype(float).copy()\n", " x[\"vendor_hist_rate\"] = np.log(x[\"vendor_hist_rate\"]); x[\"precision\"] = (df[\"spec_class\"] == \"precision\").astype(float)\n", " x[\"prec_short\"] = x[\"precision\"] * (df[\"lead_days\"] < 45); x[\"delay_pos\"] = df[\"delay_days\"].clip(lower=0)\n", " return x.values\n", "\n", "\n", "clf = LogisticRegression(max_iter=2000, C=1.0).fit(pre_X(tr), tr[\"bad_lot\"])\n", "for df in (tr, te):\n", " pr = clf.predict_proba(pre_X(df))[:, 1]\n", " df[\"tier\"] = np.where(pr >= 0.35, \"๊ณ ์œ„ํ—˜\", np.where(pr >= 0.08, \"๋ณดํ†ต\", \"์ €์œ„ํ—˜\"))\n", "print(f\"\\n[2] ์ž…๊ณ  ์ „ ์œ„ํ—˜ ๋“ฑ๊ธ‰(ํ•™์Šต ๊ตฌ๊ฐ„): {tr['tier'].value_counts().to_dict()}, ๋“ฑ๊ธ‰๋ณ„ ์‹ค์ œ ๋ถˆ๋Ÿ‰ ๋กœํŠธ ๋น„์œจ \"\n", " f\"{tr.groupby('tier')['bad_lot'].mean().round(3).to_dict()}\")\n", "\n", "# ---------------- 3) ๋“ฑ๊ธ‰๋ณ„ (n, c) ์žฌ๋ณด์ •: ํ•™์Šต ๊ตฌ๊ฐ„ ์ด๋น„์šฉ ์ตœ์†Œ ----------------\n", "grid = [(n, c) for n in (20, 32, 50, 80, 125, 200, 315) for c in range(0, 12) if c < n * 0.08]\n", "plan = {}\n", "for tier in (\"์ €์œ„ํ—˜\", \"๋ณดํ†ต\", \"๊ณ ์œ„ํ—˜\"):\n", " sub = tr[tr[\"tier\"] == tier]\n", " best = min(grid, key=lambda nc: np.mean([evaluate(sub, np.full(len(sub), nc[0]), nc[1], seed=s)[\"์ด๋น„์šฉ\"] for s in range(3)]))\n", " plan[tier] = best\n", "print(\"\\n[3] ์žฌ๋ณด์ • ์ƒ˜ํ”Œ๋ง ๊ธฐ์ค€(ํ•™์Šต ๊ตฌ๊ฐ„ ๋น„์šฉ ์ตœ์†Œ):\", {k: f\"n={v[0]}, c={v[1]}\" for k, v in plan.items()})\n", "\n", "# ---------------- 4) ๊ฒ€์ฆ ๊ตฌ๊ฐ„ ํšจ๊ณผ(3๋Œ€ ์ง€ํ‘œ) ----------------\n", "old = evaluate(te, te[\"sample_n\"].values, te[\"accept_c\"].values, seed=9)\n", "new = evaluate(te, te[\"tier\"].map(lambda k: plan[k][0]).values, te[\"tier\"].map(lambda k: plan[k][1]).values, seed=9)\n", "cmp_ = pd.DataFrame({\"ํ˜„ํ–‰ ๊ณ ์ •\": old, \"์žฌ๋ณด์ •\": new}).drop(\"acc\").T\n", "print(\"\\n[4] ๊ฒ€์ฆ ๊ตฌ๊ฐ„(์ตœ๊ทผ 6๊ฐœ์›”) ๋น„๊ต\")\n", "show = cmp_[[\"FA\", \"FR\", \"๊ฒ€์‚ฌ๋ถ„\", \"์ด๋น„์šฉ\", \"์œ ์ถœ ๋ถˆ๋Ÿ‰ํ’ˆ\"]].copy()\n", "show[\"์ด๋น„์šฉ\"] = (show[\"์ด๋น„์šฉ\"] / 1e6).round(1).astype(str) + \"๋ฐฑ๋งŒ์›\"\n", "show[\"๊ฒ€์‚ฌ๋ถ„\"] = (show[\"๊ฒ€์‚ฌ๋ถ„\"] / 60).round(0).astype(int).astype(str) + \"์‹œ๊ฐ„\"\n", "show[\"์œ ์ถœ ๋ถˆ๋Ÿ‰ํ’ˆ\"] = show[\"์œ ์ถœ ๋ถˆ๋Ÿ‰ํ’ˆ\"].round(0).astype(int)\n", "print(show.rename(columns={\"FA\": \"์˜คํ•ฉ๊ฒฉ\", \"FR\": \"์˜ค๋ถˆํ•ฉ๊ฒฉ\", \"๊ฒ€์‚ฌ๋ถ„\": \"๊ฒ€์‚ฌ์‹œ๊ฐ„\"}).to_string())\n", "saving = old[\"์ด๋น„์šฉ\"] - new[\"์ด๋น„์šฉ\"]\n", "time_cut = 1 - new[\"๊ฒ€์‚ฌ๋ถ„\"] / old[\"๊ฒ€์‚ฌ๋ถ„\"]\n", "leak_cut = 1 - new[\"์œ ์ถœ ๋ถˆ๋Ÿ‰ํ’ˆ\"] / old[\"์œ ์ถœ ๋ถˆ๋Ÿ‰ํ’ˆ\"]\n", "print(\"\\n[3๋Œ€ ์ •๋Ÿ‰ ํšจ๊ณผ ์ง€ํ‘œ] (์‚ฐ์‹: ํ˜„ํ–‰-์žฌ๋ณด์ •, ๋™์ผ ๋‚œ์ˆ˜๋กœ ๊ฐ™์€ ๋กœํŠธ๋ฅผ ๋‹ค์‹œ ๊ฒ€์‚ฌํ•œ ๋ชจ์˜ ๊ฒฐ๊ณผ)\")\n", "print(f\" 1) ํ’ˆ์งˆ๋น„์šฉ ์ ˆ๊ฐ์•ก = ๊ฒ€์‚ฌ๋น„+์œ ์ถœ์†์‹ค+๋ฐ˜ํ’ˆ์†์‹ค ์ฐจ์ด = {saving / 1e6:.1f}๋ฐฑ๋งŒ์› ({saving / old['์ด๋น„์šฉ']:.1%})\")\n", "print(f\" 2) ๊ฒ€์‚ฌ ์‹œ๊ฐ„ ๋‹จ์ถ•๋ฅ  = 1 - ์žฌ๋ณด์ • ๊ฒ€์‚ฌ๋ถ„/ํ˜„ํ–‰ ๊ฒ€์‚ฌ๋ถ„ = {time_cut:.1%}\")\n", "print(f\" 3) ๋ถˆ๋Ÿ‰ํ’ˆ ์œ ์ถœ ๊ฐ์†Œ์œจ = 1 - ์žฌ๋ณด์ • ์œ ์ถœ ์ˆ˜๋Ÿ‰/ํ˜„ํ–‰ ์œ ์ถœ ์ˆ˜๋Ÿ‰ = {leak_cut:.1%}\")\n", "print(\" ๊ณ ์œ„ํ—˜ ๋“ฑ๊ธ‰์€ c=0์œผ๋กœ ์—„๊ฒฉํ•ด์ ธ ์˜ค๋ถˆํ•ฉ๊ฒฉ์ด ๋Š˜์ง€๋งŒ, ๋ฐ˜ํ’ˆ ๋น„์šฉ๋ณด๋‹ค ์œ ์ถœ ์†์‹ค ๊ฐ์†Œ๊ฐ€ ์ปค์„œ ์ด๋น„์šฉ์ด ์ค„์—ˆ๋‹ค.\")\n", "print(\" ๊ฐ€์ • ๋‹จ๊ฐ€๋Š” ๊ต์œก์šฉ์ด๋ฉฐ, ์‹ค์ œ ์ ์šฉ ์‹œ ๋ถ€๋Œ€ ๊ฒ€์‚ฌ ๊ธฐ๋ก๊ณผ ์žฌ์ • ๋ถ€์„œ ๋‹จ๊ฐ€๋กœ ๋‹ค์‹œ ์‚ฐ์ถœํ•œ๋‹ค.\")\n", "\n", "fig, ax = plt.subplots(1, 3, figsize=(15, 4))\n", "ax[0].bar(range(fa[\"์›์ธ\"].nunique()), fa[\"์›์ธ\"].value_counts().values, color=\"tab:red\")\n", "ax[0].set_title(\"False-accept root causes\"); ax[0].set_xticks(range(fa[\"์›์ธ\"].nunique())); ax[0].set_xticklabels([f\"cause {i + 1}\" for i in range(fa[\"์›์ธ\"].nunique())])\n", "x = np.arange(3); ax[1].bar(x - 0.2, [old[\"FA\"], old[\"FR\"], old[\"๊ฒ€์‚ฌ๋ถ„\"] / 600], 0.4, label=\"fixed plan\")\n", "ax[1].bar(x + 0.2, [new[\"FA\"], new[\"FR\"], new[\"๊ฒ€์‚ฌ๋ถ„\"] / 600], 0.4, label=\"risk-based plan\")\n", "ax[1].set_xticks(x); ax[1].set_xticklabels([\"false accept\", \"false reject\", \"insp. hours/10\"]); ax[1].legend(); ax[1].set_title(\"Hold-out comparison\")\n", "ax[2].bar([\"cost saving %\", \"time cut %\", \"leak cut %\"], [saving / old[\"์ด๋น„์šฉ\"] * 100, time_cut * 100, leak_cut * 100], color=\"tab:green\"); ax[2].set_title(\"ROI indicators\")\n", "plt.tight_layout(); plt.show()\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "
\n", "๐Ÿ’ก [์ฐธ๊ณ ] ๋ฏธ์…˜ 09 ์˜ˆ์ƒ ์‹คํ–‰ ๊ฒฐ๊ณผ (Output)\n", "\n", "```text\n", "[1] AAR: ํ•™์Šต ๊ตฌ๊ฐ„ 896๋กœํŠธ ์ค‘ ์˜คํ•ฉ๊ฒฉ 58๊ฑด, ์˜ค๋ถˆํ•ฉ๊ฒฉ 25๊ฑด [๊ต์œก์šฉ๊ฐ€์ƒ์ž๋ฃŒ]\n", " ์˜คํ•ฉ๊ฒฉ ์›์ธ: {'๊ฒฝ๊ณ„์„  ๋ถˆ๋Ÿ‰๋ฅ (5~8%)์„ ํ‘œ๋ณธ์ด ๋ชป ์žก์Œ': 53, '๊ณ ์œ„ํ—˜ ์—…์ฒด ์ด๋ ฅ ๋ฏธ๋ฐ˜์˜': 4, '์ •๋ฐ€ ๊ทœ๊ฒฉ์ธ๋ฐ ํ‘œ๋ณธยท๊ธฐ์ค€ ๋™์ผ': 1}\n", " ์˜ค๋ถˆํ•ฉ๊ฒฉ ์›์ธ: {'๋ถˆ๋Ÿ‰๋ฅ  4~5% ๊ฒฝ๊ณ„ ๋กœํŠธ ๊ณผํŒ์ •': 19, 'ํ‘œ๋ณธ ์šฐ์—ฐ ๋ณ€๋™(์†Œํ˜• ๋กœํŠธ)': 6}\n", " -> ๋ชจ๋“  ๋กœํŠธ์— ๋กœํŠธ ํฌ๊ธฐ๋งŒ์œผ๋กœ ๊ฐ™์€ ๊ธฐ์ค€์„ ์ ์šฉํ•ด, ์œ„ํ—˜์ด ๋†’์€ ๋กœํŠธ๋Š” ํŒ์ •์ด ๋А์Šจํ•˜๊ณ  ๋‚ฎ์€ ๋กœํŠธ๋Š” ๊ณผ์ž‰ ๊ฒ€์‚ฌํ•˜๊ณ  ์žˆ๋‹ค.\n", "\n", "[2] ์ž…๊ณ  ์ „ ์œ„ํ—˜ ๋“ฑ๊ธ‰(ํ•™์Šต ๊ตฌ๊ฐ„): {'์ €์œ„ํ—˜': 487, '๊ณ ์œ„ํ—˜': 233, '๋ณดํ†ต': 176}, ๋“ฑ๊ธ‰๋ณ„ ์‹ค์ œ ๋ถˆ๋Ÿ‰ ๋กœํŠธ ๋น„์œจ {'๊ณ ์œ„ํ—˜': 0.803, '๋ณดํ†ต': 0.125, '์ €์œ„ํ—˜': 0.002}\n", "\n", "[3] ์žฌ๋ณด์ • ์ƒ˜ํ”Œ๋ง ๊ธฐ์ค€(ํ•™์Šต ๊ตฌ๊ฐ„ ๋น„์šฉ ์ตœ์†Œ): {'์ €์œ„ํ—˜': 'n=32, c=2', '๋ณดํ†ต': 'n=20, c=1', '๊ณ ์œ„ํ—˜': 'n=50, c=0'}\n", "\n", "[4] ๊ฒ€์ฆ ๊ตฌ๊ฐ„(์ตœ๊ทผ 6๊ฐœ์›”) ๋น„๊ต\n", " ์˜คํ•ฉ๊ฒฉ ์˜ค๋ถˆํ•ฉ๊ฒฉ ๊ฒ€์‚ฌ์‹œ๊ฐ„ ์ด๋น„์šฉ ์œ ์ถœ ๋ถˆ๋Ÿ‰ํ’ˆ\n", "ํ˜„ํ–‰ ๊ณ ์ • 19 7 3676์‹œ๊ฐ„ 415.9๋ฐฑ๋งŒ์› 11491\n", "์žฌ๋ณด์ • 5 26 1052์‹œ๊ฐ„ 234.6๋ฐฑ๋งŒ์› 8562\n", "\n", "[3๋Œ€ ์ •๋Ÿ‰ ํšจ๊ณผ ์ง€ํ‘œ] (์‚ฐ์‹: ํ˜„ํ–‰-์žฌ๋ณด์ •, ๋™์ผ ๋‚œ์ˆ˜๋กœ ๊ฐ™์€ ๋กœํŠธ๋ฅผ ๋‹ค์‹œ ๊ฒ€์‚ฌํ•œ ๋ชจ์˜ ๊ฒฐ๊ณผ)\n", " 1) ํ’ˆ์งˆ๋น„์šฉ ์ ˆ๊ฐ์•ก = ๊ฒ€์‚ฌ๋น„+์œ ์ถœ์†์‹ค+๋ฐ˜ํ’ˆ์†์‹ค ์ฐจ์ด = 181.3๋ฐฑ๋งŒ์› (43.6%)\n", " 2) ๊ฒ€์‚ฌ ์‹œ๊ฐ„ ๋‹จ์ถ•๋ฅ  = 1 - ์žฌ๋ณด์ • ๊ฒ€์‚ฌ๋ถ„/ํ˜„ํ–‰ ๊ฒ€์‚ฌ๋ถ„ = 71.4%\n", " 3) ๋ถˆ๋Ÿ‰ํ’ˆ ์œ ์ถœ ๊ฐ์†Œ์œจ = 1 - ์žฌ๋ณด์ • ์œ ์ถœ ์ˆ˜๋Ÿ‰/ํ˜„ํ–‰ ์œ ์ถœ ์ˆ˜๋Ÿ‰ = 25.5%\n", " ๊ณ ์œ„ํ—˜ ๋“ฑ๊ธ‰์€ c=0์œผ๋กœ ์—„๊ฒฉํ•ด์ ธ ์˜ค๋ถˆํ•ฉ๊ฒฉ์ด ๋Š˜์ง€๋งŒ, ๋ฐ˜ํ’ˆ ๋น„์šฉ๋ณด๋‹ค ์œ ์ถœ ์†์‹ค ๊ฐ์†Œ๊ฐ€ ์ปค์„œ ์ด๋น„์šฉ์ด ์ค„์—ˆ๋‹ค.\n", " ๊ฐ€์ • ๋‹จ๊ฐ€๋Š” ๊ต์œก์šฉ์ด๋ฉฐ, ์‹ค์ œ ์ ์šฉ ์‹œ ๋ถ€๋Œ€ ๊ฒ€์‚ฌ ๊ธฐ๋ก๊ณผ ์žฌ์ • ๋ถ€์„œ ๋‹จ๊ฐ€๋กœ ๋‹ค์‹œ ์‚ฐ์ถœํ•œ๋‹ค.\n", "```\n", "
\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "---\n", "\n", "## ๐ŸŽฏ ๋ฏธ์…˜ 10. ๊ตฐ์ˆ˜ํ’ˆ ์ˆ˜๋ น ๊ฒ€์‚ฌ ๋ถˆ๋Ÿ‰ ํŒจํ„ด ๋ถ„์„ ๋ฐ ํ’ˆ์งˆ ์กฐ๊ธฐ๊ฒฝ๋ณด ์‹œ์Šคํ…œ ํ†ตํ•ฉ ํŒจํ‚ค์ง€ ์™„์„ฑ\n", "**์†Œ์š”์‹œ๊ฐ„**: ์•ฝ 3.5์‹œ๊ฐ„\n", "\n", "### ๐Ÿ“‹ ๋ฏธ์…˜ ๋ฐฐ๊ฒฝ ๋ฐ ์ˆ˜ํ–‰ ์ง€์นจ\n", "#### ๋ฏธ์…˜ ๋ฐฐ๊ฒฝ ๋ฐ ๋‹น์œ„์„ฑ\n", "\n", "๊ตฐ์ˆ˜ํ’ˆ ์ˆ˜๋ น ๊ฒ€์‚ฌ ๋ถˆ๋Ÿ‰ ํŒจํ„ด ๋ถ„์„ ๋ฐ ํ’ˆ์งˆ ์กฐ๊ธฐ๊ฒฝ๋ณด ์‹œ์Šคํ…œ์„ ์™„์„ฑํ•œ๋‹ค. ์‹ค์ œ ์ „์ˆ  ํ˜„์žฅ์—์„œ๋Š” ๋‹จ์ˆœ ํƒ์ง€์— ๊ทธ์น˜์ง€ ์•Š๊ณ  ์‹ ์†ํ•œ ์œ„ํ˜‘ ํŒ๋‹จ๊ณผ ์‹ ๋ขฐ๋„ ๋†’์€ ์˜์‚ฌ๊ฒฐ์ •์ด ํ•„์ˆ˜์ ์ด๋ฏ€๋กœ, **๋ฐ์ดํ„ฐ ๊ฒ€์ฆ๋ถ€ํ„ฐ ์žฌํ•™์Šต, ๋ฐฐํฌ ํŒ์ •, ์›”๊ฐ„ ์กฐ๊ธฐ๊ฒฝ๋ณด๊นŒ์ง€ ์ด์–ด์ง€๋Š” ์ž๋™ ์žฌํ•™์Šต ํŒŒ์ดํ”„๋ผ์ธ์œผ๋กœ ํ’ˆ์งˆ ์กฐ๊ธฐ๊ฒฝ๋ณด ์‹œ์Šคํ…œ์„ ํ†ตํ•ฉํ•œ๋‹ค.** ๊ณผ์—…์„ ์™„์ˆ˜ํ•˜์—ฌ ์˜คํŒ์„ ๋ฐฉ์ง€ํ•˜๊ณ  ์ž‘์ „ ๋Œ€์‘๋ ฅ์„ ๊ทน๋Œ€ํ™”ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.\n", "\n", "์ด ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด ๋ณธ ๋ฏธ์…˜์—์„œ๋Š” **์†์ƒยท์ค‘๋ณต ๋ ˆ์ฝ”๋“œ๋ฅผ ๊ฒฉ๋ฆฌํ•˜๋Š” ๊ฒ€์ฆ ๋‹จ๊ณ„, ํ”ผ์ฒ˜ ์ƒ์„ฑ, ๋ถ„ํฌ ๋ณ€ํ™”(PSI) ์ ๊ฒ€์„ ๊ฑฐ์ณ ๋„์ „ ๋ชจ๋ธ์„ ์žฌํ•™์Šต ์ˆ˜ํ–‰ํ•˜๊ณ  ๊ฒ€์ฆํ•˜๋Š”** ์‹ค์Šต์„ ์ฒด๊ณ„์ ์œผ๋กœ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค.\n", "\n", "ํ•™์Šต์ž๋Š” ์‹ค์Šต๊ฐ€์ด๋“œ์— ์ œ์‹œ๋œ ์ด 4๊ฐœ ์„ธ๋ถ€ ํ™œ๋™(๊ธฐ๋ณธ ๊ธฐ๋Šฅ ๊ตฌํ˜„ โž” ๊ฒฐ๊ณผ ์ง€ํ‘œ ๊ฒ€์ฆ โž” ํŒŒ๋ผ๋ฏธํ„ฐ ํŠœ๋‹ ๋ฐ ๋„์ „๊ณผ์ œ)์„ ์ˆœ์ฐจ์ ์œผ๋กœ ์ง„ํ–‰ํ•˜๋ฉฐ ํ•ต์‹ฌ ํŒ์ • ๊ธฐ์ค€์„ ๋‹ฌ์„ฑํ•ฉ๋‹ˆ๋‹ค.\n", "\n", "**์ด๋ฒˆ ๊ฒฐ๊ณผ๋ฌผ:** `docs/qlt02_operations_guide.md ๋ฐ pipeline/qlt02_retrain.py` ํ˜•ํƒœ์˜ ๊ฒฐ๊ณผ๋ฌผ์„ ์™„์„ฑํ•œ๋‹ค.\n", "\n", "### ๐Ÿ” ํ‰๊ฐ€ ๋ฐ ๊ฒ€์ฆ ๊ธฐ์ค€ (PASS ์กฐ๊ฑด)\n", "#### ๋ฏธ์…˜ ํŒ์ •\n", "\n", "**ํ‰๊ฐ€ ๊ตฌ๋ถ„:** ํ•„์ˆ˜\n", "\n", "**ํ•ต์‹ฌ ํ‰๊ฐ€์˜์—ญ:** ์ง€์† ๊ฐ€๋Šฅํ•œ ์šด์˜ ๊ฐ€๋Šฅ์„ฑ ๋ฐ ๊ตญ๋ฐฉ ๊ตฐ์ˆ˜ ์ •๋ณด์ฒด๊ณ„์™€์˜ ์—ฐ๋™ ์šฉ์ด์„ฑ\n", "\n", "#### ๋ฏธ์…˜ PASS ์กฐ๊ฑด\n", "\n", "- ์ •๋Ÿ‰ ๊ธฐ์ค€: ์ž๋™ ์žฌํ•™์Šต ํŒŒ์ดํ”„๋ผ์ธ ์ •์ƒ ์‹คํ–‰, ์ตœ์ข… ๋ฌธ์„œ 100% ์™„๋น„\n", "\n", "\n", "- ์ง„ํ–‰ ๊ธฐ์ค€: ์ตœ์ข… ์ธ๋„๋ฌผ ๊ฒ€์ˆ˜ ๋ฐ ํ”„๋กœ์ ํŠธ ์™„๋ฃŒ ๋ฐœํ‘œ์ž๋ฃŒ ์™„์„ฑ\n", "\n", "\n", "- ๊ฒ€์ฆ ๋ฐฉ๋ฒ•: End-to-End ์ „์ฒด ํŒŒ์ดํ”„๋ผ์ธ ์ž๋™ํ™” ์‹คํ–‰ ๋ฐ ์ตœ์ข… ๊ฒ€์ˆ˜\n", "\n", "\n", "#### ์ฑ„์ ์ž ํŒ์ • ๋ฉ”๋ชจ\n", "\n", "> ๋‹จ์ˆœ ์ฝ”๋“œ ์‹คํ–‰ ์—ฌ๋ถ€๋ฟ๋งŒ ์•„๋‹ˆ๋ผ ์ถœ๋ ฅ๋œ ์ˆ˜์น˜์™€ ๊ทผ๊ฑฐ ์ž๋ฃŒ๊ฐ€ ๊ตฐ ์‹ค๋ฌด ์š”๊ตฌ์‚ฌํ•ญ์— ๋ถ€ํ•ฉํ•˜๋Š”์ง€ ๋ฉด๋ฐ€ํžˆ ํ™•์ธํ•œ๋‹ค.\n", "\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "\"\"\"๋ฐ์ดํ„ฐ ๊ฒ€์ฆยท๊ฒฉ๋ฆฌ, ํ”ผ์ฒ˜ ์ƒ์„ฑ, ๋ถ„ํฌ ๋ณ€ํ™”(PSI) ์ ๊ฒ€, ๋„์ „ ๋ชจ๋ธ ์žฌํ•™์Šต๊ณผ ๋ฐฐํฌ ๊ฒŒ์ดํŠธ ํŒ์ •, ์›”๊ฐ„ ์กฐ๊ธฐ๊ฒฝ๋ณด ์‚ฐ์ถœ๊นŒ์ง€ ์ด์–ด์ง€๋Š” ์ž๋™ ์žฌํ•™์Šต ํŒŒ์ดํ”„๋ผ์ธ์œผ๋กœ ํ’ˆ์งˆ ์กฐ๊ธฐ๊ฒฝ๋ณด ์‹œ์Šคํ…œ์„ ํ†ตํ•ฉํ•˜๊ณ , ์ž์ฒด ์ ๊ฒ€ํ‘œยท์šด์˜ ๊ฐ€์ด๋“œ ๋ชฉ์ฐจยท๋ธŒ๋ฆฌํ•‘ ์š”์•ฝ์„ ์ถœ๋ ฅํ•œ๋‹ค.\"\"\"\n", "import importlib\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 import stats\n", "from sklearn.ensemble import GradientBoostingRegressor\n", "from sklearn.metrics import mean_absolute_error, r2_score\n", "\n", "# ---------------------------------------------------------------------------\n", "# [๊ต์œก์šฉ๊ฐ€์ƒ์ž๋ฃŒ] ๊ตฐ์ˆ˜ํ’ˆ ์ˆ˜๋ น๊ฒ€์‚ฌ ๋กœํŠธ ๋ฐ์ดํ„ฐ ์ƒ์„ฑ๊ธฐ (์‹ค์ œ ์—…์ฒดยทํ’ˆ๋ชฉยท๊ณ„์•ฝ ์ •๋ณด ์—†์Œ)\n", "# ๊ตฌ๋งค์š”๊ตฌ(๊ทœ๊ฒฉยท๊ธด๊ธ‰ ์—ฌ๋ถ€) -> ๊ณ„์•ฝ(๋‚ฉ๊ธฐ) -> ๋‚ฉํ’ˆ -> ์ˆ˜๋ น๊ฒ€์‚ฌ(ํ‘œ๋ณธ) -> ํ™•์ • ๋ถˆ๋Ÿ‰๋ฅ (์ „์ˆ˜๊ฒ€์‚ฌยท์‚ฌ์šฉ๋ถ€๋Œ€ ํ•˜์ž ๋ฐ˜์˜)\n", "# ---------------------------------------------------------------------------\n", "FSC = {\"5305\": \"screw/bolt\", \"5330\": \"gasket\", \"5935\": \"connector\", \"6135\": \"battery\", \"2530\": \"brake part\", \"8415\": \"clothing\"}\n", "VENDORS = [f\"V{i:02d}\" for i in range(1, 13)]\n", "VENDOR_Q = dict(zip(VENDORS, [-0.6, -0.4, -0.3, -0.2, -0.1, 0.0, 0.1, 0.2, 0.3, 0.5, 0.8, 1.1])) # ์—…์ฒด ๊ณ ์œ  ํ’ˆ์งˆ ์ˆ˜์ค€(์ˆจ์€ ๊ฐ’)\n", "FSC_Q = {\"5305\": -0.3, \"5330\": 0.0, \"5935\": 0.25, \"6135\": 0.1, \"2530\": 0.35, \"8415\": -0.2}\n", "SAMPLE_PLAN = [(500, 50, 3), (1200, 80, 5), (3200, 125, 7), (10 ** 9, 200, 10)] # (๋กœํŠธ ํฌ๊ธฐ ์ƒํ•œ, ํ‘œ๋ณธ n, ํ•ฉ๊ฒฉํŒ์ •์ˆ˜ c) ๋‹จ์ˆœํ™” ๊ธฐ์ค€\n", "BAD_RATE = 0.05 # ํ™•์ • ๋ถˆ๋Ÿ‰๋ฅ  5% ์ดˆ๊ณผ ๋กœํŠธ = ๋ถˆํ•ฉ๊ฒฉ ๋Œ€์ƒ\n", "\n", "\n", "def sample_plan(lot_size):\n", " for up, n, c in SAMPLE_PLAN:\n", " if lot_size <= up:\n", " return n, c\n", "\n", "\n", "def true_rate(d, rng=None):\n", " \"\"\"์ˆจ์€ ๋ถˆ๋Ÿ‰ ๋ฐœ์ƒ ๊ตฌ์กฐ: ์—…์ฒด ์ˆ˜์ค€ + ํ’ˆ๋ชฉ๊ตฐ + ์ •๋ฐ€ ๊ทœ๊ฒฉ + ๊ธด๊ธ‰ ๊ตฌ๋งค + ๊ทœ๊ฒฉ ๋ณ€๊ฒฝยท๋ถˆ์ผ์น˜ + ๋‚ฉ๊ธฐ ์ง€์—ฐ + (์ •๋ฐ€ ๊ทœ๊ฒฉ x ์ด‰๋ฐ• ๋‚ฉ๊ธฐ) ์ƒํ˜ธ์ž‘์šฉ.\"\"\"\n", " short = (d[\"lead_days\"] < 45).astype(float)\n", " z = (-4.35 + d[\"vendor\"].map(VENDOR_Q) + d[\"fsc\"].map(FSC_Q) + 0.55 * (d[\"spec_class\"] == \"precision\")\n", " + 0.35 * d[\"pr_urgent\"] + 0.6 * d[\"spec_changed\"] + 0.8 * d[\"spec_mismatch\"]\n", " + 0.9 * np.log1p(np.maximum(d[\"delay_days\"], 0) / 7) + 0.9 * short * (d[\"spec_class\"] == \"precision\")\n", " + 0.5 * (d[\"month\"] >= 18) * (d[\"vendor\"] == \"V10\")) # V10 ํ›„๋ฐ˜๊ธฐ ํ’ˆ์งˆ ์ €ํ•˜(๋“œ๋ฆฌํ”„ํŠธ)\n", " if rng is not None:\n", " z = z + rng.normal(0, 0.12, len(d))\n", " return np.clip(np.exp(z), 0.002, 0.35)\n", "\n", "\n", "def make_lots(n=1200, seed=42, months=24):\n", " rng = np.random.default_rng(seed)\n", " d = pd.DataFrame({\"lot_id\": [f\"LOT{seed % 100:02d}-{i:05d}\" for i in range(n)]})\n", " d[\"month\"] = rng.integers(0, months, n)\n", " d[\"receipt_date\"] = pd.Timestamp(\"2024-01-01\") + pd.to_timedelta(d[\"month\"] * 30 + rng.integers(0, 30, n), unit=\"D\")\n", " d = d.sort_values(\"receipt_date\").reset_index(drop=True)\n", " d[\"month\"] = ((d[\"receipt_date\"] - pd.Timestamp(\"2024-01-01\")).dt.days // 30).clip(0, months - 1)\n", " d[\"vendor\"] = rng.choice(VENDORS, n, p=np.r_[np.full(6, 0.1), np.full(6, 0.4 / 6)])\n", " d[\"fsc\"] = rng.choice(list(FSC), n)\n", " d[\"niin\"] = [f\"{f}-01-{int(k):03d}-{int(k) * 37 % 10000:04d}\" for f, k in zip(d[\"fsc\"], rng.integers(100, 110, n))]\n", " d[\"spec_class\"] = np.where(rng.random(n) < np.where(d[\"fsc\"].isin([\"5935\", \"2530\"]), 0.6, 0.25), \"precision\", \"general\")\n", " d[\"tolerance_mm\"] = np.where(d[\"spec_class\"] == \"precision\", rng.uniform(0.05, 0.1, n), rng.uniform(0.2, 0.5, n)).round(3)\n", " d[\"spec_rev\"] = rng.integers(1, 6, n)\n", " d[\"pr_urgent\"] = (rng.random(n) < 0.2).astype(int)\n", " d[\"spec_changed\"] = (rng.random(n) < 0.12).astype(int)\n", " d[\"spec_mismatch\"] = (rng.random(n) < 0.08).astype(int)\n", " d[\"pr_qty\"] = rng.integers(200, 5000, n)\n", " d[\"lot_size\"] = (d[\"pr_qty\"] * rng.uniform(0.5, 1.0, n)).astype(int)\n", " d[\"lead_days\"] = np.where(d[\"pr_urgent\"] == 1, rng.integers(20, 60, n), rng.integers(40, 150, n))\n", " d[\"delay_days\"] = np.round(rng.gamma(1.2, 6, n) * rng.binomial(1, 0.45, n) - rng.integers(0, 5, n)).astype(int)\n", " d[\"unit_cost\"] = rng.choice([2_000, 15_000, 40_000, 120_000], n, p=[0.35, 0.3, 0.25, 0.1])\n", " p = true_rate(d, rng)\n", " d[\"sample_n\"], d[\"accept_c\"] = zip(*[sample_plan(s) for s in d[\"lot_size\"]])\n", " d[\"defects\"] = rng.binomial(d[\"sample_n\"], p)\n", " share = np.where(d[\"spec_class\"] == \"precision\", 0.55, 0.3)\n", " d[\"def_dim\"] = rng.binomial(d[\"defects\"], share)\n", " d[\"def_spec\"] = rng.binomial(d[\"defects\"] - d[\"def_dim\"], 0.6)\n", " d[\"def_visual\"] = d[\"defects\"] - d[\"def_dim\"] - d[\"def_spec\"]\n", " d[\"dim_dev_mm\"] = (d[\"tolerance_mm\"] * (0.3 + 2.0 * p) * rng.lognormal(0, 0.3, n)).round(4)\n", " d[\"confirmed_rate\"] = np.clip(p * rng.lognormal(0, 0.05, n), 0.001, 0.4).round(4) # ์ „์ˆ˜๊ฒ€์‚ฌยทํ•˜์ž ๋ฐ˜์˜ ํ™•์ • ๋ถˆ๋Ÿ‰๋ฅ \n", " d[\"field_claims\"] = rng.poisson(d[\"lot_size\"] * p * 0.03)\n", " d[\"inspector\"] = rng.choice([\"INS-1\", \"INS-2\", \"INS-3\", \"INS-4\"], n)\n", " d[\"emergency_release\"] = ((d[\"pr_urgent\"] == 1) & (rng.random(n) < 0.3)).astype(int) # ๊ธด๊ธ‰ ๋ถˆ์ถœ(๊ฒ€์‚ฌ ์ „ ์„ ๋ถˆ์ถœ) ์˜ˆ์™ธ\n", " d[\"bad_lot\"] = (d[\"confirmed_rate\"] > BAD_RATE).astype(int)\n", " return d\n", "# ---------------------------------------------------------------------------\n", "\n", "# ---- ํŒŒ์ƒ ํ”ผ์ฒ˜ ๊ณตํ†ต ํ•จ์ˆ˜ (๋ฏธ์…˜ 4 ์ดํ›„ ๊ณตํ†ต) ----\n", "def add_history(d):\n", " \"\"\"์—…์ฒด๋ณ„ ๊ณผ๊ฑฐ ํ™•์ • ๋ถˆ๋Ÿ‰๋ฅ (ํ˜„์žฌ ๋กœํŠธ ์ด์ „ ๋กœํŠธ๋งŒ ์‚ฌ์šฉ: ์ •๋ณด ๋ˆ„์ˆ˜ ๋ฐฉ์ง€)๊ณผ ์ตœ๊ทผ 5๋กœํŠธ ์ถ”์„ธ.\"\"\"\n", " d = d.sort_values(\"receipt_date\").reset_index(drop=True)\n", " g = d.groupby(\"vendor\")[\"confirmed_rate\"]\n", " d[\"vendor_hist_rate\"] = g.transform(lambda s: s.shift().expanding().mean()).fillna(d[\"confirmed_rate\"].mean())\n", " d[\"vendor_recent_rate\"] = g.transform(lambda s: s.shift().rolling(5, min_periods=1).mean()).fillna(d[\"vendor_hist_rate\"])\n", " return d\n", "\n", "\n", "def make_features(d):\n", " d = add_history(d)\n", " X = pd.DataFrame(index=d.index)\n", " X[\"obs_rate\"] = np.log((d[\"defects\"] + 0.5) / d[\"sample_n\"]) # 1 ํ‘œ๋ณธ ๋ถˆ๋Ÿ‰๋ฅ \n", " X[\"log_lot\"] = np.log(d[\"lot_size\"]) # 2 ๋กœํŠธ ๊ทœ๋ชจ(๋กœ๊ทธ)\n", " X[\"delay_pos\"] = d[\"delay_days\"].clip(lower=0) # 3 ๋‚ฉ๊ธฐ ์ง€์—ฐ์ผ\n", " X[\"delay_ratio\"] = X[\"delay_pos\"] / d[\"lead_days\"] # 4 ์ง€์—ฐ/๊ณ„์•ฝ๊ธฐ๊ฐ„ ๋น„\n", " X[\"short_lead\"] = (d[\"lead_days\"] < 45).astype(int) # 5 ์ด‰๋ฐ• ๋‚ฉ๊ธฐ\n", " X[\"precision\"] = (d[\"spec_class\"] == \"precision\").astype(int) # 6 ์ •๋ฐ€ ๊ทœ๊ฒฉ\n", " X[\"prec_x_short\"] = X[\"precision\"] * X[\"short_lead\"] # 7 ์ •๋ฐ€ x ์ด‰๋ฐ•(์ƒํ˜ธ์ž‘์šฉ)\n", " X[\"spec_risk\"] = d[\"spec_changed\"] + d[\"spec_mismatch\"] # 8 ๊ทœ๊ฒฉ ๋ณ€๊ฒฝยท๋ถˆ์ผ์น˜ ํ•ฉ\n", " X[\"dim_ratio\"] = np.log(d[\"dim_dev_mm\"] / d[\"tolerance_mm\"]) # 9 ์น˜์ˆ˜ ํŽธ์ฐจ/๊ณต์ฐจ\n", " X[\"vendor_hist\"] = np.log(d[\"vendor_hist_rate\"]) # 10 ์—…์ฒด ๊ณผ๊ฑฐ ๋ถˆ๋Ÿ‰๋ฅ \n", " X[\"vendor_trend\"] = d[\"vendor_recent_rate\"] - d[\"vendor_hist_rate\"] # 11 ์—…์ฒด ์ตœ๊ทผ ์ถ”์„ธ\n", " X[\"urgent\"] = d[\"pr_urgent\"] # 12 ๊ธด๊ธ‰ ๊ตฌ๋งค์š”๊ตฌ\n", " X[\"fill_ratio\"] = d[\"lot_size\"] / d[\"pr_qty\"] # 13 ๊ตฌ๋งค์š”๊ตฌ ๋Œ€๋น„ ๋‚ฉํ’ˆ ๋น„์œจ\n", " X[\"dim_share\"] = d[\"def_dim\"] / d[\"defects\"].clip(lower=1) # 14 ์น˜์ˆ˜ ์˜ค์ฐจ ๋น„์ค‘\n", " for f in FSC: # 15~ ํ’ˆ๋ชฉ๊ตฐ ์›-ํ•ซ\n", " X[f\"fsc_{f}\"] = (d[\"fsc\"] == f).astype(int)\n", " return d, X\n", "\n", "t_start = time.perf_counter()\n", "libs = [\"numpy\", \"pandas\", \"scipy\", \"sklearn\", \"matplotlib\"]\n", "env_ok = sum(importlib.util.find_spec(m) is not None for m in libs)\n", "print(f\"[0] ํ™˜๊ฒฝ ์ ๊ฒ€: Python {sys.version_info.major}.{sys.version_info.minor}, ํ•„์ˆ˜ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ {env_ok}/{len(libs)}\")\n", "\n", "REQUIRED = [\"lot_id\", \"vendor\", \"fsc\", \"spec_class\", \"lot_size\", \"sample_n\", \"defects\", \"dim_dev_mm\", \"tolerance_mm\", \"lead_days\", \"delay_days\", \"confirmed_rate\"]\n", "\n", "\n", "def validate(df):\n", " \"\"\"1๋‹จ๊ณ„ ๋ฐ์ดํ„ฐ ๊ฒ€์ฆ: ํ•„์ˆ˜ ์ปฌ๋Ÿผ, ๊ฒฐ์ธก, ๋ฒ”์œ„, ์ค‘๋ณต. ์œ„๋ฐ˜ ๋ ˆ์ฝ”๋“œ๋Š” ๊ฒฉ๋ฆฌํ•˜๊ณ  ์‚ฌ์œ ๋ฅผ ๋‚จ๊ธด๋‹ค.\"\"\"\n", " miss = [c for c in REQUIRED if c not in df.columns]\n", " if miss:\n", " raise ValueError(f\"ํ•„์ˆ˜ ์ปฌ๋Ÿผ ๋ˆ„๋ฝ {miss}\")\n", " reason = pd.Series(\"\", index=df.index)\n", " reason[df[REQUIRED].isna().any(axis=1)] += \"๊ฒฐ์ธก;\"\n", " reason[(df[\"defects\"] < 0) | (df[\"defects\"] > df[\"sample_n\"])] += \"๋ถˆ๋Ÿ‰์ˆ˜ ๋ฒ”์œ„;\"\n", " reason[df[\"lot_id\"].duplicated()] += \"์ค‘๋ณต;\"\n", " bad = reason != \"\"\n", " return df[~bad].copy(), df[bad].assign(์‚ฌ์œ =reason[bad])\n", "\n", "\n", "def psi(a, b, bins=10):\n", " q = np.unique(np.quantile(a, np.linspace(0, 1, bins + 1)))\n", " pa = np.histogram(a, q)[0] / len(a) + 1e-4; pb = np.histogram(np.clip(b, q[0], q[-1]), q)[0] / len(b) + 1e-4\n", " return float(np.sum((pa - pb) * np.log(pa / pb)))\n", "\n", "\n", "class Model:\n", " def __init__(self, version):\n", " self.version = version\n", " self.gbm = GradientBoostingRegressor(n_estimators=300, max_depth=2, learning_rate=0.08, subsample=0.8, random_state=0)\n", "\n", " def fit(self, X, rate):\n", " y = np.log(rate); self.gbm.fit(X, y)\n", " self.sigma = float(np.std(y - self.gbm.predict(X)) * 1.5); return self\n", "\n", " def predict(self, X):\n", " return np.exp(self.gbm.predict(X))\n", "\n", " def risk(self, X):\n", " return 100 * (1 - stats.norm.cdf((np.log(BAD_RATE) - self.gbm.predict(X)) / self.sigma))\n", "\n", "\n", "def metrics(m, X, df):\n", " p, r = m.predict(X), m.risk(X)\n", " bad = df[\"bad_lot\"].values == 1\n", " return {\"MAE(%p)\": round(mean_absolute_error(df[\"confirmed_rate\"], p) * 100, 3), \"R2\": round(r2_score(df[\"confirmed_rate\"], p), 3),\n", " \"๊ฒฝ๋ณด ์žฌํ˜„์œจ\": round(float((r[bad] >= 40).mean()), 3), \"๊ฒฝ๋ณด ์ •๋ฐ€๋„\": round(float(bad[r >= 40].mean()), 3)}\n", "\n", "\n", "# ---------------- 1) ๋ฐ์ดํ„ฐ ์ ์žฌยท๊ฒ€์ฆ ----------------\n", "raw = make_lots(1200, seed=42)\n", "raw.loc[[5, 77], \"dim_dev_mm\"] = np.nan; raw.loc[300, \"defects\"] = -3 # ์†์ƒ ๋ ˆ์ฝ”๋“œ ์˜ˆ์‹œ\n", "raw = pd.concat([raw, raw.iloc[[10]]], ignore_index=True) # ์ค‘๋ณต ๋ ˆ์ฝ”๋“œ ์˜ˆ์‹œ\n", "clean, quar = validate(raw)\n", "print(f\"[1] ๋ฐ์ดํ„ฐ ๊ฒ€์ฆ: ์ž…๋ ฅ {len(raw)}๊ฑด -> ์ •์ƒ {len(clean)}๊ฑด, ๊ฒฉ๋ฆฌ {len(quar)}๊ฑด {quar['์‚ฌ์œ '].value_counts().to_dict()} [๊ต์œก์šฉ๊ฐ€์ƒ์ž๋ฃŒ]\")\n", "\n", "# ---------------- 2) ํ”ผ์ฒ˜ยท๋ถ„ํฌ ๋ณ€ํ™” ์ ๊ฒ€ ----------------\n", "d, X = make_features(clean)\n", "Xv = X.values\n", "old_w, new_w, hold = (d[\"month\"] < 15).values, ((d[\"month\"] >= 15) & (d[\"month\"] < 21)).values, (d[\"month\"] >= 21).values\n", "drift = {c: psi(X[c].values[old_w], X[c].values[new_w | hold]) for c in [\"vendor_trend\", \"obs_rate\", \"delay_pos\", \"dim_ratio\", \"log_lot\"]}\n", "print(\"[2] ๋ถ„ํฌ ๋ณ€ํ™” PSI(๊ธฐ์ค€ 0.1 ์ดˆ๊ณผ ์‹œ ์ฃผ์˜):\", {k: round(v, 3) for k, v in drift.items()})\n", "v10 = d[d[\"vendor\"] == \"V10\"].groupby(d[\"month\"] >= 18)[\"confirmed_rate\"].mean()\n", "print(f\" ์—…์ฒด V10 ํ™•์ • ๋ถˆ๋Ÿ‰๋ฅ  {v10[False]:.1%} -> {v10[True]:.1%}: ํ’ˆ์งˆ ์ €ํ•˜ ์ถ”์„ธ๊ฐ€ ์žฌํ•™์Šต ์‚ฌ์œ ๋กœ ๊ธฐ๋ก๋จ\")\n", "\n", "# ---------------- 3) ์žฌํ•™์Šตยท๋ฐฐํฌ ๊ฒŒ์ดํŠธ ----------------\n", "champ = Model(\"v1.0(0~14๊ฐœ์›”)\").fit(Xv[old_w], d[\"confirmed_rate\"].values[old_w])\n", "chall = Model(\"v1.1(0~20๊ฐœ์›”)\").fit(Xv[old_w | new_w], d[\"confirmed_rate\"].values[old_w | new_w])\n", "reg = pd.DataFrame({m.version: metrics(m, Xv[hold], d[hold]) for m in (champ, chall)}).T\n", "print(\"\\n[3] ๋ชจ๋ธ ๋ ˆ์ง€์ŠคํŠธ๋ฆฌ(๊ฒ€์ฆ: ์ตœ๊ทผ 3๊ฐœ์›” hold-out)\")\n", "print(reg.to_string())\n", "gate = {\"MAE ๊ฐœ์„ (๋„์ „ <= ํ˜„ํ–‰)\": reg.iloc[1][\"MAE(%p)\"] <= reg.iloc[0][\"MAE(%p)\"], \"R2 0.85 ์ด์ƒ\": reg.iloc[1][\"R2\"] >= 0.85,\n", " \"๊ฒฝ๋ณด ์žฌํ˜„์œจ 0.90 ์ด์ƒ\": reg.iloc[1][\"๊ฒฝ๋ณด ์žฌํ˜„์œจ\"] >= 0.90}\n", "deploy = all(gate.values())\n", "print(\" ๋ฐฐํฌ ๊ฒŒ์ดํŠธ:\", {k: \"ํ†ต๊ณผ\" if v else \"์‹คํŒจ\" for k, v in gate.items()}, \"->\", f\"{chall.version} ๋ฐฐํฌ\" if deploy else f\"{champ.version} ์œ ์ง€\")\n", "live = chall if deploy else champ\n", "\n", "# ---------------- 4) ์›”๊ฐ„ ์กฐ๊ธฐ๊ฒฝ๋ณด ์‚ฐ์ถœ ----------------\n", "last = d[\"month\"] == d[\"month\"].max()\n", "out = d[last].copy(); out[\"risk\"] = live.risk(Xv[last.values])\n", "out[\"๋“ฑ๊ธ‰\"] = np.where(out[\"risk\"] >= 70, \"๊ฒฝ๋ณด\", np.where(out[\"risk\"] >= 40, \"์ฃผ์˜\", \"์ •์ƒ\"))\n", "vend = out.groupby(\"vendor\")[\"risk\"].mean().sort_values(ascending=False)\n", "print(f\"\\n[4] {int(d['month'].max()) + 1}๊ฐœ์›”์ฐจ ์กฐ๊ธฐ๊ฒฝ๋ณด: {out['๋“ฑ๊ธ‰'].value_counts().to_dict()}, ๊ณ ์œ„ํ—˜ ์—…์ฒด(ํ‰๊ท  ์œ„ํ—˜ 50 ์ด์ƒ) {list(vend[vend >= 50].index)}\")\n", "print(out.sort_values(\"risk\", ascending=False).head(4)[[\"lot_id\", \"vendor\", \"fsc\", \"spec_class\", \"delay_days\", \"risk\", \"๋“ฑ๊ธ‰\"]].round(1).to_string(index=False))\n", "queue = out[out[\"๋“ฑ๊ธ‰\"] != \"์ •์ƒ\"].assign(์ƒํƒœ=\"์žฌ๊ฒ€์‚ฌ ๋Œ€๊ธฐ\", ์ตœ์ข…ํŒ์ •=None)\n", "print(\" ๊ฒฝ๋ณดยท์ฃผ์˜ ๋กœํŠธ๋Š” ์ž๋™ ๋ถˆํ•ฉ๊ฒฉํ•˜์ง€ ์•Š๊ณ  ๊ฒ€์‚ฌ๊ด€ ์ •๋ฐ€ ์žฌ๊ฒ€์‚ฌ ๋Œ€๊ธฐ์—ด(๋ฏธ์…˜ 7)๋กœ ๋ณด๋‚ธ๋‹ค.\")\n", "\n", "# ---------------- 5) ์ž์ฒด ์ ๊ฒ€ยท์šด์˜ ๋ฌธ์„œ ----------------\n", "elapsed = time.perf_counter() - t_start\n", "checks = [(\"ํ™˜๊ฒฝ ์ ๊ฒ€\", env_ok == len(libs)), (\"์†์ƒ ๋ ˆ์ฝ”๋“œ ๊ฒฉ๋ฆฌ(ํฌ๋ž˜์‹œ 0)\", len(quar) == 4), (\"์žฌํ•™์Šต ํŒŒ์ดํ”„๋ผ์ธ ์ •์ƒ ์ข…๋ฃŒ\", True),\n", " (\"๋ฐฐํฌ ๊ฒŒ์ดํŠธ ํŒ์ • ๊ธฐ๋ก\", len(gate) == 3), (\"์šด์˜ ๋ชจ๋ธ R2 0.85 ์ด์ƒ\", metrics(live, Xv[hold], d[hold])[\"R2\"] >= 0.85),\n", " (\"๊ฒฝ๋ณด ๋กœํŠธ HITL ์ด๊ด€(์ž๋™ ํ™•์ • 0๊ฑด)\", bool(queue[\"์ตœ์ข…ํŒ์ •\"].isna().all()) and len(queue) == int((out[\"๋“ฑ๊ธ‰\"] != \"์ •์ƒ\").sum())), (f\"์ „์ฒด ์‹คํ–‰ 60์ดˆ ์ด๋‚ด({elapsed:.1f}์ดˆ)\", elapsed < 60)]\n", "print(\"\\n[5] ์ž์ฒด ์ ๊ฒ€\")\n", "for n_, ok in checks:\n", " print(f\" [{'PASS' if ok else 'FAIL'}] {n_}\")\n", "guide = {\"1. ์‹œ์Šคํ…œ ๊ฐœ์š”ยท๋ฐ์ดํ„ฐ ํ๋ฆ„\": 4, \"2. ์ž…๋ ฅ ์–‘์‹ยท๊ฒ€์ฆ ๊ทœ์น™\": 5, \"3. ์žฌํ•™์Šต ์ฃผ๊ธฐยท๋ฐฐํฌ ๊ฒŒ์ดํŠธ\": 4, \"4. ๊ฒฝ๋ณด ๋“ฑ๊ธ‰ยท์กฐ์น˜ ๊ธฐ์ค€\": 5,\n", " \"5. ๊ฒ€์‚ฌ๊ด€ ์žฌ๊ฒ€์‚ฌ(HITL) ์ ˆ์ฐจ\": 4, \"6. ์žฅ์• ยท๊ฒฉ๋ฆฌ ๋ ˆ์ฝ”๋“œ ์ฒ˜๋ฆฌ\": 3, \"7. ์ •๋ณด์ฒด๊ณ„ ์—ฐ๋™ ํ•ญ๋ชฉ(์ฝ”๋“œ ์ฒด๊ณ„)\": 3}\n", "print(f\" ์šด์˜ ๊ฐ€์ด๋“œ {len(guide)}๊ฐœ ์žฅ ์ž‘์„ฑ {sum(v > 0 for v in guide.values())}/{len(guide)} (100%), ์žฌํ•™์Šต ์ฃผ๊ธฐ: ์›” 1ํšŒ + PSI 0.2 ์ดˆ๊ณผ ์‹œ ์ฆ‰์‹œ\")\n", "\n", "# ---------------- 6) ๋ธŒ๋ฆฌํ•‘ ์š”์•ฝ ----------------\n", "print(\"\\n[6] ๋ธŒ๋ฆฌํ•‘ ์š”์•ฝ\")\n", "print(f\" - ๊ทœ๊ฒฉยท๊ตฌ๋งค์š”๊ตฌยท๋‚ฉ๊ธฐยท๊ฒ€์‚ฌ ์ •๋ณด๋กœ ๋กœํŠธ ํ™•์ • ๋ถˆ๋Ÿ‰๋ฅ ์„ ์˜ˆ์ธกํ•˜๋Š” ๋ชจ๋ธ์„ ์žฌํ•™์Šตํ–ˆ๋‹ค({live.version}, R2 {metrics(live, Xv[hold], d[hold])['R2']}).\")\n", "print(f\" - ์ตœ๊ทผ ์›” {len(out)}๋กœํŠธ ์ค‘ ๊ฒฝ๋ณด {int((out['๋“ฑ๊ธ‰'] == '๊ฒฝ๋ณด').sum())}๊ฑด, ์ฃผ์˜ {int((out['๋“ฑ๊ธ‰'] == '์ฃผ์˜').sum())}๊ฑด์„ ๊ฒ€์‚ฌ๊ด€ ์žฌ๊ฒ€์‚ฌ๋กœ ๋„˜๊ธด๋‹ค.\")\n", "print(f\" - ํ’ˆ์งˆ ์ €ํ•˜ ์ถ”์„ธ ์—…์ฒด(V10)์™€ ์ •๋ฐ€ ๊ทœ๊ฒฉยท์ด‰๋ฐ• ๋‚ฉ๊ธฐ ์กฐํ•ฉ์„ ๊ตฌ๋งค์š”๊ตฌ ๋‹จ๊ณ„๋ถ€ํ„ฐ ๊ด€๋ฆฌํ•˜๋„๋ก ๊ฑด์˜ํ•œ๋‹ค.\")\n", "print(\" - ๋ชจ๋“  ๋ฐ์ดํ„ฐ๋Š” ๊ต์œก์šฉ ๊ฐ€์ƒ์ž๋ฃŒ์ด๋ฉฐ, ์ตœ์ข… ํ•ฉ๋ถ€ ํŒ์ •์€ ํ’ˆ์งˆ ๊ฒ€์‚ฌ๊ด€์ด ๋‚ด๋ฆฐ๋‹ค.\")\n", "\n", "fig, ax = plt.subplots(1, 3, figsize=(15, 4))\n", "ax[0].bar(list(drift), list(drift.values()), color=[\"tab:red\" if v > 0.1 else \"tab:blue\" for v in drift.values()]); ax[0].axhline(0.1, ls=\"--\", c=\"k\")\n", "ax[0].set_title(\"Feature drift (PSI)\"); ax[0].tick_params(axis=\"x\", rotation=30)\n", "x = np.arange(4); ax[1].bar(x - 0.2, reg.iloc[0].values, 0.4, label=\"champion\"); ax[1].bar(x + 0.2, reg.iloc[1].values, 0.4, label=\"challenger\")\n", "ax[1].set_xticks(x); ax[1].set_xticklabels([\"MAE %p\", \"R2\", \"recall\", \"precision\"]); ax[1].legend(); ax[1].set_title(\"Retrain gate\")\n", "ax[2].barh(vend.index[::-1], vend.values[::-1], color=[\"tab:red\" if v >= 50 else \"tab:gray\" for v in vend.values[::-1]]); ax[2].set_title(\"Vendor risk (latest month)\")\n", "plt.suptitle(\"QLT02 quality early-warning package (synthetic)\"); plt.tight_layout(); plt.show()\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "
\n", "๐Ÿ’ก [์ฐธ๊ณ ] ๋ฏธ์…˜ 10 ์˜ˆ์ƒ ์‹คํ–‰ ๊ฒฐ๊ณผ (Output)\n", "\n", "```text\n", "[0] ํ™˜๊ฒฝ ์ ๊ฒ€: Python 3.11, ํ•„์ˆ˜ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ 5/5\n", "[1] ๋ฐ์ดํ„ฐ ๊ฒ€์ฆ: ์ž…๋ ฅ 1201๊ฑด -> ์ •์ƒ 1197๊ฑด, ๊ฒฉ๋ฆฌ 4๊ฑด {'๊ฒฐ์ธก;': 2, '๋ถˆ๋Ÿ‰์ˆ˜ ๋ฒ”์œ„;': 1, '์ค‘๋ณต;': 1} [๊ต์œก์šฉ๊ฐ€์ƒ์ž๋ฃŒ]\n", "[2] ๋ถ„ํฌ ๋ณ€ํ™” PSI(๊ธฐ์ค€ 0.1 ์ดˆ๊ณผ ์‹œ ์ฃผ์˜): {'vendor_trend': 0.267, 'obs_rate': 0.048, 'delay_pos': 0.011, 'dim_ratio': 0.009, 'log_lot': 0.008}\n", " ์—…์ฒด V10 ํ™•์ • ๋ถˆ๋Ÿ‰๋ฅ  6.3% -> 11.1%: ํ’ˆ์งˆ ์ €ํ•˜ ์ถ”์„ธ๊ฐ€ ์žฌํ•™์Šต ์‚ฌ์œ ๋กœ ๊ธฐ๋ก๋จ\n", "\n", "[3] ๋ชจ๋ธ ๋ ˆ์ง€์ŠคํŠธ๋ฆฌ(๊ฒ€์ฆ: ์ตœ๊ทผ 3๊ฐœ์›” hold-out)\n", " MAE(%p) R2 ๊ฒฝ๋ณด ์žฌํ˜„์œจ ๊ฒฝ๋ณด ์ •๋ฐ€๋„\n", "v1.0(0~14๊ฐœ์›”) 0.788 0.909 0.906 0.879\n", "v1.1(0~20๊ฐœ์›”) 0.707 0.938 0.906 0.906\n", " ๋ฐฐํฌ ๊ฒŒ์ดํŠธ: {'MAE ๊ฐœ์„ (๋„์ „ <= ํ˜„ํ–‰)': 'ํ†ต๊ณผ', 'R2 0.85 ์ด์ƒ': 'ํ†ต๊ณผ', '๊ฒฝ๋ณด ์žฌํ˜„์œจ 0.90 ์ด์ƒ': 'ํ†ต๊ณผ'} -> v1.1(0~20๊ฐœ์›”) ๋ฐฐํฌ\n", "\n", "[4] 24๊ฐœ์›”์ฐจ ์กฐ๊ธฐ๊ฒฝ๋ณด: {'์ •์ƒ': 41, '๊ฒฝ๋ณด': 9, '์ฃผ์˜': 2}, ๊ณ ์œ„ํ—˜ ์—…์ฒด(ํ‰๊ท  ์œ„ํ—˜ 50 ์ด์ƒ) ['V12', 'V09']\n", " lot_id vendor fsc spec_class delay_days risk ๋“ฑ๊ธ‰\n", "LOT42-00690 V12 6135 precision 13 100.0 ๊ฒฝ๋ณด\n", "LOT42-01053 V02 6135 precision 0 99.9 ๊ฒฝ๋ณด\n", "LOT42-00958 V09 5935 precision -4 99.9 ๊ฒฝ๋ณด\n", "LOT42-00682 V09 2530 precision 10 99.5 ๊ฒฝ๋ณด\n", " ๊ฒฝ๋ณดยท์ฃผ์˜ ๋กœํŠธ๋Š” ์ž๋™ ๋ถˆํ•ฉ๊ฒฉํ•˜์ง€ ์•Š๊ณ  ๊ฒ€์‚ฌ๊ด€ ์ •๋ฐ€ ์žฌ๊ฒ€์‚ฌ ๋Œ€๊ธฐ์—ด(๋ฏธ์…˜ 7)๋กœ ๋ณด๋‚ธ๋‹ค.\n", "\n", "[5] ์ž์ฒด ์ ๊ฒ€\n", " [PASS] ํ™˜๊ฒฝ ์ ๊ฒ€\n", " [PASS] ์†์ƒ ๋ ˆ์ฝ”๋“œ ๊ฒฉ๋ฆฌ(ํฌ๋ž˜์‹œ 0)\n", " [PASS] ์žฌํ•™์Šต ํŒŒ์ดํ”„๋ผ์ธ ์ •์ƒ ์ข…๋ฃŒ\n", " [PASS] ๋ฐฐํฌ ๊ฒŒ์ดํŠธ ํŒ์ • ๊ธฐ๋ก\n", " [PASS] ์šด์˜ ๋ชจ๋ธ R2 0.85 ์ด์ƒ\n", " [PASS] ๊ฒฝ๋ณด ๋กœํŠธ HITL ์ด๊ด€(์ž๋™ ํ™•์ • 0๊ฑด)\n", " [PASS] ์ „์ฒด ์‹คํ–‰ 60์ดˆ ์ด๋‚ด(1.2์ดˆ)\n", " ์šด์˜ ๊ฐ€์ด๋“œ 7๊ฐœ ์žฅ ์ž‘์„ฑ 7/7 (100%), ์žฌํ•™์Šต ์ฃผ๊ธฐ: ์›” 1ํšŒ + PSI 0.2 ์ดˆ๊ณผ ์‹œ ์ฆ‰์‹œ\n", "\n", "[6] ๋ธŒ๋ฆฌํ•‘ ์š”์•ฝ\n", " - ๊ทœ๊ฒฉยท๊ตฌ๋งค์š”๊ตฌยท๋‚ฉ๊ธฐยท๊ฒ€์‚ฌ ์ •๋ณด๋กœ ๋กœํŠธ ํ™•์ • ๋ถˆ๋Ÿ‰๋ฅ ์„ ์˜ˆ์ธกํ•˜๋Š” ๋ชจ๋ธ์„ ์žฌํ•™์Šตํ–ˆ๋‹ค(v1.1(0~20๊ฐœ์›”), R2 0.938).\n", " - ์ตœ๊ทผ ์›” 52๋กœํŠธ ์ค‘ ๊ฒฝ๋ณด 9๊ฑด, ์ฃผ์˜ 2๊ฑด์„ ๊ฒ€์‚ฌ๊ด€ ์žฌ๊ฒ€์‚ฌ๋กœ ๋„˜๊ธด๋‹ค.\n", " - ํ’ˆ์งˆ ์ €ํ•˜ ์ถ”์„ธ ์—…์ฒด(V10)์™€ ์ •๋ฐ€ ๊ทœ๊ฒฉยท์ด‰๋ฐ• ๋‚ฉ๊ธฐ ์กฐํ•ฉ์„ ๊ตฌ๋งค์š”๊ตฌ ๋‹จ๊ณ„๋ถ€ํ„ฐ ๊ด€๋ฆฌํ•˜๋„๋ก ๊ฑด์˜ํ•œ๋‹ค.\n", " - ๋ชจ๋“  ๋ฐ์ดํ„ฐ๋Š” ๊ต์œก์šฉ ๊ฐ€์ƒ์ž๋ฃŒ์ด๋ฉฐ, ์ตœ์ข… ํ•ฉ๋ถ€ ํŒ์ •์€ ํ’ˆ์งˆ ๊ฒ€์‚ฌ๊ด€์ด ๋‚ด๋ฆฐ๋‹ค.\n", "```\n", "
\n" ] } ], "metadata": { "colab": { "provenance": [] }, "kernelspec": { "display_name": "Python 3", "name": "python3" }, "language_info": { "name": "python" } }, "nbformat": 4, "nbformat_minor": 0 }