{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# ๐ฏ [MNT06] AI ๊ธฐ๋ฐ ๋ถํ ํ๊ธฐ์จ ์ฐ์ ยท์์ธก ๋ชจ๋ธ\n",
"### Google Colab ์ค์ต ํ๊ฒฝ (์ฅ๋น์ ๋นยท์๋ฆฌ๋ถ์ / Level 3)\n",
"\n",
"[](https://colab.research.google.com/github/edunlab/milicontent2/blob/main/uploads/MNT06/MNT06_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",
"์๋ฆฌ๋ถ์์ ํ๊ธฐ์จ์ด ๋์ผ๋ฉด ์ ๋น๋น์ฉ๊ณผ ์ฌ๊ณ ๋ถ๋ด์ด ์ปค์ง๊ณ , ์กฐ๋ฌ์ด ๋ฆ์ด์ง๋ ๋์ ์ฅ๋น๋ ๊ฐ๋ํ์ง ๋ชปํ๋ค. ํ๊ธฐ๊ฐ ์ฆ์ ๋ถํ ๊ฐ์ด๋ฐ ์ผ์ ์์ 3D ํ๋ฆฐํ
์ผ๋ก ๋์ฒด ์ ์ํ ์ ์๋ ๋ถํ์ ๊ฐ๋ ค๋ด๋ฉด ํ๊ธฐ ์์ค๊ณผ ์กฐ๋ฌ ๋๊ธฐ๋ฅผ ํจ๊ป ์ค์ผ ์ ์๋ค. ๋ณธ ํ๋ก์ ํธ๋ ๋ถํ ํ๊ธฐ์จ์ ์ฐ์ ยท์์ธก์ ๋์ฒด ์ ์ ํ๋น์ฑ ํ๋จ๊ณผ ์ฐ๊ฒฐํ๋ ๊ฒ์ ๋ชฉํ๋ก, ๊ต์ก์ฉ ๊ฐ์ ๋ถํ ์๋ฃ(CAD ์์ฝ ์น์, ๊ธ์ยทํด๋ฆฌ๋จธ ์์ฌ ๋ฌผ์ฑ, ์ฌ์ฉ ํ์ค, ํ๊ธฐ์จ, ์กฐ๋ฌ ์์์ผ)๋ฅผ ์ฝ๋๋ก ๋ง๋ค์ด ๋ถ์ํ๋ค. ๋ฐ์ดํฐ ์ ์์ ๋ฉ์ฌยท์ฌ๋ผ์ด์ฑ ์ ์ฒ๋ฆฌ, ํ์ ๊ธฐ๋ฐ Baseline๊ณผ ํ์์ ๋ถ์, ์ธ์ฅ๊ฐ๋ยทํผ๋ก ์๋ช
์์ธก๊ณผ ๊ตฐ ๊ท๊ฒฉ ๋ถํฉ ์ ๋ขฐ๋ ์ ์, ๋์๋ณด๋์ ์ค๋ช
๊ฐ๋ฅ์ฑ, ์ ๋น๊ธฐ์ ๊ด ์น์ธ(HITL) ์๋ฎฌ๋ ์ดํฐ, 50์ข
์ผ๊ด ํ๊ฐ ์คํธ๋ ์ค ์ํ, ํ๊ดด ์ํ ๋๋น 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. 3D ํ๋ฆฐํ
์ ์ ๋์ ๊ตฐ์ ๋ถํ 3D CAD ๋ชจ๋ธ ๋ฐ ๊ธ์/ํด๋ฆฌ๋จธ ์์ฌ ๋ฌผ์ฑ ๋ฐ์ดํฐ ์ ์\n",
"**์์์๊ฐ**: ์ฝ 1.5์๊ฐ\n",
"\n",
"### ๐ ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ์ํ ์ง์นจ\n",
"#### ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ๋น์์ฑ\n",
"\n",
"3D ํ๋ฆฐํ
๋์ ๊ตฐ์ ๋ถํ 3D CAD ๋ชจ๋ธ ๋ฐ ์์ฌ ๋ฌผ์ฑ ๋ฐ์ดํฐ๋ฅผ ์ ์ํ๋ค. ์ค์ ์ ์ ํ์ฅ์์๋ ๋จ์ ํ์ง์ ๊ทธ์น์ง ์๊ณ ์ ์ํ ์ํ ํ๋จ๊ณผ ์ ๋ขฐ๋ ๋์ ์์ฌ๊ฒฐ์ ์ด ํ์์ ์ด๋ฏ๋ก, **ํ๊ธฐ์จ์ด ๋์ ์๋ฆฌ๋ถ์ ๊ฐ์ด๋ฐ 3D ํ๋ฆฐํ
๋์ฒด ์ ์ ํ๋ณด์ 3D CAD ๋ชจ๋ธ ์์ฝ ์ ๋ณด์ ๊ธ์ยทํด๋ฆฌ๋จธ ์์ฌ ๋ฌผ์ฑ ๋ฐ์ดํฐ๋ฅผ ์ ์ํ๋ค.** ๊ณผ์
์ ์์ํ์ฌ ์คํ์ ๋ฐฉ์งํ๊ณ ์์ ๋์๋ ฅ์ ๊ทน๋ํํด์ผ ํฉ๋๋ค.\n",
"\n",
"์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ณธ ๋ฏธ์
์์๋ **ํ๊ธฐ ๋ถํ ์๋ณ๋ถํฐ CAD ํ๋ณด, ์์ฌ ์ ์ , ์ ์ ๊ฐ๋ฅ์ฑ ํ๋ณ, ์ ๋ขฐ์ฑ ํ๊ฐ, ์ ๋น๊ธฐ์ ๊ด ์น์ธ, ์ฅ์ฐฉ ์ํ, ์ฌํ๊ฒํ ๊น์ง ์
๋ฌด ํ๋ฆ์ ๋จ๊ณ๋ณ๋ก ์ ๋ฆฌํ๊ณ ๋ถํยทCADยท์์ฌยท์ฌ์ฉ ์กฐ๊ฑดยท๊ฒฐ๊ณผ ๋ค์ฏ ๋ฌถ์์ผ๋ก ํผ์ฒ 20๊ฐ ์ด์ ์ ์ํ๊ณ ๊ฒ์ฆํ๋** ์ค์ต์ ์ฒด๊ณ์ ์ผ๋ก ์ํํฉ๋๋ค.\n",
"\n",
"ํ์ต์๋ ์ค์ต๊ฐ์ด๋์ ์ ์๋ ์ด 4๊ฐ ์ธ๋ถ ํ๋(๊ธฐ๋ณธ ๊ธฐ๋ฅ ๊ตฌํ โ ๊ฒฐ๊ณผ ์งํ ๊ฒ์ฆ โ ํ๋ผ๋ฏธํฐ ํ๋ ๋ฐ ๋์ ๊ณผ์ )์ ์์ฐจ์ ์ผ๋ก ์งํํ๋ฉฐ ํต์ฌ ํ์ ๊ธฐ์ค์ ๋ฌ์ฑํฉ๋๋ค.\n",
"\n",
"**์ด๋ฒ ๊ฒฐ๊ณผ๋ฌผ:** `docs/mnt06_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": [
"\"\"\"ํ๊ธฐ์จ์ด ๋์ ์๋ฆฌ๋ถ์์ ์ผ์ 3D ํ๋ฆฐํ
๋์ฒด ์ ์ ํ๋จ์ ํ์ํ 3D CAD ๋ชจ๋ธยท๊ธ์/ํด๋ฆฌ๋จธ ์์ฌ ๋ฌผ์ฑยท์ฌ์ฉ ์กฐ๊ฑด ๋ฐ์ดํฐ์ ์คํค๋ง์ ๋ฐ์ดํฐ ๋์
๋๋ฆฌ(ํผ์ฒ 20๊ฐ ์ด์), ์
๋ฌด ํ๋ก์ธ์ค ๋งต์ ์ ์ํ๊ณ ๊ฐ์ ์ํ์ ๊ฒ์ฆํ๋ค.\"\"\"\n",
"import json\n",
"import re\n",
"import numpy as np\n",
"import pandas as pd\n",
"\n",
"# 1) ์
๋ฌด ํ๋ก์ธ์ค ๋งต: ํ๊ธฐ ๋ถํ ์๋ณ์์ ๋์ฒด ์ ์ ์น์ธยทํจ๊ณผ ํ์ธ๊น์ง\n",
"PROCESS = [(\"ํ๊ธฐ ๋ถํ ์๋ณ\", \"๋ถํ ์ฝ๋, ์ ํ๊ธฐ์๋, ํ๊ธฐ์จ, ์กฐ๋ฌ ์์์ผ\"), (\"CAD ํ๋ณด\", \"CAD ํ์ผ ํ์, ์ผ๊ฐํ ์, ๋จ์, ์น์\"),\n",
" (\"์์ฌ ์ ์ \", \"์์ฌ ์ฝ๋, ๊ณต์ ๊ตฌ๋ถ, ์ธ์ฅ๊ฐ๋ ๊ธฐ์ค๊ฐ, ์ธ์ฆ์ ์ฌ๋ถ\"), (\"์ ์ ๊ฐ๋ฅ์ฑ ํ๋ณ\", \"๋ฒฝ๋๊ป, ๋์ถ ๋น์จ, ์ต๋ ์น์, ๋์ฌ๋ถ\"),\n",
" (\"์ ๋ขฐ์ฑ ํ๊ฐ\", \"์ฌ์ฉ ํ์ค, ์์ ์จ, ํผ๋ก ์๋ช
, ๊ตฐ ๊ท๊ฒฉ ๋ถํฉ ์ ์\"), (\"์ ๋น๊ธฐ์ ๊ด ์น์ธ\", \"์น์ธยท๋ฐ๋ คยท๋ณด๋ฅ, ์ฌ์ \"),\n",
" (\"์ถ๋ ฅยทํ์ฒ๋ฆฌ\", \"์ถ๋ ฅ ์๊ฐ, ์์ฌ๋, ํ์ฒ๋ฆฌ ํญ๋ชฉ\"), (\"์ค ์ฅ์ฐฉยท์ํ\", \"์ฅ์ฐฉ ๋ถ๋, ์ํ ๊ฒฐ๊ณผ, ํ๊ดด์ํ ๊ฐ๋\"), (\"์ฌํ๊ฒํ (AAR)\", \"์์ธก ์ค์ฐจ, ํ๊ธฐ์จ ๋ณํ, ๋ณด์ ์ด๋ ฅ\")]\n",
"\n",
"# 2) ์คํค๋ง(๋ฐ์ดํฐ ๋์
๋๋ฆฌ): ๊ทธ๋ฃน / ํ๋ / ์๋ฃํ / ๊ท์น / ์ค๋ช
\n",
"S = {\n",
" \"part\": {\n",
" \"part_id\": (str, {\"pattern\": r\"^P\\d{3}$\"}, \"๋ถํ ID(๊ฐ์)\"),\n",
" \"item_code\": (str, {\"pattern\": r\"^VX-\\d{4}-\\d{4}$\"}, \"๋ถํ ์๋ณ์ฝ๋(๊ตญ๋ฐฉ ํ์ค ๊ตฐ์ ์ฝ๋ ํ์์ ๋ชจ์ฌํ ๊ฐ์ ์ฝ๋)\"),\n",
" \"fsc_group\": (str, {\"pattern\": r\"^\\d{4}$\"}, \"๋ณด๊ธ ๋ถ๋ฅ๊ตฐ(๊ฐ์ 4์๋ฆฌ)\"),\n",
" \"category\": (str, {\"enum\": [\"bracket\", \"housing\", \"clip\", \"gear\", \"duct\", \"handle\"]}, \"๋ถํ๊ตฐ\"),\n",
" \"crit\": (int, {\"enum\": [1, 2, 3]}, \"์์ ์ค์๋(1 ์ผ๋ฐ, 2 ์ค์, 3 ํต์ฌ)\"),\n",
" \"scrap_rate\": (float, {\"min\": 0, \"max\": 1}, \"์ ํ๊ธฐ์จ(ํ๊ธฐ์๋/์์์๋) - ์์ธก๋์ 1\"),\n",
" \"lead_days\": (float, {\"min\": 0, \"max\": 400}, \"๊ธฐ์กด ์กฐ๋ฌ ์์์ผ\"),\n",
" },\n",
" \"cad\": {\n",
" \"cad_format\": (str, {\"enum\": [\"stl\", \"step\", \"obj\"]}, \"CAD ํ์ผ ํ์\"),\n",
" \"unit\": (str, {\"enum\": [\"mm\"]}, \"์น์ ๋จ์(mm๋ก ํต์ผ)\"),\n",
" \"volume_cm3\": (float, {\"min\": 0.5, \"max\": 1000}, \"๋ถํผ\"),\n",
" \"max_dim_mm\": (float, {\"min\": 5, \"max\": 400}, \"์ต๋ ์น์\"),\n",
" \"min_wall_mm\": (float, {\"min\": 0.2, \"max\": 20}, \"์ต์ ๋ฒฝ๋๊ป\"),\n",
" \"overhang\": (float, {\"min\": 0, \"max\": 1}, \"45๋ ์ด๊ณผ ๋์ถ๋ฉด ๋น์จ(์ง์ง๋ ํ์ ์ ๋)\"),\n",
" \"n_triangles\": (int, {\"min\": 100, \"max\": 5000000}, \"๋ฉ์ฌ ์ผ๊ฐํ ์\"),\n",
" \"thread\": (bool, {}, \"๋์ฌ๋ถ ํฌํจ ์ฌ๋ถ\"),\n",
" },\n",
" \"material\": {\n",
" \"material\": (str, {\"enum\": [\"PA12\", \"PETG\", \"CF-PA\", \"AlSi10Mg\", \"SS316L\", \"Ti64\"]}, \"์์ฌ ์ฝ๋\"),\n",
" \"process\": (str, {\"enum\": [\"polymer\", \"metal\"]}, \"๊ณต์ ๊ตฌ๋ถ(ํด๋ฆฌ๋จธ ์์ถยท๋ถ๋ง / ๊ธ์ ๋ถ๋ง ์ฉ์ต)\"),\n",
" \"base_strength\": (float, {\"min\": 10, \"max\": 1500}, \"์์ฌ ๊ธฐ์ค ์ธ์ฅ๊ฐ๋(MPa, ๊ฐ์)\"),\n",
" \"cert\": (bool, {}, \"์์ฌ ์ธ์ฆ์ ๋ณด์ ์ฌ๋ถ\"),\n",
" },\n",
" \"use\": {\n",
" \"load_N\": (float, {\"min\": 1, \"max\": 30000}, \"์ฌ์ฉ ํ์ค(N)\"),\n",
" \"temp_C\": (float, {\"min\": -40, \"max\": 150}, \"์ฌ์ฉ ์จ๋\"),\n",
" \"orient_deg\": (float, {\"min\": 0, \"max\": 90}, \"์ถ๋ ฅ ๋ฐฉํฅ ๊ฐ๋(์ ์ธต ๋ฐฉํฅ ๋๋น)\"),\n",
" },\n",
" \"result\": {\n",
" \"printable\": (bool, {}, \"์ํ ์ถ๋ ฅ ์ฑ๊ณต ์ฌ๋ถ - ์์ธก๋์ 2\"),\n",
" \"strength_mpa\": (float, {\"min\": 0, \"max\": 1500}, \"์ค์ธก ์ธ์ฅ๊ฐ๋ - ์์ธก๋์ 3\"),\n",
" \"log_cycles\": (float, {\"min\": 0, \"max\": 9}, \"ํผ๋ก ์๋ช
log10(ํ)\"),\n",
" \"conform\": (float, {\"min\": 0, \"max\": 100}, \"๊ตฐ ๊ท๊ฒฉ ๋ถํฉ ์ ๋ขฐ๋ ์ ์ - ์์ธก๋์ 4\"),\n",
" },\n",
"}\n",
"\n",
"\n",
"def check(name, v, typ, rule):\n",
" if not isinstance(v, typ) or (typ in (int, float) and isinstance(v, bool)):\n",
" return [f\"{name}: ์๋ฃํ ์ค๋ฅ({type(v).__name__})\"]\n",
" e = []\n",
" if \"enum\" in rule and v not in rule[\"enum\"]:\n",
" e.append(f\"{name}: ํ์ฉ๋์ง ์์ ๊ฐ '{v}'\")\n",
" if \"min\" in rule and v < rule[\"min\"]:\n",
" e.append(f\"{name}: ์ต์๊ฐ {rule['min']} ๋ฏธ๋ง\")\n",
" if \"max\" in rule and v > rule[\"max\"]:\n",
" e.append(f\"{name}: ์ต๋๊ฐ {rule['max']} ์ด๊ณผ\")\n",
" if \"pattern\" in rule and not re.match(rule[\"pattern\"], v):\n",
" e.append(f\"{name}: ํ์ ๋ถ์ผ์น '{v}'\")\n",
" return e\n",
"\n",
"\n",
"def validate(rec):\n",
" errs = []\n",
" for g, fields in S.items():\n",
" for k, (typ, rule, _) in fields.items():\n",
" if k not in rec.get(g, {}):\n",
" errs.append(f\"{g}.{k}: ํ์ ํ๋ ๋๋ฝ\")\n",
" else:\n",
" errs += check(f\"{g}.{k}\", rec[g][k], typ, rule)\n",
" return errs\n",
"\n",
"\n",
"# 3) [๊ต์ก์ฉ๊ฐ์์๋ฃ] ์ํ 20๊ฑด\n",
"rng = np.random.default_rng(1)\n",
"MATS = {\"PA12\": (\"polymer\", 45), \"PETG\": (\"polymer\", 50), \"CF-PA\": (\"polymer\", 70), \"AlSi10Mg\": (\"metal\", 350), \"SS316L\": (\"metal\", 520), \"Ti64\": (\"metal\", 900)}\n",
"recs = []\n",
"for i in range(20):\n",
" m = str(rng.choice(list(MATS)))\n",
" recs.append({\n",
" \"part\": {\"part_id\": f\"P{i:03d}\", \"item_code\": f\"VX-{rng.integers(1000, 9999)}-{rng.integers(1000, 9999)}\", \"fsc_group\": f\"{rng.integers(1000, 9999)}\",\n",
" \"category\": str(rng.choice([\"bracket\", \"housing\", \"clip\", \"gear\", \"duct\", \"handle\"])), \"crit\": int(rng.integers(1, 4)),\n",
" \"scrap_rate\": float(round(rng.uniform(0.02, 0.3), 3)), \"lead_days\": float(round(rng.uniform(10, 120), 0))},\n",
" \"cad\": {\"cad_format\": str(rng.choice([\"stl\", \"step\", \"obj\"])), \"unit\": \"mm\", \"volume_cm3\": float(round(rng.uniform(5, 200), 1)),\n",
" \"max_dim_mm\": float(round(rng.uniform(20, 180), 1)), \"min_wall_mm\": float(round(rng.uniform(0.8, 6), 1)),\n",
" \"overhang\": float(round(rng.uniform(0, 0.5), 2)), \"n_triangles\": int(rng.integers(2000, 400000)), \"thread\": bool(rng.random() < 0.25)},\n",
" \"material\": {\"material\": m, \"process\": MATS[m][0], \"base_strength\": float(MATS[m][1]), \"cert\": bool(rng.random() < 0.5)},\n",
" \"use\": {\"load_N\": float(round(rng.uniform(50, 3000), 0)), \"temp_C\": float(round(rng.uniform(-10, 80), 0)), \"orient_deg\": float(round(rng.uniform(0, 90), 0))},\n",
" \"result\": {\"printable\": bool(rng.random() < 0.8), \"strength_mpa\": float(round(MATS[m][1] * rng.uniform(0.6, 1.0), 1)),\n",
" \"log_cycles\": float(round(rng.uniform(3, 7.5), 2)), \"conform\": float(round(rng.uniform(30, 98), 1))}})\n",
"\n",
"ok = sum(not validate(r) for r in recs)\n",
"print(f\"์ ์ ์ํ {len(recs)}๊ฑด ์ค ๊ฒ์ฆ ํต๊ณผ {ok}๊ฑด, ์ค๋ฅ {len(recs) - ok}๊ฑด\")\n",
"\n",
"# 4) ์ค๋ฅ ์ฃผ์
์ํ\n",
"bad = json.loads(json.dumps(recs[0]))\n",
"bad[\"cad\"][\"min_wall_mm\"] = 0.0 # ํ์ฉ ๋ฒ์ ๋ฏธ๋ง(0 ์ดํ ๋ฒฝ๋๊ป)\n",
"bad[\"cad\"][\"unit\"] = \"inch\" # ๋จ์ ํผ์ฌ\n",
"bad[\"part\"][\"item_code\"] = \"1234567890123\" # ์ฝ๋ ํ์ ์ค๋ฅ\n",
"del bad[\"use\"][\"load_N\"] # ์ฌ์ฉ ํ์ค ๋๋ฝ\n",
"bad[\"result\"][\"conform\"] = \"๋์\" # ์๋ฃํ ์ค๋ฅ(๋ฌธ์์ด)\n",
"print(\"\\n[์ค๋ฅ ์ฃผ์
๊ฒฐ๊ณผ]\")\n",
"for e in validate(bad):\n",
" print(\" -\", e)\n",
"\n",
"# 5) ํ๋ก์ธ์ค ๋งต๊ณผ ๋ฐ์ดํฐ ๋์
๋๋ฆฌ\n",
"print(\"\\n[์
๋ฌด ํ๋ก์ธ์ค ๋งต]\")\n",
"print(\" -> \".join(f\"{i}.{p}\" for i, (p, _) in enumerate(PROCESS, 1)))\n",
"for p, d in PROCESS:\n",
" print(f\" {p:10s}: {d}\")\n",
"rows = [(g, k, t.__name__, str(r) if r else \"-\", d) for g, f in S.items() for k, (t, r, d) in f.items()]\n",
"dd = pd.DataFrame(rows, columns=[\"๊ทธ๋ฃน\", \"ํ๋\", \"์๋ฃํ\", \"๊ท์น\", \"์ค๋ช
\"])\n",
"print(f\"\\n[๋ฐ์ดํฐ ๋์
๋๋ฆฌ] ํผ์ฒ {len(dd)}๊ฐ (๋ชฉํ 20๊ฐ ์ด์: {'๋ฌ์ฑ' if len(dd) >= 20 else '๋ฏธ๋ฌ'})\")\n",
"print(dd[[\"๊ทธ๋ฃน\", \"ํ๋\", \"์๋ฃํ\", \"์ค๋ช
\"]].to_string(index=False))\n",
"print(\"\\n์์ธก ๋์: ์ ํ๊ธฐ์จ, ์ํ ์ถ๋ ฅ ์ฑ๊ณต ์ฌ๋ถ, ์ธ์ฅ๊ฐ๋, ๊ตฐ ๊ท๊ฒฉ ๋ถํฉ ์ ์. ์ต์ข
ํ์ฉ: ํ๊ธฐ์จ์ด ๋๊ณ ๋์ฒด ์ ์์ด ๊ฐ๋ฅํ ๋ถํ์ ์ฐ์ ์ ์ \")\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"๐ก [์ฐธ๊ณ ] ๋ฏธ์
01 ์์ ์คํ ๊ฒฐ๊ณผ (Output)
\n",
"\n",
"```text\n",
"์ ์ ์ํ 20๊ฑด ์ค ๊ฒ์ฆ ํต๊ณผ 20๊ฑด, ์ค๋ฅ 0๊ฑด\n",
"\n",
"[์ค๋ฅ ์ฃผ์
๊ฒฐ๊ณผ]\n",
" - part.item_code: ํ์ ๋ถ์ผ์น '1234567890123'\n",
" - cad.unit: ํ์ฉ๋์ง ์์ ๊ฐ 'inch'\n",
" - cad.min_wall_mm: ์ต์๊ฐ 0.2 ๋ฏธ๋ง\n",
" - use.load_N: ํ์ ํ๋ ๋๋ฝ\n",
" - result.conform: ์๋ฃํ ์ค๋ฅ(str)\n",
"\n",
"[์
๋ฌด ํ๋ก์ธ์ค ๋งต]\n",
"1.ํ๊ธฐ ๋ถํ ์๋ณ -> 2.CAD ํ๋ณด -> 3.์์ฌ ์ ์ -> 4.์ ์ ๊ฐ๋ฅ์ฑ ํ๋ณ -> 5.์ ๋ขฐ์ฑ ํ๊ฐ -> 6.์ ๋น๊ธฐ์ ๊ด ์น์ธ -> 7.์ถ๋ ฅยทํ์ฒ๋ฆฌ -> 8.์ค ์ฅ์ฐฉยท์ํ -> 9.์ฌํ๊ฒํ (AAR)\n",
" ํ๊ธฐ ๋ถํ ์๋ณ : ๋ถํ ์ฝ๋, ์ ํ๊ธฐ์๋, ํ๊ธฐ์จ, ์กฐ๋ฌ ์์์ผ\n",
" CAD ํ๋ณด : CAD ํ์ผ ํ์, ์ผ๊ฐํ ์, ๋จ์, ์น์\n",
" ์์ฌ ์ ์ : ์์ฌ ์ฝ๋, ๊ณต์ ๊ตฌ๋ถ, ์ธ์ฅ๊ฐ๋ ๊ธฐ์ค๊ฐ, ์ธ์ฆ์ ์ฌ๋ถ\n",
" ์ ์ ๊ฐ๋ฅ์ฑ ํ๋ณ : ๋ฒฝ๋๊ป, ๋์ถ ๋น์จ, ์ต๋ ์น์, ๋์ฌ๋ถ\n",
" ์ ๋ขฐ์ฑ ํ๊ฐ : ์ฌ์ฉ ํ์ค, ์์ ์จ, ํผ๋ก ์๋ช
, ๊ตฐ ๊ท๊ฒฉ ๋ถํฉ ์ ์\n",
" ์ ๋น๊ธฐ์ ๊ด ์น์ธ : ์น์ธยท๋ฐ๋ คยท๋ณด๋ฅ, ์ฌ์ \n",
" ์ถ๋ ฅยทํ์ฒ๋ฆฌ : ์ถ๋ ฅ ์๊ฐ, ์์ฌ๋, ํ์ฒ๋ฆฌ ํญ๋ชฉ\n",
" ์ค ์ฅ์ฐฉยท์ํ : ์ฅ์ฐฉ ๋ถ๋, ์ํ ๊ฒฐ๊ณผ, ํ๊ดด์ํ ๊ฐ๋\n",
" ์ฌํ๊ฒํ (AAR) : ์์ธก ์ค์ฐจ, ํ๊ธฐ์จ ๋ณํ, ๋ณด์ ์ด๋ ฅ\n",
"\n",
"[๋ฐ์ดํฐ ๋์
๋๋ฆฌ] ํผ์ฒ 26๊ฐ (๋ชฉํ 20๊ฐ ์ด์: ๋ฌ์ฑ)\n",
" ๊ทธ๋ฃน ํ๋ ์๋ฃํ ์ค๋ช
\n",
" part part_id str ๋ถํ ID(๊ฐ์)\n",
" part item_code str ๋ถํ ์๋ณ์ฝ๋(๊ตญ๋ฐฉ ํ์ค ๊ตฐ์ ์ฝ๋ ํ์์ ๋ชจ์ฌํ ๊ฐ์ ์ฝ๋)\n",
" part fsc_group str ๋ณด๊ธ ๋ถ๋ฅ๊ตฐ(๊ฐ์ 4์๋ฆฌ)\n",
" part category str ๋ถํ๊ตฐ\n",
" part crit int ์์ ์ค์๋(1 ์ผ๋ฐ, 2 ์ค์, 3 ํต์ฌ)\n",
" part scrap_rate float ์ ํ๊ธฐ์จ(ํ๊ธฐ์๋/์์์๋) - ์์ธก๋์ 1\n",
" part lead_days float ๊ธฐ์กด ์กฐ๋ฌ ์์์ผ\n",
" cad cad_format str CAD ํ์ผ ํ์\n",
" cad unit str ์น์ ๋จ์(mm๋ก ํต์ผ)\n",
" cad volume_cm3 float ๋ถํผ\n",
" cad max_dim_mm float ์ต๋ ์น์\n",
" cad min_wall_mm float ์ต์ ๋ฒฝ๋๊ป\n",
" cad overhang float 45๋ ์ด๊ณผ ๋์ถ๋ฉด ๋น์จ(์ง์ง๋ ํ์ ์ ๋)\n",
" cad n_triangles int ๋ฉ์ฌ ์ผ๊ฐํ ์\n",
" cad thread bool ๋์ฌ๋ถ ํฌํจ ์ฌ๋ถ\n",
"material material str ์์ฌ ์ฝ๋\n",
"material process str ๊ณต์ ๊ตฌ๋ถ(ํด๋ฆฌ๋จธ ์์ถยท๋ถ๋ง / ๊ธ์ ๋ถ๋ง ์ฉ์ต)\n",
"... (์ดํ ์๋ต)\n",
"```\n",
" \n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## ๐ฏ ๋ฏธ์
02. CAD ํ์ผ ๋ฉ์ฌ ์ ์ , ์ฌ๋ผ์ด์ฑ ๋ฐ์ดํฐ ํ์ฑ ๋ฐ ์ ์ ๊ท๊ฒฉ ์ ์ฒ๋ฆฌ ๋ชจ๋ ๊ตฌํ\n",
"**์์์๊ฐ**: ์ฝ 2.0์๊ฐ\n",
"\n",
"### ๐ ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ์ํ ์ง์นจ\n",
"#### ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ๋น์์ฑ\n",
"\n",
"CAD ๋ฉ์ฌ ์ ์ ๋ฐ ์ฌ๋ผ์ด์ฑ ๋ฐ์ดํฐ ํ์ฑ ์ ์ฒ๋ฆฌ ๋ชจ๋์ ๊ตฌํํ๋ค. ์ค์ ์ ์ ํ์ฅ์์๋ ๋จ์ ํ์ง์ ๊ทธ์น์ง ์๊ณ ์ ์ํ ์ํ ํ๋จ๊ณผ ์ ๋ขฐ๋ ๋์ ์์ฌ๊ฒฐ์ ์ด ํ์์ ์ด๋ฏ๋ก, **CAD ๋ฉ์ฌ ์ ์ ์ ์ฌ๋ผ์ด์ฑ ๋ฐ์ดํฐ ํ์ฑ, ๋ถํ ์ด๋ ฅ ๋ ์ฝ๋ ์ ์ ๋ฅผ ์ํํ๋ ์ ์ ๊ท๊ฒฉ ์ ์ฒ๋ฆฌ ๋ชจ๋์ ๊ตฌํํ๋ค.** ๊ณผ์
์ ์์ํ์ฌ ์คํ์ ๋ฐฉ์งํ๊ณ ์์ ๋์๋ ฅ์ ๊ทน๋ํํด์ผ ํฉ๋๋ค.\n",
"\n",
"์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ณธ ๋ฏธ์
์์๋ **์ผ๊ฐํ ๋ฉ์ฌ์ NaN ์ ์ ยทํดํ ์ผ๊ฐํยท์ค๋ณต ์ผ๊ฐํ์ ์ ๊ฑฐํ๊ณ ๋ค์งํ ๋ฒ์ ์ ๋ฐ๋ก์ก์ ๋ถํผ๊ฐ ๊ธฐ์ค๊ณผ ์ผ์นํ๋์ง ํ์ธํ๊ณ ์ ํจ์ฑ์ ์ ๊ฒํ๋** ์ค์ต์ ์ฒด๊ณ์ ์ผ๋ก ์ํํฉ๋๋ค.\n",
"\n",
"ํ์ต์๋ ์ค์ต๊ฐ์ด๋์ ์ ์๋ ์ด 4๊ฐ ์ธ๋ถ ํ๋(๊ธฐ๋ณธ ๊ธฐ๋ฅ ๊ตฌํ โ ๊ฒฐ๊ณผ ์งํ ๊ฒ์ฆ โ ํ๋ผ๋ฏธํฐ ํ๋ ๋ฐ ๋์ ๊ณผ์ )์ ์์ฐจ์ ์ผ๋ก ์งํํ๋ฉฐ ํต์ฌ ํ์ ๊ธฐ์ค์ ๋ฌ์ฑํฉ๋๋ค.\n",
"\n",
"**์ด๋ฒ ๊ฒฐ๊ณผ๋ฌผ:** `src/etl/mnt06_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": [
"\"\"\"๋ถํ ์ด๋ ฅ ๋ ์ฝ๋(ํ๊ธฐยท๋จ์ยท์ค๋ณตยท๊ฒฐ์ธก ์ค๋ฅ), CAD ์ผ๊ฐํ ๋ฉ์ฌ(NaN ์ ์ ยทํดํยท์ค๋ณตยท๋ค์งํ ๋ฒ์ ), ์ฌ๋ผ์ด์ฑ(G-code๋ฅ) ์ถ๋ ฅ ํ์ผ์ ๋ถ๋ ๋ผ์ธ์ด ์์ธ ๊ฐ์ ์์ฒ ์๋ฃ์ ์ ์ ๊ท์น 12์ข
์ ์ ์ฉํ๊ณ , ๊ธด๊ธ ์ ์ ์์ธ ๋ ์ฝ๋๋ ๋ณด์กดํ๋ฉด์ ์ ํฉ์ฑ ๊ฒ์ฆ ํต๊ณผ์จ๊ณผ ์ ์ ์ ํ ํต๊ณ๋ฅผ ํ์ธํ๋ค.\"\"\"\n",
"import re\n",
"import numpy as np\n",
"import pandas as pd\n",
"import matplotlib.pyplot as plt\n",
"from sklearn.impute import KNNImputer\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# [๊ต์ก์ฉ๊ฐ์์๋ฃ] ์ผ์ 3D ํ๋ฆฐํ
๋์ฒด ์ ์ ํ๋ณด ๋ถํ ์์ฑ๊ธฐ (์ค์ ๋ถํยท์ฌ๊ณ ยท๊ท๊ฒฉ ๊ฐ ์๋)\n",
"# ํ๊ธฐ์จ์ด ๋์ ์๋ฆฌ๋ถ์ ์ค 3D ํ๋ฆฐํ
์ผ๋ก ๋์ฒด ์ ์ํ ์ ์๋์ง ํ๋จํ๊ธฐ ์ํ ๊ฐ์ ๋ถํ ์๋ฃ๋ฅผ ๋ง๋ ๋ค.\n",
"# ์์ฌ ๋ฌผ์ฑยท์น์ยทํ์ค์ ๊ต์ก์ฉ์ผ๋ก ๋จ์ํํ ๊ฐ์ด๋ฉฐ ์ค์ ์ํ ์ฑ์ ์๋ ๊ตฐ ๊ท๊ฒฉ ๊ฐ๊ณผ ๋ฌด๊ดํ๋ค.\n",
"# ---------------------------------------------------------------------------\n",
"MAT = { # ์์ฌ: (๊ณต์ , ๊ธฐ์ค ์ธ์ฅ๊ฐ๋ MPa, ๋ฐ๋ g/cm3, ์์ฌ ๋จ๊ฐ ๋ง์/kg, ์ถ๋ ฅ๋น ๋ง์/h, ์ต์ ๋ฒฝ๋๊ป mm, ์ถ๋ ฅ ์๋ cm3/h)\n",
" \"PA12\": (\"polymer\", 45, 1.0, 8, 3, 0.8, 18), \"PETG\": (\"polymer\", 50, 1.27, 5, 2, 0.8, 15),\n",
" \"CF-PA\": (\"polymer\", 70, 1.2, 14, 3, 1.0, 12), \"AlSi10Mg\": (\"metal\", 350, 2.67, 30, 25, 0.5, 4),\n",
" \"SS316L\": (\"metal\", 520, 7.9, 28, 25, 0.6, 3.5), \"Ti64\": (\"metal\", 900, 4.4, 90, 30, 0.6, 3)}\n",
"CATS = [\"bracket\", \"housing\", \"clip\", \"gear\", \"duct\", \"handle\"]\n",
"CAT_SCRAP = {\"bracket\": 0.04, \"housing\": 0.05, \"clip\": 0.12, \"gear\": 0.09, \"duct\": 0.08, \"handle\": 0.10} # ๋ถํ๊ตฐ๋ณ ๊ธฐ๋ณธ ์ ํ๊ธฐ์จ\n",
"BUILD_MM = {\"polymer\": 300, \"metal\": 200} # ๊ฐ์ ์ถ๋ ฅ ๊ฐ๋ฅ ์ต๋ ์น์\n",
"\n",
"\n",
"def make_parts(n=300, seed=0):\n",
" \"\"\"๊ฐ์ ๋ถํ n์ข
: ํ์(๋ถํผยท๋ฒฝ๋๊ปยท๋์ถ ๋น์จ), ์์ฌ, ์ฌ์ฉ ํ์ค, ํ๊ธฐ์จ๊ณผ ์กฐ๋ฌ ์ ๋ณด๋ฅผ ๋ง๋ ๋ค.\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" mat = rng.choice(list(MAT), n, p=[0.2, 0.15, 0.1, 0.2, 0.2, 0.15])\n",
" proc = np.array([MAT[m][0] for m in mat])\n",
" vol = np.clip(rng.lognormal(np.log(30), 0.9, n), 2, 800)\n",
" aspect = rng.lognormal(0.3, 0.4, n)\n",
" max_dim = 10 * vol ** (1 / 3) * aspect ** (1 / 3) * rng.uniform(1.0, 1.6, n)\n",
" wall = np.clip(rng.lognormal(np.log(2.0), 0.6, n), 0.3, 8)\n",
" ovh = rng.beta(2, 5, n)\n",
" is_poly = proc == \"polymer\"\n",
" df = pd.DataFrame({\n",
" \"part_id\": [f\"P{i:03d}\" for i in range(n)], \"cat\": rng.choice(CATS, n, p=[0.2, 0.15, 0.2, 0.1, 0.15, 0.2]), \"material\": mat, \"process\": proc,\n",
" \"volume_cm3\": vol, \"max_dim_mm\": max_dim, \"area_cm2\": 6 * vol ** (2 / 3) * rng.uniform(1.0, 2.2, n), \"min_wall_mm\": wall, \"overhang\": ovh,\n",
" \"n_holes\": rng.poisson(2.0, n), \"thread\": rng.random(n) < 0.25, \"orient_deg\": rng.uniform(0, 90, n),\n",
" \"layer_mm\": np.where(is_poly, rng.choice([0.1, 0.2, 0.3], n), rng.choice([0.03, 0.05], n)),\n",
" \"infill\": np.where(is_poly, rng.choice([40, 60, 80, 100], n, p=[0.15, 0.3, 0.3, 0.25]), 100),\n",
" \"load_N\": np.clip(rng.lognormal(np.where(is_poly, np.log(230), np.log(2300)), 0.7, n), 10, 20000), \"temp_C\": rng.uniform(-20, 90, n),\n",
" \"crit\": rng.choice([1, 2, 3], n, p=[0.35, 0.4, 0.25]), \"cert\": rng.choice([0, 1], n, p=[0.45, 0.55]), # ์์ฌ ์ธ์ฆ์ ๋ณด์ \n",
" \"import_part\": rng.random(n) < 0.4})\n",
" df[\"scrap_rate\"] = np.clip(df[\"cat\"].map(CAT_SCRAP) * rng.lognormal(0, 0.35, n) * (1 + 0.5 * (df[\"load_N\"] > 800)) * (1 + 0.3 * (df[\"temp_C\"] > 60)), 0.005, 0.5)\n",
" df[\"lead_days\"] = np.clip(rng.lognormal(np.log(25), 0.5, n) * np.where(df[\"import_part\"], 2.2, 1.0), 5, 300)\n",
" # ---- ์จ์ ์ง์ค๊ฐ(์ค์ ์ํ ๊ฒฐ๊ณผ์ ํด๋น): ๋ชจ๋ธ์ ์ ์
๋ ฅ์ผ๋ก ์ด๊ฒ์ ์์ธกํ๋ค ----\n",
" eff = np.where(is_poly, 0.55 + 0.45 * df[\"infill\"] / 100, 1 - 0.3 * ovh * 0.2)\n",
" aniso = np.where(is_poly, 1 - 0.3 * np.sin(np.radians(df[\"orient_deg\"])) ** 2, 1 - 0.08 * np.sin(np.radians(df[\"orient_deg\"])))\n",
" heat = np.where(is_poly, 1 - 0.35 * np.clip((df[\"temp_C\"] - 40) / 80, 0, 1), 0.98)\n",
" base = np.array([MAT[m][1] for m in mat])\n",
" df[\"strength_mpa\"] = base * eff * aniso * heat * rng.lognormal(0, 0.06, n)\n",
" cross = df[\"min_wall_mm\"] * np.sqrt(df[\"max_dim_mm\"]) * 1.0 # ํ์ค ์ง์ง ๋จ๋ฉด์ mm2(๋จ์ํ)\n",
" df[\"stress_mpa\"] = df[\"load_N\"] / cross\n",
" df[\"safety\"] = df[\"strength_mpa\"] / df[\"stress_mpa\"]\n",
" ratio = np.clip(df[\"stress_mpa\"] / df[\"strength_mpa\"], 0, 1.5)\n",
" df[\"log_cycles\"] = np.clip(7.5 - 6.0 * ratio + rng.normal(0, 0.25, n), 2, 7.5) # ํผ๋ก ์๋ช
log10(ํ)\n",
" support = 1 + 0.5 * ovh\n",
" df[\"build_h\"] = vol * support / np.array([MAT[m][6] for m in mat]) + 0.5\n",
" mass_kg = vol * np.array([MAT[m][2] for m in mat]) * support / 1000\n",
" df[\"print_cost\"] = mass_kg * np.array([MAT[m][3] for m in mat]) + df[\"build_h\"] * np.array([MAT[m][4] for m in mat]) + 3\n",
" df[\"buy_cost\"] = df[\"print_cost\"] * rng.lognormal(0.25, 0.45, n) + 5 # ๊ตฌ๋งค ๋จ๊ฐ(๋ง์, ๊ฐ์)\n",
" wall_ok = df[\"min_wall_mm\"] >= np.array([MAT[m][5] for m in mat]) * rng.uniform(0.95, 1.25, n)\n",
" dim_ok = df[\"max_dim_mm\"] <= np.where(is_poly, BUILD_MM[\"polymer\"], BUILD_MM[\"metal\"])\n",
" warp = (~is_poly) & (df[\"max_dim_mm\"] > 140) & (df[\"overhang\"] > 0.3) # ๊ธ์ ๋ํ ์ค๋ฒํ ๋ณํ\n",
" thr_bad = df[\"thread\"] & (df[\"min_wall_mm\"] < 1.5) & is_poly # ์์ ๋์ฌ๋ถ ํ์\n",
" df[\"printable\"] = wall_ok & dim_ok & (ovh < 0.5) & ~warp & ~thr_bad\n",
" risk = np.clip(1 - df[\"safety\"] / 3, 0, 1)\n",
" df[\"conform\"] = np.clip(100 * (0.45 * np.minimum(df[\"safety\"] / 3, 1) + 0.25 * np.clip((df[\"log_cycles\"] - 3) / 4.5, 0, 1)\n",
" + 0.15 * (1 - np.clip(ovh * 1.2, 0, 1)) + 0.15 * df[\"cert\"]) - 8 * risk * df[\"crit\"] / 3 + rng.normal(0, 2, n), 0, 100)\n",
" df[\"demand_month\"] = rng.poisson(np.where(df[\"cat\"].isin([\"clip\", \"handle\"]), 40, 15)) + 1 # ์ ์์์๋(๊ฐ์)\n",
" df[\"scrap_cost\"] = df[\"scrap_rate\"] * df[\"demand_month\"] * df[\"buy_cost\"] # ์ ํ๊ธฐ ์์ค์ก(๋ง์, ๊ฐ์)\n",
" return df\n",
"\n",
"rng = np.random.default_rng(2)\n",
"applied = []\n",
"\n",
"\n",
"def rule(name, n):\n",
" applied.append((name, int(n)))\n",
"\n",
"\n",
"# ---------------- 1) ๋ถํ ์ด๋ ฅ ๋ ์ฝ๋ ----------------\n",
"P = make_parts(200, seed=5)\n",
"raw = P[[\"part_id\", \"material\", \"max_dim_mm\", \"min_wall_mm\", \"volume_cm3\", \"scrap_rate\", \"load_N\"]].copy()\n",
"raw[\"unit\"] = \"mm\"\n",
"raw[\"test_date\"] = [f\"2026-{rng.integers(1, 10):02d}-{rng.integers(1, 28):02d}\" for _ in range(len(raw))]\n",
"raw[\"emergency\"] = False\n",
"idx = rng.choice(len(raw), 80, replace=False)\n",
"a, b, c, d, e, f, g = np.split(idx[:77], 7)\n",
"raw.loc[a, \"material\"] = rng.choice([\"pa12\", \"PA-12\", \"ss316l\", \"SS 316L\", \"ti64\"], len(a)) # ์์ฌ ํ๊ธฐ ๋ถ์ผ์น\n",
"raw.loc[b, [\"max_dim_mm\", \"min_wall_mm\"]] = raw.loc[b, [\"max_dim_mm\", \"min_wall_mm\"]].values / 25.4 # inch ๊ฐ\n",
"raw.loc[b, \"unit\"] = \"inch\"\n",
"raw.loc[c, \"scrap_rate\"] = (raw.loc[c, \"scrap_rate\"] * 100).round(1) # % ๋จ์ ํผ์ฌ\n",
"raw.loc[d, \"test_date\"] = [x.replace(\"-\", \"/\") for x in raw.loc[d, \"test_date\"]] # ๋ ์ง ํ์ ํผ์ฌ\n",
"raw.loc[e, \"min_wall_mm\"] = rng.choice([0.0, -1.2], len(e)) # ๋ถ๊ฐ๋ฅ ๊ฐ\n",
"raw.loc[f, [\"load_N\", \"volume_cm3\"]] = np.nan # ๊ฒฐ์ธก\n",
"raw.loc[g, \"emergency\"] = True; raw.loc[g, \"load_N\"] = raw.loc[g, \"load_N\"] * 12 # ๊ธด๊ธ ์ ์ ์์ฒญ(์์ธ: ๊ณผ๋ ํ์ค ๊ธฐ๋ก, ๋ณด์กด)\n",
"raw = pd.concat([raw, raw.sample(10, random_state=1)], ignore_index=True) # ์ค๋ณต ๋ ์ฝ๋\n",
"print(f\"[๋ถํ ์ด๋ ฅ] ์์ฒ {len(raw)}๊ฑด (์ค๋ณต 10๊ฑด, ์ค๋ฅ ์ฃผ์
{len(idx[:77])}๊ฑด ํฌํจ)\")\n",
"df = raw.copy()\n",
"rule(\"R1 ์ค๋ณต ๋ ์ฝ๋ ์ ๊ฑฐ\", df.duplicated().sum()); df = df.drop_duplicates().reset_index(drop=True)\n",
"mat = df[\"material\"].str.upper().str.replace(r\"[\\s-]\", \"\", regex=True).map({\"PA12\": \"PA12\", \"PETG\": \"PETG\", \"CFPA\": \"CF-PA\", \"ALSI10MG\": \"AlSi10Mg\", \"SS316L\": \"SS316L\", \"TI64\": \"Ti64\"})\n",
"rule(\"R2 ์์ฌ ์ฝ๋ ํ๊ธฐ ํต์ผ\", (mat != df[\"material\"]).sum()); df[\"material\"] = mat\n",
"inch = df[\"unit\"] == \"inch\"\n",
"df.loc[inch, [\"max_dim_mm\", \"min_wall_mm\"]] *= 25.4; df[\"unit\"] = \"mm\"; rule(\"R3 CAD ์น์ ๋จ์ ํต์ผ(inch -> mm)\", inch.sum())\n",
"pct = df[\"scrap_rate\"] > 1; df.loc[pct, \"scrap_rate\"] /= 100; rule(\"R4 ํ๊ธฐ์จ ๋จ์ ํต์ผ(% -> ๋น์จ)\", pct.sum())\n",
"t1 = pd.to_datetime(df[\"test_date\"], format=\"%Y-%m-%d\", errors=\"coerce\")\n",
"t2 = pd.to_datetime(df[\"test_date\"], format=\"%Y/%m/%d\", errors=\"coerce\"); rule(\"R5 ์ํ์ผ ํ์ ํต์ผ\", t1.isna().sum()); df[\"test_date\"] = t1.fillna(t2)\n",
"bad = df[\"min_wall_mm\"] <= 0; df.loc[bad, \"min_wall_mm\"] = np.nan; rule(\"R6 ๋ถ๊ฐ๋ฅ ๋ฒฝ๋๊ป ๊ฒฐ์ธก ์ฒ๋ฆฌ\", bad.sum())\n",
"miss = df[[\"load_N\", \"volume_cm3\", \"min_wall_mm\"]].isna().sum().sum()\n",
"df[[\"load_N\", \"volume_cm3\", \"min_wall_mm\"]] = KNNImputer(n_neighbors=5).fit_transform(df[[\"load_N\", \"volume_cm3\", \"min_wall_mm\", \"max_dim_mm\"]])[:, :3]\n",
"rule(\"R7 ๊ฒฐ์ธก KNN ๋ณด์ \", miss)\n",
"q1, q3 = df[\"load_N\"].quantile([0.25, 0.75]); out = df[\"load_N\"] > q3 + 3 * (q3 - q1)\n",
"df[\"outlier_flag\"] = out & ~df[\"emergency\"]\n",
"rule(\"R12 ๊ธด๊ธ ์ ์ ์์ธ ๋ณด์กดยท์ผ๋ฐ ์ด์๊ฐ ํ์\", (out & df[\"emergency\"]).sum())\n",
"\n",
"# ---------------- 2) CAD ๋ฉ์ฌ ----------------\n",
"def box_tris(a_, b_, c_, k=4):\n",
" dims, tris = (a_, b_, c_), []\n",
" for ax in range(3):\n",
" u, v = [i for i in range(3) if i != ax]\n",
" for sgn in (0, 1):\n",
" for i in range(k):\n",
" for j in range(k):\n",
" def Pt(ii, jj):\n",
" p = np.zeros(3); p[ax] = dims[ax] * sgn; p[u] = dims[u] * ii / k; p[v] = dims[v] * jj / k; return p\n",
" p00, p10, p11, p01 = Pt(i, j), Pt(i + 1, j), Pt(i + 1, j + 1), Pt(i, j + 1)\n",
" flip = (-1 if ax == 1 else 1) != (1 if sgn else -1)\n",
" for t in ([p00, p10, p11], [p00, p11, p01]):\n",
" tris.append(t[::-1] if flip else t)\n",
" return np.array(tris)\n",
"\n",
"\n",
"def volume(T):\n",
" return float(np.einsum(\"ij,ij->i\", T[:, 0], np.cross(T[:, 1], T[:, 2])).sum() / 6)\n",
"\n",
"\n",
"T0 = box_tris(40.0, 30.0, 20.0) # ์ ์ ๋ฉ์ฌ(๊ฐ์ ๋ธ๋ํท ๋ธ๋ญํฌ, ๋ถํผ 24 cm3)\n",
"nan_extra = T0[rng.choice(len(T0), 3, replace=False)].copy(); nan_extra[:, 0] = np.nan # NaN ์ ์ ์ ๊ฐ์ง ๋ถ๋ ์ผ๊ฐํ\n",
"deg_extra = T0[rng.choice(len(T0), 8, replace=False)].copy(); deg_extra[:, 1] = deg_extra[:, 0] # ํดํ(๋ฉด์ 0) ์ผ๊ฐํ\n",
"dup_extra = T0[rng.choice(len(T0), 6, replace=False)].copy() # ์ค๋ณต ์ผ๊ฐํ\n",
"m = np.concatenate([T0, nan_extra, deg_extra, dup_extra])\n",
"fl = rng.choice(len(T0), 10, replace=False); m[fl] = m[fl][:, ::-1] # ๋ฒ์ ๋ค์งํ(์๋ณธ ์ค 10๊ฐ)\n",
"vol_raw = volume(np.nan_to_num(m))\n",
"n_raw = len(m)\n",
"nan_t = np.isnan(m).any(axis=(1, 2)); rule(\"R8 NaN ์ ์ ์ผ๊ฐํ ์ ๊ฑฐ\", nan_t.sum()); m = m[~nan_t]\n",
"area = 0.5 * np.linalg.norm(np.cross(m[:, 1] - m[:, 0], m[:, 2] - m[:, 0]), axis=1)\n",
"degen = area < 1e-9\n",
"keys = [tuple(sorted(map(tuple, np.round(t, 4)))) for t in m]\n",
"dup = np.array([k in set(keys[:i]) for i, k in enumerate(keys)])\n",
"rule(\"R9 ํดํยท์ค๋ณต ์ผ๊ฐํ ์ ๊ฑฐ\", (degen | dup).sum()); m = m[~(degen | dup)]\n",
"cen = m.mean(axis=(0, 1)); nrm = np.cross(m[:, 1] - m[:, 0], m[:, 2] - m[:, 0]); inward = np.einsum(\"ij,ij->i\", nrm, m.mean(axis=1) - cen) < 0\n",
"m[inward] = m[inward][:, ::-1]; rule(\"R10 ๋ฒ์ ๋ฐฉํฅ ํต์ผ(์์ชฝ ํฅํจ ๋ค์ง๊ธฐ)\", inward.sum())\n",
"vol_clean = volume(m)\n",
"\n",
"# ---------------- 3) ์ฌ๋ผ์ด์ฑ ์ถ๋ ฅ ํ์ฑ ----------------\n",
"lines = [\"; layer_height=0.2\"]\n",
"for L in range(30):\n",
" lines.append(f\";LAYER:{L}\")\n",
" for s in range(12):\n",
" lines.append(f\"G1 X{rng.uniform(0, 40):.2f} Y{rng.uniform(0, 30):.2f} E{rng.uniform(0.01, 0.2):.3f} F{rng.choice([1200, 1800, 2400])}\")\n",
"for i in rng.choice(len(lines), 8, replace=False):\n",
" if lines[i].startswith(\"G1\"):\n",
" lines[i] = lines[i].replace(\" Y\", \" Yx\") if i % 2 else lines[i].replace(\"F\", \"F-\") # ์ขํ ๊นจ์งยท์์ ์ด์ก์๋\n",
"pat = re.compile(r\"^G1 X(-?\\d+\\.\\d+) Y(-?\\d+\\.\\d+) E(\\d+\\.\\d+) F(\\d+)$\")\n",
"good, rej, layer = [], [], -1\n",
"for ln in lines:\n",
" if ln.startswith(\";LAYER:\"):\n",
" layer = int(ln.split(\":\")[1])\n",
" elif ln.startswith(\"G1\"):\n",
" mt = pat.match(ln)\n",
" (good if mt else rej).append((layer, *map(float, mt.groups())) if mt else ln)\n",
"G = pd.DataFrame(good, columns=[\"layer\", \"x\", \"y\", \"e\", \"f\"])\n",
"rule(\"R11 ์ฌ๋ผ์ด์ฑ ๋ถ๋ ๋ผ์ธ ๊ฒฉ๋ฆฌ(์๋ ํ์ธ ๋ชฉ๋ก)\", len(rej))\n",
"\n",
"# ---------------- 4) ์ ํฉ์ฑ ๊ฒ์ฆ ----------------\n",
"chk = {\"์ค๋ณต ๋ ์ฝ๋ 0๊ฑด\": not df.duplicated().any(), \"์์ฌ ์ฝ๋ ํ์ฉ๊ฐ\": df[\"material\"].isin(list(MAT)).all(),\n",
" \"์น์ ๋จ์ mm\": (df[\"unit\"] == \"mm\").all(), \"ํ๊ธฐ์จ 0~1\": df[\"scrap_rate\"].between(0, 1).all(),\n",
" \"์ํ์ผ ๋ณํ\": df[\"test_date\"].notna().all(), \"๋ฒฝ๋๊ป 0 ์ด๊ณผ\": (df[\"min_wall_mm\"] > 0).all(),\n",
" \"๊ฒฐ์ธก 0๊ฑด\": df[[\"load_N\", \"volume_cm3\", \"min_wall_mm\"]].notna().all().all(),\n",
" f\"๊ธด๊ธ ์ ์ {int(df['emergency'].sum())}๊ฑด ๋ณด์กด\": int(df[\"emergency\"].sum()) == len(g),\n",
" \"๋ฉ์ฌ NaN 0๊ฑด\": not np.isnan(m).any(), \"๋ฉ์ฌ ๋ถํผ ์ค์ฐจ 1% ์ด๋ด\": abs(vol_clean - 24000) / 24000 < 0.01,\n",
" \"์ฌ๋ผ์ด์ฑ ์ด์ก์๋ ์์\": bool((G[\"f\"] > 0).all())}\n",
"rate = np.mean(list(chk.values()))\n",
"print(\"\\n[์ ์ฉ ๊ท์น๊ณผ ์ฒ๋ฆฌ ๊ฑด์]\")\n",
"applied.sort(key=lambda r_: int(re.match(r\"R(\\d+)\", r_[0]).group(1)))\n",
"for nme, n in applied:\n",
" print(f\" {nme:42s} {n:4d}๊ฑด\")\n",
"print(f\"\\n์ ํฉ์ฑ ๊ฒ์ฆ {sum(chk.values())}/{len(chk)} ํต๊ณผ, ํต๊ณผ์จ {rate:.0%} (๋ชฉํ 100%: {'๋ฌ์ฑ' if rate == 1 else '๋ฏธ๋ฌ'}), ๊ท์น {len(applied)}์ข
(๋ชฉํ 10์ข
์ด์: {'๋ฌ์ฑ' if len(applied) >= 10 else '๋ฏธ๋ฌ'})\")\n",
"for k, v in chk.items():\n",
" if not v:\n",
" print(\" ์คํจ:\", k)\n",
"print(f\"์ ์ ํ ๋ถํ ๋ ์ฝ๋ {len(df)}๊ฑด, ์ผ๋ฐ ์ด์๊ฐ ํ์ {int(df['outlier_flag'].sum())}๊ฑด, ๊ธด๊ธ ์ ์ ์์ธ {int(df['emergency'].sum())}๊ฑด ์ ์ง\")\n",
"print(f\"๋ฉ์ฌ ์ผ๊ฐํ {n_raw} -> {len(m)}๊ฐ, ๋ถํผ {vol_raw / 1000:.2f} -> {vol_clean / 1000:.2f} cm3(๊ธฐ์ค 24.00)\")\n",
"print(f\"์ฌ๋ผ์ด์ฑ: {int(G['layer'].nunique())}์ธต, ์ ํจ {len(G)}์ค, ๊ฒฉ๋ฆฌ {len(rej)}์ค, ์ด ์์ถ๋ {G['e'].sum():.1f}, ์์ ์ด์ก ์๊ฐ {(np.hypot(G['x'].diff(), G['y'].diff()).fillna(0) / G['f'] * 60).sum():.2f}๋ถ\")\n",
"\n",
"num = lambda x: pd.to_numeric(x, errors=\"coerce\")\n",
"cmp = pd.DataFrame({\"์ ์ ์ ํ๊ท \": [num(raw[c_]).mean() for c_ in [\"max_dim_mm\", \"min_wall_mm\", \"scrap_rate\", \"load_N\"]],\n",
" \"์ ์ ํ ํ๊ท \": [df[c_].mean() for c_ in [\"max_dim_mm\", \"min_wall_mm\", \"scrap_rate\", \"load_N\"]],\n",
" \"์ ์ ์ ์ต์\": [num(raw[c_]).min() for c_ in [\"max_dim_mm\", \"min_wall_mm\", \"scrap_rate\", \"load_N\"]],\n",
" \"์ ์ ํ ์ต์\": [df[c_].min() for c_ in [\"max_dim_mm\", \"min_wall_mm\", \"scrap_rate\", \"load_N\"]]},\n",
" index=[\"์ต๋์น์mm\", \"๋ฒฝ๋๊ปmm\", \"ํ๊ธฐ์จ\", \"ํ์คN\"]).round(3)\n",
"print(\"\\n[์ ์ ์ ํ ๊ธฐ์ ํต๊ณ]\"); print(cmp.to_string())\n",
"\n",
"fig, ax = plt.subplots(1, 2, figsize=(10, 3.5))\n",
"ax[0].hist(num(raw[\"scrap_rate\"]).dropna(), bins=30, alpha=0.6, label=\"raw\"); ax[0].hist(df[\"scrap_rate\"], bins=30, alpha=0.6, label=\"cleaned\")\n",
"ax[0].set_title(\"Scrap rate before/after unit fix\"); ax[0].legend()\n",
"ax[1].bar([n_.split()[0] for n_, _ in applied], [n_ for _, n_ in applied]); ax[1].set_title(\"Records changed by rule\")\n",
"plt.tight_layout(); plt.show()\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"๐ก [์ฐธ๊ณ ] ๋ฏธ์
02 ์์ ์คํ ๊ฒฐ๊ณผ (Output)
\n",
"\n",
"```text\n",
"[๋ถํ ์ด๋ ฅ] ์์ฒ 210๊ฑด (์ค๋ณต 10๊ฑด, ์ค๋ฅ ์ฃผ์
77๊ฑด ํฌํจ)\n",
"\n",
"[์ ์ฉ ๊ท์น๊ณผ ์ฒ๋ฆฌ ๊ฑด์]\n",
" R1 ์ค๋ณต ๋ ์ฝ๋ ์ ๊ฑฐ 10๊ฑด\n",
" R2 ์์ฌ ์ฝ๋ ํ๊ธฐ ํต์ผ 11๊ฑด\n",
" R3 CAD ์น์ ๋จ์ ํต์ผ(inch -> mm) 11๊ฑด\n",
" R4 ํ๊ธฐ์จ ๋จ์ ํต์ผ(% -> ๋น์จ) 11๊ฑด\n",
" R5 ์ํ์ผ ํ์ ํต์ผ 11๊ฑด\n",
" R6 ๋ถ๊ฐ๋ฅ ๋ฒฝ๋๊ป ๊ฒฐ์ธก ์ฒ๋ฆฌ 11๊ฑด\n",
" R7 ๊ฒฐ์ธก KNN ๋ณด์ 33๊ฑด\n",
" R8 NaN ์ ์ ์ผ๊ฐํ ์ ๊ฑฐ 3๊ฑด\n",
" R9 ํดํยท์ค๋ณต ์ผ๊ฐํ ์ ๊ฑฐ 14๊ฑด\n",
" R10 ๋ฒ์ ๋ฐฉํฅ ํต์ผ(์์ชฝ ํฅํจ ๋ค์ง๊ธฐ) 10๊ฑด\n",
" R11 ์ฌ๋ผ์ด์ฑ ๋ถ๋ ๋ผ์ธ ๊ฒฉ๋ฆฌ(์๋ ํ์ธ ๋ชฉ๋ก) 7๊ฑด\n",
" R12 ๊ธด๊ธ ์ ์ ์์ธ ๋ณด์กดยท์ผ๋ฐ ์ด์๊ฐ ํ์ 5๊ฑด\n",
"\n",
"์ ํฉ์ฑ ๊ฒ์ฆ 11/11 ํต๊ณผ, ํต๊ณผ์จ 100% (๋ชฉํ 100%: ๋ฌ์ฑ), ๊ท์น 12์ข
(๋ชฉํ 10์ข
์ด์: ๋ฌ์ฑ)\n",
"์ ์ ํ ๋ถํ ๋ ์ฝ๋ 200๊ฑด, ์ผ๋ฐ ์ด์๊ฐ ํ์ 3๊ฑด, ๊ธด๊ธ ์ ์ ์์ธ 11๊ฑด ์ ์ง\n",
"๋ฉ์ฌ ์ผ๊ฐํ 209 -> 192๊ฐ, ๋ถํผ 21.00 -> 24.00 cm3(๊ธฐ์ค 24.00)\n",
"์ฌ๋ผ์ด์ฑ: 30์ธต, ์ ํจ 353์ค, ๊ฒฉ๋ฆฌ 7์ค, ์ด ์์ถ๋ 39.0, ์์ ์ด์ก ์๊ฐ 238.36๋ถ\n",
"\n",
"[์ ์ ์ ํ ๊ธฐ์ ํต๊ณ]\n",
" ์ ์ ์ ํ๊ท ์ ์ ํ ํ๊ท ์ ์ ์ ์ต์ ์ ์ ํ ์ต์\n",
"์ต๋์น์mm 43.270 46.097 0.817 18.448\n",
"๋ฒฝ๋๊ปmm 2.114 2.407 -1.200 0.405\n",
"ํ๊ธฐ์จ 0.663 0.117 0.029 0.029\n",
"ํ์คN 2785.396 2864.990 50.301 50.301\n",
"```\n",
" \n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## ๐ฏ ๋ฏธ์
03. ๊ธฐํํ์ ํ์ ๊ธฐ๋ฐ 1์ฐจ 3D ํ๋ฆฐํ
์ ์ ๊ฐ๋ฅ ์ฌ๋ถ ํ๋ณ Baseline ์๊ณ ๋ฆฌ์ฆ ๊ตฌํ\n",
"**์์์๊ฐ**: ์ฝ 2.5์๊ฐ\n",
"\n",
"### ๐ ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ์ํ ์ง์นจ\n",
"#### ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ๋น์์ฑ\n",
"\n",
"ํ์ ๊ธฐ๋ฐ 1์ฐจ 3D ํ๋ฆฐํ
์ ์ ๊ฐ๋ฅ ์ฌ๋ถ ํ๋ณ Baseline ์๊ณ ๋ฆฌ์ฆ์ ๊ตฌํํ๋ค. ์ค์ ์ ์ ํ์ฅ์์๋ ๋จ์ ํ์ง์ ๊ทธ์น์ง ์๊ณ ์ ์ํ ์ํ ํ๋จ๊ณผ ์ ๋ขฐ๋ ๋์ ์์ฌ๊ฒฐ์ ์ด ํ์์ ์ด๋ฏ๋ก, **๋ฒฝ๋๊ปยท๋์ถ ๋น์จยท์ต๋ ์น์ ๊ฐ์ ๊ธฐํํ์ ํ์๋ง์ผ๋ก 3D ํ๋ฆฐํ
์ ์ ๊ฐ๋ฅ ์ฌ๋ถ๋ฅผ 1์ฐจ ํ๋ณํ๋ Baseline ์๊ณ ๋ฆฌ์ฆ์ ๊ตฌํํ๊ณ ํ์์ ๋ถ์์ผ๋ก ํ๊ณ์ ํต์ฌ ๊ด๋ฆฌ ๋์์ ์ฐพ๋๋ค.** ๊ณผ์
์ ์์ํ์ฌ ์คํ์ ๋ฐฉ์งํ๊ณ ์์ ๋์๋ ฅ์ ๊ทน๋ํํด์ผ ํฉ๋๋ค.\n",
"\n",
"์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ณธ ๋ฏธ์
์์๋ **๊ฐ์ ๋ถํ 600์ข
์ ๊ท์น ๊ธฐ๋ฐ ํ๋ณ์ ์ ์ฉํด ์ ํ๋ยท์ ๋ฐ๋ยท์ฌํ์จ๊ณผ ํผ๋ํ๋ ฌ์ ๊ตฌํ๊ณ , ๊ฐ๋ฅ์ด๋ผ ํ์ ํ์ผ๋ ์คํจํ ์์ธ(์์ ๋์ฌ๋ถ, ์์ฌ๋ณ ์ต์ ๋ฒฝ๋๊ป)์ ๋ถ๋ฅ ์ํํ๊ณ ๊ฒ์ฆํ๋** ์ค์ต์ ์ฒด๊ณ์ ์ผ๋ก ์ํํฉ๋๋ค.\n",
"\n",
"ํ์ต์๋ ์ค์ต๊ฐ์ด๋์ ์ ์๋ ์ด 4๊ฐ ์ธ๋ถ ํ๋(๊ธฐ๋ณธ ๊ธฐ๋ฅ ๊ตฌํ โ ๊ฒฐ๊ณผ ์งํ ๊ฒ์ฆ โ ํ๋ผ๋ฏธํฐ ํ๋ ๋ฐ ๋์ ๊ณผ์ )์ ์์ฐจ์ ์ผ๋ก ์งํํ๋ฉฐ ํต์ฌ ํ์ ๊ธฐ์ค์ ๋ฌ์ฑํฉ๋๋ค.\n",
"\n",
"**์ด๋ฒ ๊ฒฐ๊ณผ๋ฌผ:** `notebooks/mnt06_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": [
"\"\"\"๋ถํ์ ํ์(์ต์ ๋ฒฝ๋๊ปยท๋์ถ ๋น์จยท์ต๋ ์น์)๋ง์ผ๋ก 3D ํ๋ฆฐํ
์ ์ ๊ฐ๋ฅ ์ฌ๋ถ๋ฅผ 1์ฐจ ํ๋ณํ๋ ๊ท์น ๊ธฐ๋ฐ Baseline ์๊ณ ๋ฆฌ์ฆ์ ๊ตฌํํ๊ณ , ํ์์ ๋ถ์(EDA) ์ฐจํธ 8์ข
ยท์๊ด๋ถ์(p-value)ยทํ๋ ํ (ABC) ๋ถ์์ผ๋ก Baseline์ ํ๊ณ์ ํ๊ธฐ ์์ค์ด ํฐ ํต์ฌ ๊ด๋ฆฌ ๋์์ ์ฐพ๋๋ค.\"\"\"\n",
"import numpy as np\n",
"import pandas as pd\n",
"import matplotlib.pyplot as plt\n",
"from scipy import stats\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# [๊ต์ก์ฉ๊ฐ์์๋ฃ] ์ผ์ 3D ํ๋ฆฐํ
๋์ฒด ์ ์ ํ๋ณด ๋ถํ ์์ฑ๊ธฐ (์ค์ ๋ถํยท์ฌ๊ณ ยท๊ท๊ฒฉ ๊ฐ ์๋)\n",
"# ํ๊ธฐ์จ์ด ๋์ ์๋ฆฌ๋ถ์ ์ค 3D ํ๋ฆฐํ
์ผ๋ก ๋์ฒด ์ ์ํ ์ ์๋์ง ํ๋จํ๊ธฐ ์ํ ๊ฐ์ ๋ถํ ์๋ฃ๋ฅผ ๋ง๋ ๋ค.\n",
"# ์์ฌ ๋ฌผ์ฑยท์น์ยทํ์ค์ ๊ต์ก์ฉ์ผ๋ก ๋จ์ํํ ๊ฐ์ด๋ฉฐ ์ค์ ์ํ ์ฑ์ ์๋ ๊ตฐ ๊ท๊ฒฉ ๊ฐ๊ณผ ๋ฌด๊ดํ๋ค.\n",
"# ---------------------------------------------------------------------------\n",
"MAT = { # ์์ฌ: (๊ณต์ , ๊ธฐ์ค ์ธ์ฅ๊ฐ๋ MPa, ๋ฐ๋ g/cm3, ์์ฌ ๋จ๊ฐ ๋ง์/kg, ์ถ๋ ฅ๋น ๋ง์/h, ์ต์ ๋ฒฝ๋๊ป mm, ์ถ๋ ฅ ์๋ cm3/h)\n",
" \"PA12\": (\"polymer\", 45, 1.0, 8, 3, 0.8, 18), \"PETG\": (\"polymer\", 50, 1.27, 5, 2, 0.8, 15),\n",
" \"CF-PA\": (\"polymer\", 70, 1.2, 14, 3, 1.0, 12), \"AlSi10Mg\": (\"metal\", 350, 2.67, 30, 25, 0.5, 4),\n",
" \"SS316L\": (\"metal\", 520, 7.9, 28, 25, 0.6, 3.5), \"Ti64\": (\"metal\", 900, 4.4, 90, 30, 0.6, 3)}\n",
"CATS = [\"bracket\", \"housing\", \"clip\", \"gear\", \"duct\", \"handle\"]\n",
"CAT_SCRAP = {\"bracket\": 0.04, \"housing\": 0.05, \"clip\": 0.12, \"gear\": 0.09, \"duct\": 0.08, \"handle\": 0.10} # ๋ถํ๊ตฐ๋ณ ๊ธฐ๋ณธ ์ ํ๊ธฐ์จ\n",
"BUILD_MM = {\"polymer\": 300, \"metal\": 200} # ๊ฐ์ ์ถ๋ ฅ ๊ฐ๋ฅ ์ต๋ ์น์\n",
"\n",
"\n",
"def make_parts(n=300, seed=0):\n",
" \"\"\"๊ฐ์ ๋ถํ n์ข
: ํ์(๋ถํผยท๋ฒฝ๋๊ปยท๋์ถ ๋น์จ), ์์ฌ, ์ฌ์ฉ ํ์ค, ํ๊ธฐ์จ๊ณผ ์กฐ๋ฌ ์ ๋ณด๋ฅผ ๋ง๋ ๋ค.\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" mat = rng.choice(list(MAT), n, p=[0.2, 0.15, 0.1, 0.2, 0.2, 0.15])\n",
" proc = np.array([MAT[m][0] for m in mat])\n",
" vol = np.clip(rng.lognormal(np.log(30), 0.9, n), 2, 800)\n",
" aspect = rng.lognormal(0.3, 0.4, n)\n",
" max_dim = 10 * vol ** (1 / 3) * aspect ** (1 / 3) * rng.uniform(1.0, 1.6, n)\n",
" wall = np.clip(rng.lognormal(np.log(2.0), 0.6, n), 0.3, 8)\n",
" ovh = rng.beta(2, 5, n)\n",
" is_poly = proc == \"polymer\"\n",
" df = pd.DataFrame({\n",
" \"part_id\": [f\"P{i:03d}\" for i in range(n)], \"cat\": rng.choice(CATS, n, p=[0.2, 0.15, 0.2, 0.1, 0.15, 0.2]), \"material\": mat, \"process\": proc,\n",
" \"volume_cm3\": vol, \"max_dim_mm\": max_dim, \"area_cm2\": 6 * vol ** (2 / 3) * rng.uniform(1.0, 2.2, n), \"min_wall_mm\": wall, \"overhang\": ovh,\n",
" \"n_holes\": rng.poisson(2.0, n), \"thread\": rng.random(n) < 0.25, \"orient_deg\": rng.uniform(0, 90, n),\n",
" \"layer_mm\": np.where(is_poly, rng.choice([0.1, 0.2, 0.3], n), rng.choice([0.03, 0.05], n)),\n",
" \"infill\": np.where(is_poly, rng.choice([40, 60, 80, 100], n, p=[0.15, 0.3, 0.3, 0.25]), 100),\n",
" \"load_N\": np.clip(rng.lognormal(np.where(is_poly, np.log(230), np.log(2300)), 0.7, n), 10, 20000), \"temp_C\": rng.uniform(-20, 90, n),\n",
" \"crit\": rng.choice([1, 2, 3], n, p=[0.35, 0.4, 0.25]), \"cert\": rng.choice([0, 1], n, p=[0.45, 0.55]), # ์์ฌ ์ธ์ฆ์ ๋ณด์ \n",
" \"import_part\": rng.random(n) < 0.4})\n",
" df[\"scrap_rate\"] = np.clip(df[\"cat\"].map(CAT_SCRAP) * rng.lognormal(0, 0.35, n) * (1 + 0.5 * (df[\"load_N\"] > 800)) * (1 + 0.3 * (df[\"temp_C\"] > 60)), 0.005, 0.5)\n",
" df[\"lead_days\"] = np.clip(rng.lognormal(np.log(25), 0.5, n) * np.where(df[\"import_part\"], 2.2, 1.0), 5, 300)\n",
" # ---- ์จ์ ์ง์ค๊ฐ(์ค์ ์ํ ๊ฒฐ๊ณผ์ ํด๋น): ๋ชจ๋ธ์ ์ ์
๋ ฅ์ผ๋ก ์ด๊ฒ์ ์์ธกํ๋ค ----\n",
" eff = np.where(is_poly, 0.55 + 0.45 * df[\"infill\"] / 100, 1 - 0.3 * ovh * 0.2)\n",
" aniso = np.where(is_poly, 1 - 0.3 * np.sin(np.radians(df[\"orient_deg\"])) ** 2, 1 - 0.08 * np.sin(np.radians(df[\"orient_deg\"])))\n",
" heat = np.where(is_poly, 1 - 0.35 * np.clip((df[\"temp_C\"] - 40) / 80, 0, 1), 0.98)\n",
" base = np.array([MAT[m][1] for m in mat])\n",
" df[\"strength_mpa\"] = base * eff * aniso * heat * rng.lognormal(0, 0.06, n)\n",
" cross = df[\"min_wall_mm\"] * np.sqrt(df[\"max_dim_mm\"]) * 1.0 # ํ์ค ์ง์ง ๋จ๋ฉด์ mm2(๋จ์ํ)\n",
" df[\"stress_mpa\"] = df[\"load_N\"] / cross\n",
" df[\"safety\"] = df[\"strength_mpa\"] / df[\"stress_mpa\"]\n",
" ratio = np.clip(df[\"stress_mpa\"] / df[\"strength_mpa\"], 0, 1.5)\n",
" df[\"log_cycles\"] = np.clip(7.5 - 6.0 * ratio + rng.normal(0, 0.25, n), 2, 7.5) # ํผ๋ก ์๋ช
log10(ํ)\n",
" support = 1 + 0.5 * ovh\n",
" df[\"build_h\"] = vol * support / np.array([MAT[m][6] for m in mat]) + 0.5\n",
" mass_kg = vol * np.array([MAT[m][2] for m in mat]) * support / 1000\n",
" df[\"print_cost\"] = mass_kg * np.array([MAT[m][3] for m in mat]) + df[\"build_h\"] * np.array([MAT[m][4] for m in mat]) + 3\n",
" df[\"buy_cost\"] = df[\"print_cost\"] * rng.lognormal(0.25, 0.45, n) + 5 # ๊ตฌ๋งค ๋จ๊ฐ(๋ง์, ๊ฐ์)\n",
" wall_ok = df[\"min_wall_mm\"] >= np.array([MAT[m][5] for m in mat]) * rng.uniform(0.95, 1.25, n)\n",
" dim_ok = df[\"max_dim_mm\"] <= np.where(is_poly, BUILD_MM[\"polymer\"], BUILD_MM[\"metal\"])\n",
" warp = (~is_poly) & (df[\"max_dim_mm\"] > 140) & (df[\"overhang\"] > 0.3) # ๊ธ์ ๋ํ ์ค๋ฒํ ๋ณํ\n",
" thr_bad = df[\"thread\"] & (df[\"min_wall_mm\"] < 1.5) & is_poly # ์์ ๋์ฌ๋ถ ํ์\n",
" df[\"printable\"] = wall_ok & dim_ok & (ovh < 0.5) & ~warp & ~thr_bad\n",
" risk = np.clip(1 - df[\"safety\"] / 3, 0, 1)\n",
" df[\"conform\"] = np.clip(100 * (0.45 * np.minimum(df[\"safety\"] / 3, 1) + 0.25 * np.clip((df[\"log_cycles\"] - 3) / 4.5, 0, 1)\n",
" + 0.15 * (1 - np.clip(ovh * 1.2, 0, 1)) + 0.15 * df[\"cert\"]) - 8 * risk * df[\"crit\"] / 3 + rng.normal(0, 2, n), 0, 100)\n",
" df[\"demand_month\"] = rng.poisson(np.where(df[\"cat\"].isin([\"clip\", \"handle\"]), 40, 15)) + 1 # ์ ์์์๋(๊ฐ์)\n",
" df[\"scrap_cost\"] = df[\"scrap_rate\"] * df[\"demand_month\"] * df[\"buy_cost\"] # ์ ํ๊ธฐ ์์ค์ก(๋ง์, ๊ฐ์)\n",
" return df\n",
"\n",
"df = make_parts(600, seed=3)\n",
"\n",
"\n",
"def baseline(d):\n",
" \"\"\"ํ์ ๊ธฐ๋ฐ 1์ฐจ ํ๋ณ: ๋ฒฝ๋๊ป >= ๊ณตํต ๊ธฐ์ค 1.0mm, ์ต๋ ์น์ <= ์ถ๋ ฅ ๊ฐ๋ฅ ์น์, ๋์ถ ๋น์จ < 0.5 (์์ฌ๋ณ ์ฐจ์ด๋ ๋ณด์ง ์๋๋ค).\"\"\"\n",
" return (d[\"min_wall_mm\"] >= 1.0) & (d[\"max_dim_mm\"] <= d[\"process\"].map(BUILD_MM)) & (d[\"overhang\"] < 0.5)\n",
"\n",
"\n",
"df[\"pred\"] = baseline(df)\n",
"tp, fp = int((df[\"pred\"] & df[\"printable\"]).sum()), int((df[\"pred\"] & ~df[\"printable\"]).sum())\n",
"fn, tn = int((~df[\"pred\"] & df[\"printable\"]).sum()), int((~df[\"pred\"] & ~df[\"printable\"]).sum())\n",
"acc, prec, rec = (tp + tn) / len(df), tp / (tp + fp), tp / (tp + fn)\n",
"print(f\"[Baseline ๊ฒฐ๊ณผ] ๋ถํ {len(df)}์ข
, ์ํ ์ถ๋ ฅ ์ฑ๊ณต {df['printable'].mean():.1%}\")\n",
"print(f\"์ ํ๋ {acc:.1%}, ์ ๋ฐ๋ {prec:.1%}, ์ฌํ์จ {rec:.1%} (ํผ๋ํ๋ ฌ TP {tp} FP {fp} FN {fn} TN {tn})\")\n",
"miss = df[df[\"pred\"] & ~df[\"printable\"]]\n",
"why = pd.Series(np.select([miss[\"thread\"] & (miss[\"process\"] == \"polymer\") & (miss[\"min_wall_mm\"] < 1.5), (miss[\"process\"] == \"metal\") & (miss[\"max_dim_mm\"] > 140) & (miss[\"overhang\"] > 0.3)],\n",
" [\"์์ ๋์ฌ๋ถ ํ์\", \"๊ธ์ ๋ํ ์ค๋ฒํ ๋ณํ\"], \"์์ฌ๋ณ ์ต์ ๋ฒฝ๋๊ป ๋ฏธ๋ฌ\"))\n",
"print(\"Baseline์ด '๊ฐ๋ฅ'์ด๋ผ ํ์ผ๋ ์คํจํ ์์ธ:\", \", \".join(f\"{k} {v}๊ฑด\" for k, v in why.value_counts().items()))\n",
"print(f\"Baseline์ด '๋ถ๊ฐ'๋ผ ํ์ผ๋ ์ค์ ์ฑ๊ณตํ ๋ถํ {fn}๊ฑด(๊ธ์ ๋ฑ ์์ ๋ฒฝ ์ถ๋ ฅ ๊ฐ๋ฅ ์์ฌ๋ฅผ ๋์นจ)\")\n",
"print(\"-> ํ์ ๊ท์น๋ง์ผ๋ก๋ ๋์ฌ๋ถยท์ด๋ณํยท์์ฌ ํธ์ฐจ๋ฅผ ๋ชป ๋ณธ๋ค. ์ํํ ์คํ(FP)์ ์ ๋น๊ธฐ์ ๊ด ๊ฒํ ์ ๊ฑธ๋ฌ์ผ ํ๋ค(๋ฏธ์
4~5์ ๋๊ธฐ).\")\n",
"\n",
"# 1) ์๊ด๋ถ์: ์ํ ์ถ๋ ฅ ์ฑ๊ณต ์ฌ๋ถ์์ ๊ด๊ณ(์ ์ด์ฐ ์๊ด, ์ ์์์ค 0.05)\n",
"df[\"wall_ratio\"] = df[\"min_wall_mm\"] / df[\"material\"].map(lambda m: MAT[m][5])\n",
"df[\"is_metal\"] = (df[\"process\"] == \"metal\").astype(int)\n",
"feats = [\"wall_ratio\", \"min_wall_mm\", \"overhang\", \"max_dim_mm\", \"volume_cm3\", \"is_metal\", \"n_holes\", \"thread\", \"layer_mm\", \"load_N\", \"crit\"]\n",
"rows = []\n",
"for f in feats:\n",
" r, p = stats.pearsonr(df[f].astype(float), df[\"printable\"].astype(float))\n",
" rows.append({\"ํผ์ฒ\": f, \"์๊ด๊ณ์\": round(r, 3), \"p-value\": f\"{p:.1e}\", \"์ ์\": p < 0.05})\n",
"corr = pd.DataFrame(rows).sort_values(\"์๊ด๊ณ์\", key=abs, ascending=False)\n",
"print(\"\\n[์ํ ์ถ๋ ฅ ์ฑ๊ณต ์ฌ๋ถ์์ ์๊ด๋ถ์]\")\n",
"print(corr.to_string(index=False))\n",
"n_sig = int(corr[\"์ ์\"].sum())\n",
"print(f\"์ ์๋ฏธํ ํผ์ฒ {n_sig}๊ฐ (๋ชฉํ 5๊ฐ ์ด์: {'๋ฌ์ฑ' if n_sig >= 5 else '๋ฏธ๋ฌ'})\")\n",
"t, p = stats.ttest_ind(df.loc[df[\"printable\"], \"scrap_rate\"], df.loc[~df[\"printable\"], \"scrap_rate\"])\n",
"print(f\"ํ๊ธฐ์จ ํ๊ท : ์ ์ ๊ฐ๋ฅ {df.loc[df['printable'], 'scrap_rate'].mean():.3f} vs ๋ถ๊ฐ {df.loc[~df['printable'], 'scrap_rate'].mean():.3f} (t๊ฒ์ p={p:.2f}) -> ํ๊ธฐ์จ๊ณผ ์ ์ ๊ฐ๋ฅ์ฑ์ ๋ณ๊ฐ ์ถ์ด๋ฏ๋ก ๋์ ํจ๊ป ๋ด์ผ ์ฐ์ ์์๊ฐ ์ ํด์ง๋ค\")\n",
"\n",
"# 2) ํ๋ ํ (ABC): ์ ํ๊ธฐ ์์ค์ก์ด ํฐ (๋ถํ๊ตฐ, ์์ฌ) ์กฐํฉ ์ค ์ ์ ๊ฐ๋ฅํ ๊ฒ\n",
"g = df.groupby([\"cat\", \"material\"])[\"scrap_cost\"].sum().sort_values(ascending=False)\n",
"cum = g.cumsum() / g.sum()\n",
"abc = pd.cut(cum, [0, 0.8, 0.95, 1.0], labels=[\"A\", \"B\", \"C\"])\n",
"top = [f\"{c}/{m}\" for (c, m), v in abc.items() if v == \"A\"]\n",
"print(f\"\\n[ํ๋ ํ A๋ฑ๊ธ(๋์ ํ๊ธฐ ์์ค 80%)] {len(top)}๊ฐ ์กฐํฉ: \" + \", \".join(top[:6]) + \" ...\")\n",
"print(f\"A๋ฑ๊ธ์ ์ ์ฒด ์กฐํฉ {len(g)}๊ฐ ์ค {(abc == 'A').mean():.0%}, ์ด ์ค Baseline ์ ์ ๊ฐ๋ฅ ๋ถํ ๋น์จ {df[df.set_index(['cat', 'material']).index.isin(list(abc[abc == 'A'].index))]['pred'].mean():.0%}\")\n",
"\n",
"# 3) EDA ์ฐจํธ 8์ข
\n",
"fig, ax = plt.subplots(2, 4, figsize=(18, 8))\n",
"a = ax.ravel()\n",
"a[0].hist(df[\"scrap_rate\"], bins=30); a[0].set_title(\"1. Scrap rate distribution\")\n",
"df.groupby(\"cat\")[\"scrap_rate\"].mean().plot.bar(ax=a[1]); a[1].set_title(\"2. Mean scrap rate by category\")\n",
"a[2].scatter(df[\"min_wall_mm\"], df[\"max_dim_mm\"], c=df[\"printable\"], cmap=\"coolwarm_r\", s=8); a[2].set_title(\"3. Wall vs size (blue=printable)\")\n",
"df.boxplot(column=\"overhang\", by=\"printable\", ax=a[3]); a[3].set_title(\"4. Overhang by print result\")\n",
"df[\"material\"].value_counts().plot.bar(ax=a[4]); a[4].set_title(\"5. Parts by material\")\n",
"a[5].scatter(df[\"load_N\"], df[\"safety\"], s=6); a[5].set_xscale(\"log\"); a[5].set_yscale(\"log\"); a[5].set_title(\"6. Load vs safety factor\")\n",
"a[6].barh(corr[\"ํผ์ฒ\"], corr[\"์๊ด๊ณ์\"]); a[6].set_title(\"7. Correlation with print success\")\n",
"a[7].plot(range(1, len(cum) + 1), cum.values, \"o-\"); a[7].axhline(0.8, ls=\"--\", c=\"gray\"); a[7].set_title(\"8. Pareto of scrap loss\")\n",
"plt.suptitle(\"MNT06 EDA - baseline printability rule (synthetic data)\"); plt.tight_layout(); plt.show()\n",
"print(\"\\nEDA ์ฐจํธ 8์ข
์์ฑ (๋ชฉํ 8์ข
์ด์: ๋ฌ์ฑ)\")\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"๐ก [์ฐธ๊ณ ] ๋ฏธ์
03 ์์ ์คํ ๊ฒฐ๊ณผ (Output)
\n",
"\n",
"```text\n",
"[Baseline ๊ฒฐ๊ณผ] ๋ถํ 600์ข
, ์ํ ์ถ๋ ฅ ์ฑ๊ณต 80.8%\n",
"์ ํ๋ 91.0%, ์ ๋ฐ๋ 96.5%, ์ฌํ์จ 92.2% (ํผ๋ํ๋ ฌ TP 447 FP 16 FN 38 TN 99)\n",
"Baseline์ด '๊ฐ๋ฅ'์ด๋ผ ํ์ผ๋ ์คํจํ ์์ธ: ์์ ๋์ฌ๋ถ ํ์ 14๊ฑด, ์์ฌ๋ณ ์ต์ ๋ฒฝ๋๊ป ๋ฏธ๋ฌ 2๊ฑด\n",
"Baseline์ด '๋ถ๊ฐ'๋ผ ํ์ผ๋ ์ค์ ์ฑ๊ณตํ ๋ถํ 38๊ฑด(๊ธ์ ๋ฑ ์์ ๋ฒฝ ์ถ๋ ฅ ๊ฐ๋ฅ ์์ฌ๋ฅผ ๋์นจ)\n",
"-> ํ์ ๊ท์น๋ง์ผ๋ก๋ ๋์ฌ๋ถยท์ด๋ณํยท์์ฌ ํธ์ฐจ๋ฅผ ๋ชป ๋ณธ๋ค. ์ํํ ์คํ(FP)์ ์ ๋น๊ธฐ์ ๊ด ๊ฒํ ์ ๊ฑธ๋ฌ์ผ ํ๋ค(๋ฏธ์
4~5์ ๋๊ธฐ).\n",
"\n",
"[์ํ ์ถ๋ ฅ ์ฑ๊ณต ์ฌ๋ถ์์ ์๊ด๋ถ์]\n",
" ํผ์ฒ ์๊ด๊ณ์ p-value ์ ์\n",
" overhang -0.385 1.3e-22 True\n",
" wall_ratio 0.294 2.1e-13 True\n",
"min_wall_mm 0.279 3.5e-12 True\n",
" is_metal 0.188 3.7e-06 True\n",
" layer_mm -0.159 8.9e-05 True\n",
" load_N 0.103 1.1e-02 True\n",
" crit -0.078 5.5e-02 False\n",
" thread -0.069 9.2e-02 False\n",
" max_dim_mm 0.026 5.3e-01 False\n",
" n_holes 0.018 6.6e-01 False\n",
" volume_cm3 -0.005 9.1e-01 False\n",
"์ ์๋ฏธํ ํผ์ฒ 6๊ฐ (๋ชฉํ 5๊ฐ ์ด์: ๋ฌ์ฑ)\n",
"ํ๊ธฐ์จ ํ๊ท : ์ ์ ๊ฐ๋ฅ 0.124 vs ๋ถ๊ฐ 0.105 (t๊ฒ์ p=0.01) -> ํ๊ธฐ์จ๊ณผ ์ ์ ๊ฐ๋ฅ์ฑ์ ๋ณ๊ฐ ์ถ์ด๋ฏ๋ก ๋์ ํจ๊ป ๋ด์ผ ์ฐ์ ์์๊ฐ ์ ํด์ง๋ค\n",
"\n",
"[ํ๋ ํ A๋ฑ๊ธ(๋์ ํ๊ธฐ ์์ค 80%)] 8๊ฐ ์กฐํฉ: handle/SS316L, handle/AlSi10Mg, clip/Ti64, clip/AlSi10Mg, handle/Ti64, clip/SS316L ...\n",
"A๋ฑ๊ธ์ ์ ์ฒด ์กฐํฉ 36๊ฐ ์ค 22%, ์ด ์ค Baseline ์ ์ ๊ฐ๋ฅ ๋ถํ ๋น์จ 82%\n",
"\n",
"EDA ์ฐจํธ 8์ข
์์ฑ (๋ชฉํ 8์ข
์ด์: ๋ฌ์ฑ)\n",
"```\n",
" \n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## ๐ฏ ๋ฏธ์
04. ์ ํ์์ํด์(FEA) ์ฐ๊ณ ์ผ์ ๋ถํ ์ธ์ฅ๊ฐ๋ยทํผ๋ก๋ด๊ตฌ๋ ์ ๋ขฐ์ฑ ํ๊ฐ ๋จธ์ ๋ฌ๋ ๊ณ ๋ํ\n",
"**์์์๊ฐ**: ์ฝ 3.0์๊ฐ\n",
"\n",
"### ๐ ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ์ํ ์ง์นจ\n",
"#### ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ๋น์์ฑ\n",
"\n",
"์ ํ์์ํด์(FEA) ์ฐ๊ณ ๋ถํ ์ธ์ฅ๊ฐ๋ยท๋ด๊ตฌ๋ ์ ๋ขฐ์ฑ ํ๊ฐ ๋ชจ๋ธ์ ๊ณ ๋ํํ๋ค. ์ค์ ์ ์ ํ์ฅ์์๋ ๋จ์ ํ์ง์ ๊ทธ์น์ง ์๊ณ ์ ์ํ ์ํ ํ๋จ๊ณผ ์ ๋ขฐ๋ ๋์ ์์ฌ๊ฒฐ์ ์ด ํ์์ ์ด๋ฏ๋ก, **์ ํ์์ํด์(FEA)์ ๊ฐ์ดํํ ์๋ ฅ ๊ณ์ฐ๊ณผ ์์ฌยท๊ณต์ ์ง์์ ์ฐ๊ณํด ์ผ์ ๋ถํ์ ์ธ์ฅ๊ฐ๋์ ํผ๋ก ์๋ช
์ ์์ธกํ๋ ์ ๋ขฐ์ฑ ํ๊ฐ ๋จธ์ ๋ฌ๋ ๋ชจ๋ธ์ ๊ณ ๋ํํ๋ค.** ๊ณผ์
์ ์์ํ์ฌ ์คํ์ ๋ฐฉ์งํ๊ณ ์์ ๋์๋ ฅ์ ๊ทน๋ํํด์ผ ํฉ๋๋ค.\n",
"\n",
"์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ณธ ๋ฏธ์
์์๋ **๋จ๋ฉด์ , ๊ตฌ๋ฉยท๋์ฌ๋ถ ์๋ ฅ์ง์ค, ์ ์ธต ๋ฐฉํฅ, ์ถฉ์ ์จ, ์จ๋ ์ ํ ๋ฑ์ผ๋ก ํ์ ํผ์ฒ 10๊ฐ ์ด์์ ๋ง๋ค๊ณ ์์ฌ ๊ธฐ์ค๊ฐ๋ ๋๋น ์ ๊ฐ ๊ณ์๋ฅผ ํ์ต ์ํํ๊ณ ๊ฒ์ฆํ๋** ์ค์ต์ ์ฒด๊ณ์ ์ผ๋ก ์ํํฉ๋๋ค.\n",
"\n",
"ํ์ต์๋ ์ค์ต๊ฐ์ด๋์ ์ ์๋ ์ด 4๊ฐ ์ธ๋ถ ํ๋(๊ธฐ๋ณธ ๊ธฐ๋ฅ ๊ตฌํ โ ๊ฒฐ๊ณผ ์งํ ๊ฒ์ฆ โ ํ๋ผ๋ฏธํฐ ํ๋ ๋ฐ ๋์ ๊ณผ์ )์ ์์ฐจ์ ์ผ๋ก ์งํํ๋ฉฐ ํต์ฌ ํ์ ๊ธฐ์ค์ ๋ฌ์ฑํฉ๋๋ค.\n",
"\n",
"**์ด๋ฒ ๊ฒฐ๊ณผ๋ฌผ:** `src/features/mnt06_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": [
"\"\"\"์ ํ์์ํด์(FEA)์ ๊ฐ์ดํํ ์๋ ฅ ๊ณ์ฐ๊ฐ๊ณผ ์์ฌยท๊ณต์ ์ง์์ผ๋ก ํ์ ํผ์ฒ 16๊ฐ๋ฅผ ๋ง๋ค์ด ์ผ์ ๋ถํ์ ์ธ์ฅ๊ฐ๋์ ํผ๋ก ์๋ช
(์ ๋ขฐ์ฑ)์ ์์ธกํ๋ ๋จธ์ ๋ฌ๋ ๊ธฐ์ค ๋ชจ๋ธ์ ์๊ณ์ด ๊ต์ฐจ๊ฒ์ฆ(MAPE)์ผ๋ก ํ๊ฐํ๊ณ , ํผ์ฒ ์ค์๋๋ก ์ฐจ์ ์ถ์ ํ๋ณด๋ฅผ ๊ณ ๋ฅธ๋ค.\"\"\"\n",
"import numpy as np\n",
"import pandas as pd\n",
"import matplotlib.pyplot as plt\n",
"from sklearn.ensemble import GradientBoostingRegressor\n",
"from sklearn.model_selection import TimeSeriesSplit\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# [๊ต์ก์ฉ๊ฐ์์๋ฃ] ์ผ์ 3D ํ๋ฆฐํ
๋์ฒด ์ ์ ํ๋ณด ๋ถํ ์์ฑ๊ธฐ (์ค์ ๋ถํยท์ฌ๊ณ ยท๊ท๊ฒฉ ๊ฐ ์๋)\n",
"# ํ๊ธฐ์จ์ด ๋์ ์๋ฆฌ๋ถ์ ์ค 3D ํ๋ฆฐํ
์ผ๋ก ๋์ฒด ์ ์ํ ์ ์๋์ง ํ๋จํ๊ธฐ ์ํ ๊ฐ์ ๋ถํ ์๋ฃ๋ฅผ ๋ง๋ ๋ค.\n",
"# ์์ฌ ๋ฌผ์ฑยท์น์ยทํ์ค์ ๊ต์ก์ฉ์ผ๋ก ๋จ์ํํ ๊ฐ์ด๋ฉฐ ์ค์ ์ํ ์ฑ์ ์๋ ๊ตฐ ๊ท๊ฒฉ ๊ฐ๊ณผ ๋ฌด๊ดํ๋ค.\n",
"# ---------------------------------------------------------------------------\n",
"MAT = { # ์์ฌ: (๊ณต์ , ๊ธฐ์ค ์ธ์ฅ๊ฐ๋ MPa, ๋ฐ๋ g/cm3, ์์ฌ ๋จ๊ฐ ๋ง์/kg, ์ถ๋ ฅ๋น ๋ง์/h, ์ต์ ๋ฒฝ๋๊ป mm, ์ถ๋ ฅ ์๋ cm3/h)\n",
" \"PA12\": (\"polymer\", 45, 1.0, 8, 3, 0.8, 18), \"PETG\": (\"polymer\", 50, 1.27, 5, 2, 0.8, 15),\n",
" \"CF-PA\": (\"polymer\", 70, 1.2, 14, 3, 1.0, 12), \"AlSi10Mg\": (\"metal\", 350, 2.67, 30, 25, 0.5, 4),\n",
" \"SS316L\": (\"metal\", 520, 7.9, 28, 25, 0.6, 3.5), \"Ti64\": (\"metal\", 900, 4.4, 90, 30, 0.6, 3)}\n",
"CATS = [\"bracket\", \"housing\", \"clip\", \"gear\", \"duct\", \"handle\"]\n",
"CAT_SCRAP = {\"bracket\": 0.04, \"housing\": 0.05, \"clip\": 0.12, \"gear\": 0.09, \"duct\": 0.08, \"handle\": 0.10} # ๋ถํ๊ตฐ๋ณ ๊ธฐ๋ณธ ์ ํ๊ธฐ์จ\n",
"BUILD_MM = {\"polymer\": 300, \"metal\": 200} # ๊ฐ์ ์ถ๋ ฅ ๊ฐ๋ฅ ์ต๋ ์น์\n",
"\n",
"\n",
"def make_parts(n=300, seed=0):\n",
" \"\"\"๊ฐ์ ๋ถํ n์ข
: ํ์(๋ถํผยท๋ฒฝ๋๊ปยท๋์ถ ๋น์จ), ์์ฌ, ์ฌ์ฉ ํ์ค, ํ๊ธฐ์จ๊ณผ ์กฐ๋ฌ ์ ๋ณด๋ฅผ ๋ง๋ ๋ค.\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" mat = rng.choice(list(MAT), n, p=[0.2, 0.15, 0.1, 0.2, 0.2, 0.15])\n",
" proc = np.array([MAT[m][0] for m in mat])\n",
" vol = np.clip(rng.lognormal(np.log(30), 0.9, n), 2, 800)\n",
" aspect = rng.lognormal(0.3, 0.4, n)\n",
" max_dim = 10 * vol ** (1 / 3) * aspect ** (1 / 3) * rng.uniform(1.0, 1.6, n)\n",
" wall = np.clip(rng.lognormal(np.log(2.0), 0.6, n), 0.3, 8)\n",
" ovh = rng.beta(2, 5, n)\n",
" is_poly = proc == \"polymer\"\n",
" df = pd.DataFrame({\n",
" \"part_id\": [f\"P{i:03d}\" for i in range(n)], \"cat\": rng.choice(CATS, n, p=[0.2, 0.15, 0.2, 0.1, 0.15, 0.2]), \"material\": mat, \"process\": proc,\n",
" \"volume_cm3\": vol, \"max_dim_mm\": max_dim, \"area_cm2\": 6 * vol ** (2 / 3) * rng.uniform(1.0, 2.2, n), \"min_wall_mm\": wall, \"overhang\": ovh,\n",
" \"n_holes\": rng.poisson(2.0, n), \"thread\": rng.random(n) < 0.25, \"orient_deg\": rng.uniform(0, 90, n),\n",
" \"layer_mm\": np.where(is_poly, rng.choice([0.1, 0.2, 0.3], n), rng.choice([0.03, 0.05], n)),\n",
" \"infill\": np.where(is_poly, rng.choice([40, 60, 80, 100], n, p=[0.15, 0.3, 0.3, 0.25]), 100),\n",
" \"load_N\": np.clip(rng.lognormal(np.where(is_poly, np.log(230), np.log(2300)), 0.7, n), 10, 20000), \"temp_C\": rng.uniform(-20, 90, n),\n",
" \"crit\": rng.choice([1, 2, 3], n, p=[0.35, 0.4, 0.25]), \"cert\": rng.choice([0, 1], n, p=[0.45, 0.55]), # ์์ฌ ์ธ์ฆ์ ๋ณด์ \n",
" \"import_part\": rng.random(n) < 0.4})\n",
" df[\"scrap_rate\"] = np.clip(df[\"cat\"].map(CAT_SCRAP) * rng.lognormal(0, 0.35, n) * (1 + 0.5 * (df[\"load_N\"] > 800)) * (1 + 0.3 * (df[\"temp_C\"] > 60)), 0.005, 0.5)\n",
" df[\"lead_days\"] = np.clip(rng.lognormal(np.log(25), 0.5, n) * np.where(df[\"import_part\"], 2.2, 1.0), 5, 300)\n",
" # ---- ์จ์ ์ง์ค๊ฐ(์ค์ ์ํ ๊ฒฐ๊ณผ์ ํด๋น): ๋ชจ๋ธ์ ์ ์
๋ ฅ์ผ๋ก ์ด๊ฒ์ ์์ธกํ๋ค ----\n",
" eff = np.where(is_poly, 0.55 + 0.45 * df[\"infill\"] / 100, 1 - 0.3 * ovh * 0.2)\n",
" aniso = np.where(is_poly, 1 - 0.3 * np.sin(np.radians(df[\"orient_deg\"])) ** 2, 1 - 0.08 * np.sin(np.radians(df[\"orient_deg\"])))\n",
" heat = np.where(is_poly, 1 - 0.35 * np.clip((df[\"temp_C\"] - 40) / 80, 0, 1), 0.98)\n",
" base = np.array([MAT[m][1] for m in mat])\n",
" df[\"strength_mpa\"] = base * eff * aniso * heat * rng.lognormal(0, 0.06, n)\n",
" cross = df[\"min_wall_mm\"] * np.sqrt(df[\"max_dim_mm\"]) * 1.0 # ํ์ค ์ง์ง ๋จ๋ฉด์ mm2(๋จ์ํ)\n",
" df[\"stress_mpa\"] = df[\"load_N\"] / cross\n",
" df[\"safety\"] = df[\"strength_mpa\"] / df[\"stress_mpa\"]\n",
" ratio = np.clip(df[\"stress_mpa\"] / df[\"strength_mpa\"], 0, 1.5)\n",
" df[\"log_cycles\"] = np.clip(7.5 - 6.0 * ratio + rng.normal(0, 0.25, n), 2, 7.5) # ํผ๋ก ์๋ช
log10(ํ)\n",
" support = 1 + 0.5 * ovh\n",
" df[\"build_h\"] = vol * support / np.array([MAT[m][6] for m in mat]) + 0.5\n",
" mass_kg = vol * np.array([MAT[m][2] for m in mat]) * support / 1000\n",
" df[\"print_cost\"] = mass_kg * np.array([MAT[m][3] for m in mat]) + df[\"build_h\"] * np.array([MAT[m][4] for m in mat]) + 3\n",
" df[\"buy_cost\"] = df[\"print_cost\"] * rng.lognormal(0.25, 0.45, n) + 5 # ๊ตฌ๋งค ๋จ๊ฐ(๋ง์, ๊ฐ์)\n",
" wall_ok = df[\"min_wall_mm\"] >= np.array([MAT[m][5] for m in mat]) * rng.uniform(0.95, 1.25, n)\n",
" dim_ok = df[\"max_dim_mm\"] <= np.where(is_poly, BUILD_MM[\"polymer\"], BUILD_MM[\"metal\"])\n",
" warp = (~is_poly) & (df[\"max_dim_mm\"] > 140) & (df[\"overhang\"] > 0.3) # ๊ธ์ ๋ํ ์ค๋ฒํ ๋ณํ\n",
" thr_bad = df[\"thread\"] & (df[\"min_wall_mm\"] < 1.5) & is_poly # ์์ ๋์ฌ๋ถ ํ์\n",
" df[\"printable\"] = wall_ok & dim_ok & (ovh < 0.5) & ~warp & ~thr_bad\n",
" risk = np.clip(1 - df[\"safety\"] / 3, 0, 1)\n",
" df[\"conform\"] = np.clip(100 * (0.45 * np.minimum(df[\"safety\"] / 3, 1) + 0.25 * np.clip((df[\"log_cycles\"] - 3) / 4.5, 0, 1)\n",
" + 0.15 * (1 - np.clip(ovh * 1.2, 0, 1)) + 0.15 * df[\"cert\"]) - 8 * risk * df[\"crit\"] / 3 + rng.normal(0, 2, n), 0, 100)\n",
" df[\"demand_month\"] = rng.poisson(np.where(df[\"cat\"].isin([\"clip\", \"handle\"]), 40, 15)) + 1 # ์ ์์์๋(๊ฐ์)\n",
" df[\"scrap_cost\"] = df[\"scrap_rate\"] * df[\"demand_month\"] * df[\"buy_cost\"] # ์ ํ๊ธฐ ์์ค์ก(๋ง์, ๊ฐ์)\n",
" return df\n",
"\n",
"# ---- ๋จ์ํํ ํด์(FEA ๋์ฉ) ๊ฐ๊ณผ ํ์ ํผ์ฒ: ์
๋ ฅ ์ ๋ณด๋ง์ผ๋ก ๊ณ์ฐํ๋ค(์ํ ๊ฒฐ๊ณผ ๋์ค ์์) ----\n",
"def make_features(d):\n",
" \"\"\"๊ฐ์ด ์๋ ฅ ํด์(์๋ ฅ์ง์ค๊ณ์ ํฌํจ)๊ณผ ์์ฌยท๊ณต์ ์ง์์์ ๋ง๋ ํ์ ํผ์ฒ 16๊ฐ.\"\"\"\n",
" X = pd.DataFrame(index=d.index)\n",
" base = d[\"material\"].map(lambda m: MAT[m][1]).astype(float)\n",
" X[\"base_strength\"] = base\n",
" X[\"is_metal\"] = (d[\"process\"] == \"metal\").astype(int)\n",
" X[\"cross_mm2\"] = d[\"min_wall_mm\"] * np.sqrt(d[\"max_dim_mm\"])\n",
" X[\"kt\"] = 1 + 0.15 * d[\"n_holes\"] + 0.5 * d[\"thread\"].astype(int) # ๊ตฌ๋ฉยท๋์ฌ๋ถ ์๋ ฅ์ง์ค๊ณ์(๊ฐ์ด)\n",
" X[\"fea_stress\"] = d[\"load_N\"] / X[\"cross_mm2\"] * X[\"kt\"] # ๊ฐ์ด ํด์ ์๋ ฅ(MPa)\n",
" X[\"stress_ratio\"] = X[\"fea_stress\"] / base # ๊ธฐ์ค๊ฐ๋ ๋๋น ์๋ ฅ๋น\n",
" X[\"infill_eff\"] = np.where(X[\"is_metal\"] == 1, 1.0, 0.55 + 0.45 * d[\"infill\"] / 100)\n",
" X[\"orient_sin2\"] = np.sin(np.radians(d[\"orient_deg\"])) ** 2\n",
" X[\"temp_derate\"] = np.where(X[\"is_metal\"] == 1, 0.0, np.clip((d[\"temp_C\"] - 40) / 80, 0, 1))\n",
" X[\"overhang_layer\"] = d[\"overhang\"] * d[\"layer_mm\"]\n",
" X[\"slender\"] = d[\"max_dim_mm\"] / d[\"min_wall_mm\"]\n",
" X[\"surf_vol\"] = d[\"area_cm2\"] / d[\"volume_cm3\"]\n",
" X[\"wall_ratio\"] = d[\"min_wall_mm\"] / d[\"material\"].map(lambda m: MAT[m][5])\n",
" X[\"load_per_vol\"] = d[\"load_N\"] / d[\"volume_cm3\"]\n",
" X[\"cert\"] = d[\"cert\"].astype(int)\n",
" X[\"crit\"] = d[\"crit\"]\n",
" return X\n",
"\n",
"parts = make_parts(900, seed=7) # ์ธ๋ฑ์ค ์์ = ์ํ ์ํ ์์(์์ด ๊ณผ๊ฑฐ)\n",
"X = make_features(parts)\n",
"print(f\"ํ์ ํผ์ฒ {X.shape[1]}๊ฐ ์์ฑ (๋ชฉํ 10๊ฐ ์ด์: {'๋ฌ์ฑ' if X.shape[1] >= 10 else '๋ฏธ๋ฌ'})\")\n",
"mape = lambda a, p: float(np.mean(np.abs(a - p) / np.abs(a)))\n",
"gb = lambda: GradientBoostingRegressor(n_estimators=150, max_depth=3, learning_rate=0.05, random_state=0)\n",
"\n",
"# 1) ์๊ณ์ด ๊ต์ฐจ๊ฒ์ฆ: ๊ณผ๊ฑฐ ์ํ์ผ๋ก ํ์ต -> ์ดํ ์ํ ์์ธก (5๋ถํ )\n",
"rows = []\n",
"for i, (tr, te) in enumerate(TimeSeriesSplit(5).split(X), 1):\n",
" ys, yf, bs = parts[\"strength_mpa\"], parts[\"log_cycles\"], X[\"base_strength\"]\n",
" base = parts.iloc[tr].groupby(\"material\")[\"strength_mpa\"].mean() # ๊ธฐ์ค๋ชจ๋ธ: ์์ฌ๋ณ ํ๊ท ๊ฐ๋\n",
" p0 = parts.iloc[te][\"material\"].map(base)\n",
" p1 = gb().fit(X.iloc[tr], (ys / bs).iloc[tr]).predict(X.iloc[te]) * bs.iloc[te] # ์์ฌ ๊ธฐ์ค๊ฐ๋ ๋๋น ์ ๊ฐ ๊ณ์๋ฅผ ํ์ต\n",
" q1 = gb().fit(X.iloc[tr], yf.iloc[tr]).predict(X.iloc[te])\n",
" rows.append({\"๋ถํ \": i, \"ํ์ต\": len(tr), \"๊ฒ์ฆ\": len(te), \"๊ฐ๋ ๊ธฐ์ค MAPE\": round(mape(ys.iloc[te], p0), 3), \"๊ฐ๋ GBM MAPE\": round(mape(ys.iloc[te], p1), 3),\n",
" \"ํผ๋ก์๋ช
MAE(log10)\": round(float(np.mean(np.abs(yf.iloc[te] - q1))), 2)})\n",
"cv = pd.DataFrame(rows)\n",
"print(cv.to_string(index=False))\n",
"m_b, m_g = cv[\"๊ฐ๋ ๊ธฐ์ค MAPE\"].mean(), cv[\"๊ฐ๋ GBM MAPE\"].mean()\n",
"print(f\"ํ๊ท ์ธ์ฅ๊ฐ๋ MAPE: ๊ธฐ์ค๋ชจ๋ธ(์์ฌ๋ณ ํ๊ท ) {m_b:.1%}, GBM {m_g:.1%} (๋ชฉํ 25% ์ดํ: {'๋ฌ์ฑ' if m_g <= 0.25 else '๋ฏธ๋ฌ'})\")\n",
"print(f\"ํผ๋ก ์๋ช
์์ธก ํ๊ท ์ค์ฐจ {cv['ํผ๋ก์๋ช
MAE(log10)'].mean():.2f} ์๋ฆฟ์(log10 ํ)\")\n",
"\n",
"# 2) ๋ง์ง๋ง 20%๋ฅผ ์ ๋ขฐ์ฑ Hold-out์ผ๋ก: ์์ธก ์์ ์จ๋ก '๋์ฒด ์ ์ ์ ๋ขฐ์ฑ ์ถฉ์กฑ(์์ ์จ 1.5 ์ด์)' ํ๋ณ\n",
"cut = int(len(parts) * 0.8)\n",
"ratio = parts[\"strength_mpa\"] / X[\"base_strength\"]\n",
"ms = gb().fit(X.iloc[:cut], ratio.iloc[:cut])\n",
"te = parts.iloc[cut:]\n",
"p_all = ms.predict(X.iloc[cut:]) * X.iloc[cut:][\"base_strength\"]\n",
"pred_sf = p_all / (te[\"load_N\"] / X.iloc[cut:][\"cross_mm2\"])\n",
"ok_true, ok_pred = te[\"safety\"] >= 1.5, pred_sf >= 1.5\n",
"print(f\"\\n[์์ ์จ 1.5 ์ด์ ํ๋ณ] ์ ํ๋ {(ok_true == ok_pred).mean():.1%}, ์ํ ์คํ(๋ถ์ถฉ๋ถํ๋ฐ ์ถฉ์กฑ์ด๋ผ ํ์ ) {int((~ok_true & ok_pred).sum())}๊ฑด / {len(te)}๊ฑด\")\n",
"print(\"๊ฐ์ด ํด์ ์๋ ฅ๋ง์ผ๋ก ๋ง๋ ํ๋ณ์ ์ ํ๋:\", f\"{((X.iloc[cut:]['stress_ratio'] < 1 / 1.5) == ok_true).mean():.1%}\")\n",
"\n",
"# 3) ํผ์ฒ ์ค์๋์ ์ฐจ์ ์ถ์ ํ๋ณด\n",
"model = gb().fit(X, ratio)\n",
"imp = pd.Series(model.feature_importances_, X.columns).sort_values(ascending=False)\n",
"print(\"\\nํผ์ฒ ์ค์๋ ์์ 5(๊ฐ๋ ์ ๊ฐ ๊ณ์ ๋ชจ๋ธ):\", \", \".join(f\"{k}({v:.2f})\" for k, v in imp.head(5).items()))\n",
"print(\"์ค์๋ 0.01 ๋ฏธ๋ง ํผ์ฒ(์ฐจ์ ์ถ์ ํ๋ณด):\", list(imp[imp < 0.01].index))\n",
"print(\"๋จ, ๋จ๋ฉด์ ยท์๋ ฅ์ง์คยท์๋ ฅ๋น๋ ๊ฐ๋ ๋ชจ๋ธ์์๋ ์ฝํ์ง๋ง ์์ ์จ(ํ์ค ์ชฝ) ๊ณ์ฐ์ ํ์ํ๋ฏ๋ก ์ญ์ ํ์ง ์๊ณ ๋ชจ๋ธ๋ณ๋ก ๋๋ ์ด๋ค.\")\n",
"print(\"ํผ์ฒ ์ค๊ณ ๊ทผ๊ฑฐ: ์ถฉ์ ์จยท์ ์ธต ๋ฐฉํฅยท์์ฌยท์จ๋ ์ ํ๋ ๊ฐ๋๋ฅผ ๊น๋ ์์ธ์ด๊ณ ๋จ๋ฉด์ ยท๊ตฌ๋ฉยท๋์ฌ๋ถ๋ ์๋ ฅ์ ํค์ฐ๋ ์์ธ์ด๋ผ ์ ๋น ํ์ฅ์์๋ ์ค๋ช
์ด ๋๋ค.\")\n",
"\n",
"fig, ax = plt.subplots(1, 2, figsize=(11, 4))\n",
"imp.head(10)[::-1].plot.barh(ax=ax[0]); ax[0].set_title(\"Feature importance (strength knock-down)\")\n",
"ax[1].scatter(te[\"strength_mpa\"], p_all, s=10); ax[1].plot([0, 1000], [0, 1000], \"r--\")\n",
"ax[1].set_xlabel(\"measured strength (MPa)\"); ax[1].set_ylabel(\"predicted\"); ax[1].set_title(\"Hold-out: strength prediction\")\n",
"plt.tight_layout(); plt.show()\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"๐ก [์ฐธ๊ณ ] ๋ฏธ์
04 ์์ ์คํ ๊ฒฐ๊ณผ (Output)
\n",
"\n",
"```text\n",
"ํ์ ํผ์ฒ 16๊ฐ ์์ฑ (๋ชฉํ 10๊ฐ ์ด์: ๋ฌ์ฑ)\n",
" ๋ถํ ํ์ต ๊ฒ์ฆ ๊ฐ๋ ๊ธฐ์ค MAPE ๊ฐ๋ GBM MAPE ํผ๋ก์๋ช
MAE(log10)\n",
" 1 150 150 0.100 0.066 0.39\n",
" 2 300 150 0.102 0.057 0.27\n",
" 3 450 150 0.121 0.056 0.28\n",
" 4 600 150 0.095 0.052 0.32\n",
" 5 750 150 0.118 0.062 0.26\n",
"ํ๊ท ์ธ์ฅ๊ฐ๋ MAPE: ๊ธฐ์ค๋ชจ๋ธ(์์ฌ๋ณ ํ๊ท ) 10.7%, GBM 5.9% (๋ชฉํ 25% ์ดํ: ๋ฌ์ฑ)\n",
"ํผ๋ก ์๋ช
์์ธก ํ๊ท ์ค์ฐจ 0.30 ์๋ฆฟ์(log10 ํ)\n",
"\n",
"[์์ ์จ 1.5 ์ด์ ํ๋ณ] ์ ํ๋ 97.8%, ์ํ ์คํ(๋ถ์ถฉ๋ถํ๋ฐ ์ถฉ์กฑ์ด๋ผ ํ์ ) 2๊ฑด / 180๊ฑด\n",
"๊ฐ์ด ํด์ ์๋ ฅ๋ง์ผ๋ก ๋ง๋ ํ๋ณ์ ์ ํ๋: 93.9%\n",
"\n",
"ํผ์ฒ ์ค์๋ ์์ 5(๊ฐ๋ ์ ๊ฐ ๊ณ์ ๋ชจ๋ธ): infill_eff(0.33), orient_sin2(0.22), is_metal(0.19), base_strength(0.13), temp_derate(0.11)\n",
"์ค์๋ 0.01 ๋ฏธ๋ง ํผ์ฒ(์ฐจ์ ์ถ์ ํ๋ณด): ['overhang_layer', 'fea_stress', 'load_per_vol', 'surf_vol', 'stress_ratio', 'slender', 'kt', 'wall_ratio', 'crit', 'cross_mm2', 'cert']\n",
"๋จ, ๋จ๋ฉด์ ยท์๋ ฅ์ง์คยท์๋ ฅ๋น๋ ๊ฐ๋ ๋ชจ๋ธ์์๋ ์ฝํ์ง๋ง ์์ ์จ(ํ์ค ์ชฝ) ๊ณ์ฐ์ ํ์ํ๋ฏ๋ก ์ญ์ ํ์ง ์๊ณ ๋ชจ๋ธ๋ณ๋ก ๋๋ ์ด๋ค.\n",
"ํผ์ฒ ์ค๊ณ ๊ทผ๊ฑฐ: ์ถฉ์ ์จยท์ ์ธต ๋ฐฉํฅยท์์ฌยท์จ๋ ์ ํ๋ ๊ฐ๋๋ฅผ ๊น๋ ์์ธ์ด๊ณ ๋จ๋ฉด์ ยท๊ตฌ๋ฉยท๋์ฌ๋ถ๋ ์๋ ฅ์ ํค์ฐ๋ ์์ธ์ด๋ผ ์ ๋น ํ์ฅ์์๋ ์ค๋ช
์ด ๋๋ค.\n",
"```\n",
" \n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## ๐ฏ ๋ฏธ์
05. ๊ธด๊ธ ์ผ์ ์ ์ ๋ถํ์ ๊ตฐ์ฌ ๊ท๊ฒฉ(MIL-STD) ๋ถํฉ์ฑ ์ ๋ขฐ๋ ์ ์ ์ฐ์ถ ์์ง ๊ฐ๋ฐ\n",
"**์์์๊ฐ**: ์ฝ 3.5์๊ฐ\n",
"\n",
"### ๐ ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ์ํ ์ง์นจ\n",
"#### ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ๋น์์ฑ\n",
"\n",
"์ผ์ ์ ์ ๋ถํ์ ๊ตฐ์ฌ ๊ท๊ฒฉ(MIL-STD) ๋ถํฉ์ฑ ์ ๋ขฐ๋ ์์ง์ ๊ฐ๋ฐํ๋ค. ์ค์ ์ ์ ํ์ฅ์์๋ ๋จ์ ํ์ง์ ๊ทธ์น์ง ์๊ณ ์ ์ํ ์ํ ํ๋จ๊ณผ ์ ๋ขฐ๋ ๋์ ์์ฌ๊ฒฐ์ ์ด ํ์์ ์ด๋ฏ๋ก, **๊ธด๊ธ ์ผ์ ์ ์ ๋ถํ์ ๊ตฐ ๊ท๊ฒฉ ๋ถํฉ์ฑ์ 0~100์ ์ผ๋ก ํ์ฐํ๋ ์ ๋ขฐ๋ ์ ์ ์ฐ์ถ ์์ง์ ๊ฐ๋ฐํ๋ค(์ค์ ๊ท๊ฒฉ ์๋ฌธ ๋์ ๊ฐ์ ๊ธฐ์ค ์ ์ ์ฌ์ฉ).** ๊ณผ์
์ ์์ํ์ฌ ์คํ์ ๋ฐฉ์งํ๊ณ ์์ ๋์๋ ฅ์ ๊ทน๋ํํด์ผ ํฉ๋๋ค.\n",
"\n",
"์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ณธ ๋ฏธ์
์์๋ **์์ ์จยทํผ๋ก ์๋ช
ยท๋์ถ ์ํยท์์ฌ ์ธ์ฆ์๋ฅผ ๋ฐ์ํ ๊ฐ์ ๋ถํฉ ์ ์๋ฅผ ์์๋ธ ํ๊ท(GBM+RandomForest)๋ก ์์ธกํด ์ ์
๋ ฅ 5๊ฐ ์ ํ ๊ธฐ์ค๋ชจ๋ธ๊ณผ Hold-out์์ MAEยทRMSEยทR2๋ฅผ ๋น๊ต ์ํํ๊ณ ๊ฒ์ฆํ๋** ์ค์ต์ ์ฒด๊ณ์ ์ผ๋ก ์ํํฉ๋๋ค.\n",
"\n",
"ํ์ต์๋ ์ค์ต๊ฐ์ด๋์ ์ ์๋ ์ด 4๊ฐ ์ธ๋ถ ํ๋(๊ธฐ๋ณธ ๊ธฐ๋ฅ ๊ตฌํ โ ๊ฒฐ๊ณผ ์งํ ๊ฒ์ฆ โ ํ๋ผ๋ฏธํฐ ํ๋ ๋ฐ ๋์ ๊ณผ์ )์ ์์ฐจ์ ์ผ๋ก ์งํํ๋ฉฐ ํต์ฌ ํ์ ๊ธฐ์ค์ ๋ฌ์ฑํฉ๋๋ค.\n",
"\n",
"**์ด๋ฒ ๊ฒฐ๊ณผ๋ฌผ:** `src/models/mnt06_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": [
"\"\"\"๊ธด๊ธ ์ผ์ ์ ์ ๋ถํ์ ๊ตฐ ๊ท๊ฒฉ ๋ถํฉ ์ ๋ขฐ๋ ์ ์(0~100, ๊ฐ์ ๊ธฐ์ค)๋ฅผ ์์๋ธ ํ๊ท(GBM+RandomForest)๋ก ์ฐ์ถํ๋ ์์ง์ ๋ง๋ค๊ณ , Hold-out ๋ถํ์์ ์ ์
๋ ฅ 5๊ฐ ์ ํ ๊ธฐ์ค๋ชจ๋ธ ๋๋น MAE/RMSE ๊ฐ์ ์จ๊ณผ R2, ์ ์ ๊ตฌ๊ฐ๋ณ ์ค์ ๋ถํฉ๋ฅ (๋ณด์ ์ํ)๊ณผ ์์ธก ๋ถํ์ค์ฑ์ ๊ฒ์ฆํ๋ค.\"\"\"\n",
"import numpy as np\n",
"import pandas as pd\n",
"import matplotlib.pyplot as plt\n",
"from sklearn.ensemble import GradientBoostingRegressor, RandomForestRegressor\n",
"from sklearn.linear_model import LinearRegression\n",
"from sklearn.metrics import mean_absolute_error, mean_squared_error, r2_score, roc_auc_score\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# [๊ต์ก์ฉ๊ฐ์์๋ฃ] ์ผ์ 3D ํ๋ฆฐํ
๋์ฒด ์ ์ ํ๋ณด ๋ถํ ์์ฑ๊ธฐ (์ค์ ๋ถํยท์ฌ๊ณ ยท๊ท๊ฒฉ ๊ฐ ์๋)\n",
"# ํ๊ธฐ์จ์ด ๋์ ์๋ฆฌ๋ถ์ ์ค 3D ํ๋ฆฐํ
์ผ๋ก ๋์ฒด ์ ์ํ ์ ์๋์ง ํ๋จํ๊ธฐ ์ํ ๊ฐ์ ๋ถํ ์๋ฃ๋ฅผ ๋ง๋ ๋ค.\n",
"# ์์ฌ ๋ฌผ์ฑยท์น์ยทํ์ค์ ๊ต์ก์ฉ์ผ๋ก ๋จ์ํํ ๊ฐ์ด๋ฉฐ ์ค์ ์ํ ์ฑ์ ์๋ ๊ตฐ ๊ท๊ฒฉ ๊ฐ๊ณผ ๋ฌด๊ดํ๋ค.\n",
"# ---------------------------------------------------------------------------\n",
"MAT = { # ์์ฌ: (๊ณต์ , ๊ธฐ์ค ์ธ์ฅ๊ฐ๋ MPa, ๋ฐ๋ g/cm3, ์์ฌ ๋จ๊ฐ ๋ง์/kg, ์ถ๋ ฅ๋น ๋ง์/h, ์ต์ ๋ฒฝ๋๊ป mm, ์ถ๋ ฅ ์๋ cm3/h)\n",
" \"PA12\": (\"polymer\", 45, 1.0, 8, 3, 0.8, 18), \"PETG\": (\"polymer\", 50, 1.27, 5, 2, 0.8, 15),\n",
" \"CF-PA\": (\"polymer\", 70, 1.2, 14, 3, 1.0, 12), \"AlSi10Mg\": (\"metal\", 350, 2.67, 30, 25, 0.5, 4),\n",
" \"SS316L\": (\"metal\", 520, 7.9, 28, 25, 0.6, 3.5), \"Ti64\": (\"metal\", 900, 4.4, 90, 30, 0.6, 3)}\n",
"CATS = [\"bracket\", \"housing\", \"clip\", \"gear\", \"duct\", \"handle\"]\n",
"CAT_SCRAP = {\"bracket\": 0.04, \"housing\": 0.05, \"clip\": 0.12, \"gear\": 0.09, \"duct\": 0.08, \"handle\": 0.10} # ๋ถํ๊ตฐ๋ณ ๊ธฐ๋ณธ ์ ํ๊ธฐ์จ\n",
"BUILD_MM = {\"polymer\": 300, \"metal\": 200} # ๊ฐ์ ์ถ๋ ฅ ๊ฐ๋ฅ ์ต๋ ์น์\n",
"\n",
"\n",
"def make_parts(n=300, seed=0):\n",
" \"\"\"๊ฐ์ ๋ถํ n์ข
: ํ์(๋ถํผยท๋ฒฝ๋๊ปยท๋์ถ ๋น์จ), ์์ฌ, ์ฌ์ฉ ํ์ค, ํ๊ธฐ์จ๊ณผ ์กฐ๋ฌ ์ ๋ณด๋ฅผ ๋ง๋ ๋ค.\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" mat = rng.choice(list(MAT), n, p=[0.2, 0.15, 0.1, 0.2, 0.2, 0.15])\n",
" proc = np.array([MAT[m][0] for m in mat])\n",
" vol = np.clip(rng.lognormal(np.log(30), 0.9, n), 2, 800)\n",
" aspect = rng.lognormal(0.3, 0.4, n)\n",
" max_dim = 10 * vol ** (1 / 3) * aspect ** (1 / 3) * rng.uniform(1.0, 1.6, n)\n",
" wall = np.clip(rng.lognormal(np.log(2.0), 0.6, n), 0.3, 8)\n",
" ovh = rng.beta(2, 5, n)\n",
" is_poly = proc == \"polymer\"\n",
" df = pd.DataFrame({\n",
" \"part_id\": [f\"P{i:03d}\" for i in range(n)], \"cat\": rng.choice(CATS, n, p=[0.2, 0.15, 0.2, 0.1, 0.15, 0.2]), \"material\": mat, \"process\": proc,\n",
" \"volume_cm3\": vol, \"max_dim_mm\": max_dim, \"area_cm2\": 6 * vol ** (2 / 3) * rng.uniform(1.0, 2.2, n), \"min_wall_mm\": wall, \"overhang\": ovh,\n",
" \"n_holes\": rng.poisson(2.0, n), \"thread\": rng.random(n) < 0.25, \"orient_deg\": rng.uniform(0, 90, n),\n",
" \"layer_mm\": np.where(is_poly, rng.choice([0.1, 0.2, 0.3], n), rng.choice([0.03, 0.05], n)),\n",
" \"infill\": np.where(is_poly, rng.choice([40, 60, 80, 100], n, p=[0.15, 0.3, 0.3, 0.25]), 100),\n",
" \"load_N\": np.clip(rng.lognormal(np.where(is_poly, np.log(230), np.log(2300)), 0.7, n), 10, 20000), \"temp_C\": rng.uniform(-20, 90, n),\n",
" \"crit\": rng.choice([1, 2, 3], n, p=[0.35, 0.4, 0.25]), \"cert\": rng.choice([0, 1], n, p=[0.45, 0.55]), # ์์ฌ ์ธ์ฆ์ ๋ณด์ \n",
" \"import_part\": rng.random(n) < 0.4})\n",
" df[\"scrap_rate\"] = np.clip(df[\"cat\"].map(CAT_SCRAP) * rng.lognormal(0, 0.35, n) * (1 + 0.5 * (df[\"load_N\"] > 800)) * (1 + 0.3 * (df[\"temp_C\"] > 60)), 0.005, 0.5)\n",
" df[\"lead_days\"] = np.clip(rng.lognormal(np.log(25), 0.5, n) * np.where(df[\"import_part\"], 2.2, 1.0), 5, 300)\n",
" # ---- ์จ์ ์ง์ค๊ฐ(์ค์ ์ํ ๊ฒฐ๊ณผ์ ํด๋น): ๋ชจ๋ธ์ ์ ์
๋ ฅ์ผ๋ก ์ด๊ฒ์ ์์ธกํ๋ค ----\n",
" eff = np.where(is_poly, 0.55 + 0.45 * df[\"infill\"] / 100, 1 - 0.3 * ovh * 0.2)\n",
" aniso = np.where(is_poly, 1 - 0.3 * np.sin(np.radians(df[\"orient_deg\"])) ** 2, 1 - 0.08 * np.sin(np.radians(df[\"orient_deg\"])))\n",
" heat = np.where(is_poly, 1 - 0.35 * np.clip((df[\"temp_C\"] - 40) / 80, 0, 1), 0.98)\n",
" base = np.array([MAT[m][1] for m in mat])\n",
" df[\"strength_mpa\"] = base * eff * aniso * heat * rng.lognormal(0, 0.06, n)\n",
" cross = df[\"min_wall_mm\"] * np.sqrt(df[\"max_dim_mm\"]) * 1.0 # ํ์ค ์ง์ง ๋จ๋ฉด์ mm2(๋จ์ํ)\n",
" df[\"stress_mpa\"] = df[\"load_N\"] / cross\n",
" df[\"safety\"] = df[\"strength_mpa\"] / df[\"stress_mpa\"]\n",
" ratio = np.clip(df[\"stress_mpa\"] / df[\"strength_mpa\"], 0, 1.5)\n",
" df[\"log_cycles\"] = np.clip(7.5 - 6.0 * ratio + rng.normal(0, 0.25, n), 2, 7.5) # ํผ๋ก ์๋ช
log10(ํ)\n",
" support = 1 + 0.5 * ovh\n",
" df[\"build_h\"] = vol * support / np.array([MAT[m][6] for m in mat]) + 0.5\n",
" mass_kg = vol * np.array([MAT[m][2] for m in mat]) * support / 1000\n",
" df[\"print_cost\"] = mass_kg * np.array([MAT[m][3] for m in mat]) + df[\"build_h\"] * np.array([MAT[m][4] for m in mat]) + 3\n",
" df[\"buy_cost\"] = df[\"print_cost\"] * rng.lognormal(0.25, 0.45, n) + 5 # ๊ตฌ๋งค ๋จ๊ฐ(๋ง์, ๊ฐ์)\n",
" wall_ok = df[\"min_wall_mm\"] >= np.array([MAT[m][5] for m in mat]) * rng.uniform(0.95, 1.25, n)\n",
" dim_ok = df[\"max_dim_mm\"] <= np.where(is_poly, BUILD_MM[\"polymer\"], BUILD_MM[\"metal\"])\n",
" warp = (~is_poly) & (df[\"max_dim_mm\"] > 140) & (df[\"overhang\"] > 0.3) # ๊ธ์ ๋ํ ์ค๋ฒํ ๋ณํ\n",
" thr_bad = df[\"thread\"] & (df[\"min_wall_mm\"] < 1.5) & is_poly # ์์ ๋์ฌ๋ถ ํ์\n",
" df[\"printable\"] = wall_ok & dim_ok & (ovh < 0.5) & ~warp & ~thr_bad\n",
" risk = np.clip(1 - df[\"safety\"] / 3, 0, 1)\n",
" df[\"conform\"] = np.clip(100 * (0.45 * np.minimum(df[\"safety\"] / 3, 1) + 0.25 * np.clip((df[\"log_cycles\"] - 3) / 4.5, 0, 1)\n",
" + 0.15 * (1 - np.clip(ovh * 1.2, 0, 1)) + 0.15 * df[\"cert\"]) - 8 * risk * df[\"crit\"] / 3 + rng.normal(0, 2, n), 0, 100)\n",
" df[\"demand_month\"] = rng.poisson(np.where(df[\"cat\"].isin([\"clip\", \"handle\"]), 40, 15)) + 1 # ์ ์์์๋(๊ฐ์)\n",
" df[\"scrap_cost\"] = df[\"scrap_rate\"] * df[\"demand_month\"] * df[\"buy_cost\"] # ์ ํ๊ธฐ ์์ค์ก(๋ง์, ๊ฐ์)\n",
" return df\n",
"\n",
"# ---- ๋จ์ํํ ํด์(FEA ๋์ฉ) ๊ฐ๊ณผ ํ์ ํผ์ฒ: ์
๋ ฅ ์ ๋ณด๋ง์ผ๋ก ๊ณ์ฐํ๋ค(์ํ ๊ฒฐ๊ณผ ๋์ค ์์) ----\n",
"def make_features(d):\n",
" \"\"\"๊ฐ์ด ์๋ ฅ ํด์(์๋ ฅ์ง์ค๊ณ์ ํฌํจ)๊ณผ ์์ฌยท๊ณต์ ์ง์์์ ๋ง๋ ํ์ ํผ์ฒ 16๊ฐ.\"\"\"\n",
" X = pd.DataFrame(index=d.index)\n",
" base = d[\"material\"].map(lambda m: MAT[m][1]).astype(float)\n",
" X[\"base_strength\"] = base\n",
" X[\"is_metal\"] = (d[\"process\"] == \"metal\").astype(int)\n",
" X[\"cross_mm2\"] = d[\"min_wall_mm\"] * np.sqrt(d[\"max_dim_mm\"])\n",
" X[\"kt\"] = 1 + 0.15 * d[\"n_holes\"] + 0.5 * d[\"thread\"].astype(int) # ๊ตฌ๋ฉยท๋์ฌ๋ถ ์๋ ฅ์ง์ค๊ณ์(๊ฐ์ด)\n",
" X[\"fea_stress\"] = d[\"load_N\"] / X[\"cross_mm2\"] * X[\"kt\"] # ๊ฐ์ด ํด์ ์๋ ฅ(MPa)\n",
" X[\"stress_ratio\"] = X[\"fea_stress\"] / base # ๊ธฐ์ค๊ฐ๋ ๋๋น ์๋ ฅ๋น\n",
" X[\"infill_eff\"] = np.where(X[\"is_metal\"] == 1, 1.0, 0.55 + 0.45 * d[\"infill\"] / 100)\n",
" X[\"orient_sin2\"] = np.sin(np.radians(d[\"orient_deg\"])) ** 2\n",
" X[\"temp_derate\"] = np.where(X[\"is_metal\"] == 1, 0.0, np.clip((d[\"temp_C\"] - 40) / 80, 0, 1))\n",
" X[\"overhang_layer\"] = d[\"overhang\"] * d[\"layer_mm\"]\n",
" X[\"slender\"] = d[\"max_dim_mm\"] / d[\"min_wall_mm\"]\n",
" X[\"surf_vol\"] = d[\"area_cm2\"] / d[\"volume_cm3\"]\n",
" X[\"wall_ratio\"] = d[\"min_wall_mm\"] / d[\"material\"].map(lambda m: MAT[m][5])\n",
" X[\"load_per_vol\"] = d[\"load_N\"] / d[\"volume_cm3\"]\n",
" X[\"cert\"] = d[\"cert\"].astype(int)\n",
" X[\"crit\"] = d[\"crit\"]\n",
" return X\n",
"\n",
"train = pd.concat([make_parts(1000, seed=s) for s in (11, 12)], ignore_index=True)\n",
"test = make_parts(600, seed=21) # Hold-out: ํ์ต์ ์ฐ์ง ์์ ๋ถํ\n",
"Xtr, Xte, ytr, yte = make_features(train), make_features(test), train[\"conform\"], test[\"conform\"]\n",
"\n",
"# 1) ๊ธฐ์ค๋ชจ๋ธ(์ ์
๋ ฅ 5๊ฐ ์ ํ) vs ์์๋ธ\n",
"raw_cols = [\"load_N\", \"min_wall_mm\", \"infill\", \"temp_C\", \"cert\"]\n",
"base = LinearRegression().fit(train[raw_cols], ytr)\n",
"gbm = GradientBoostingRegressor(n_estimators=300, max_depth=3, learning_rate=0.05, random_state=0).fit(Xtr, ytr)\n",
"rf = RandomForestRegressor(n_estimators=150, min_samples_leaf=3, random_state=0, n_jobs=1).fit(Xtr, ytr)\n",
"preds = {\"๊ธฐ์ค๋ชจ๋ธ(์ ํ 5์
๋ ฅ)\": base.predict(test[raw_cols]), \"GBM\": gbm.predict(Xte), \"RandomForest\": rf.predict(Xte)}\n",
"preds[\"์์๋ธ(GBM+RF)\"] = (preds[\"GBM\"] + preds[\"RandomForest\"]) / 2\n",
"tab = pd.DataFrame([{\"๋ชจ๋ธ\": k, \"MAE\": mean_absolute_error(yte, p), \"RMSE\": mean_squared_error(yte, p) ** 0.5, \"R2\": r2_score(yte, p)} for k, p in preds.items()]).round(3)\n",
"print(f\"ํ์ต {len(train)}์ข
/ Hold-out {len(test)}์ข
\")\n",
"print(tab.to_string(index=False))\n",
"b, e = tab.iloc[0], tab.iloc[-1]\n",
"imp_mae, imp_rmse = 1 - e[\"MAE\"] / b[\"MAE\"], 1 - e[\"RMSE\"] / b[\"RMSE\"]\n",
"print(f\"๊ธฐ์ค๋ชจ๋ธ ๋๋น ๊ฐ์ : MAE {imp_mae:.1%}, RMSE {imp_rmse:.1%} (๋ชฉํ 20% ์ด์: {'๋ฌ์ฑ' if min(imp_mae, imp_rmse) >= 0.2 else '๋ฏธ๋ฌ'}), \"\n",
" f\"R2 {e['R2']:.3f} (๋ชฉํ 0.85 ์ด์: {'๋ฌ์ฑ' if e['R2'] >= 0.85 else '๋ฏธ๋ฌ'})\")\n",
"\n",
"# 2) ์ ๋ขฐ๋ ์ ์ ๊ตฌ๊ฐ๋ณ ์ค์ ๋ถํฉ๋ฅ : ์ค์ ์ ์ 70 ์ด์์ '๊ท๊ฒฉ ๋ถํฉ'์ผ๋ก ๋ณธ๋ค(๊ฐ์ ๊ธฐ์ค)\n",
"score = np.clip(preds[\"์์๋ธ(GBM+RF)\"], 0, 100)\n",
"actual = (yte >= 70).values\n",
"bins = pd.cut(score, [-1, 50, 70, 85, 101], labels=[\"<50 ๋ถ์ ํฉ ์์ฌ\", \"50~70 ๋ณด์ ํ์\", \"70~85 ์กฐ๊ฑด๋ถ\", \">=85 ๋ถํฉ ์ ๋ ฅ\"])\n",
"cal = pd.DataFrame({\"๊ตฌ๊ฐ\": bins, \"์ ์\": score, \"์ค์ ๋ถํฉ\": actual}).groupby(\"๊ตฌ๊ฐ\", observed=True).agg(๊ฑด์=(\"์ ์\", \"size\"), ํ๊ท ์ ์=(\"์ ์\", \"mean\"), ์ค์ ๋ถํฉ๋ฅ =(\"์ค์ ๋ถํฉ\", \"mean\")).round(2)\n",
"print(\"\\n[์ ์ ๊ตฌ๊ฐ๋ณ ์ค์ ๋ถํฉ๋ฅ ]\")\n",
"print(cal.to_string())\n",
"mono = bool((cal[\"์ค์ ๋ถํฉ๋ฅ \"].diff().dropna() >= 0).all())\n",
"print(f\"์ ์ ๊ตฌ๊ฐ์ด ๋์์๋ก ์ค์ ๋ถํฉ๋ฅ ์ด {'๋จ์กฐ ์ฆ๊ฐ' if mono else '๋จ์กฐ ์ฆ๊ฐํ์ง ์์'}, AUC {roc_auc_score(actual, score):.3f}\")\n",
"\n",
"# 3) ์์ธก ์์ ์ฑ: ๋ ๋ชจ๋ธ์ ์๊ฒฌ ์ฐจ์ด๊ฐ ํฐ ๋ถํ์ 'ํ์ ๋ณด๋ฅ' ํ๋ณด\n",
"gap = np.abs(preds[\"GBM\"] - preds[\"RandomForest\"])\n",
"hold = gap > np.quantile(gap, 0.9)\n",
"print(f\"\\n[์์ธก ์์ ์ฑ] ๋ชจ๋ธ ๊ฐ ์ฐจ์ด ์์ 10% {int(hold.sum())}์ข
: ํ๊ท ์ค์ฐจ {np.abs(yte[hold] - score[hold]).mean():.1f}์ vs ๋๋จธ์ง {np.abs(yte[~hold] - score[~hold]).mean():.1f}์ \")\n",
"irr = test[\"cert\"].eq(0) & test[\"crit\"].eq(3)\n",
"print(f\"์ธ์ฆ์ ์๋ ํต์ฌ ๋ถํ {int(irr.sum())}์ข
: ํ๊ท ์ ์ {score[irr.values].mean():.1f}์ -> ์ ์์ ๋ณ๊ฐ๋ก ์์ฌ ์ธ์ฆ ํ๋ณด๊ฐ ์ ํ๋์ด์ผ ํ๋ค\")\n",
"\n",
"fig, ax = plt.subplots(1, 2, figsize=(11, 4))\n",
"ax[0].scatter(yte, score, s=8); ax[0].plot([0, 100], [0, 100], \"r--\"); ax[0].set_xlabel(\"actual score\"); ax[0].set_ylabel(\"predicted\")\n",
"ax[0].set_title(f\"Conformity score hold-out (R2={e['R2']:.2f})\")\n",
"ax[1].plot(cal[\"ํ๊ท ์ ์\"], cal[\"์ค์ ๋ถํฉ๋ฅ \"], \"o-\"); ax[1].set_xlabel(\"mean confidence score\"); ax[1].set_ylabel(\"actual conform rate\"); ax[1].set_title(\"Score band calibration\")\n",
"plt.tight_layout(); plt.show()\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"๐ก [์ฐธ๊ณ ] ๋ฏธ์
05 ์์ ์คํ ๊ฒฐ๊ณผ (Output)
\n",
"\n",
"```text\n",
"ํ์ต 2000์ข
/ Hold-out 600์ข
\n",
" ๋ชจ๋ธ MAE RMSE R2\n",
"๊ธฐ์ค๋ชจ๋ธ(์ ํ 5์
๋ ฅ) 15.800 18.627 0.438\n",
" GBM 3.893 5.120 0.958\n",
"RandomForest 4.246 5.740 0.947\n",
" ์์๋ธ(GBM+RF) 3.916 5.208 0.956\n",
"๊ธฐ์ค๋ชจ๋ธ ๋๋น ๊ฐ์ : MAE 75.2%, RMSE 72.0% (๋ชฉํ 20% ์ด์: ๋ฌ์ฑ), R2 0.956 (๋ชฉํ 0.85 ์ด์: ๋ฌ์ฑ)\n",
"\n",
"[์ ์ ๊ตฌ๊ฐ๋ณ ์ค์ ๋ถํฉ๋ฅ ]\n",
" ๊ฑด์ ํ๊ท ์ ์ ์ค์ ๋ถํฉ๋ฅ \n",
"๊ตฌ๊ฐ \n",
"<50 ๋ถ์ ํฉ ์์ฌ 217 32.12 0.00\n",
"50~70 ๋ณด์ ํ์ 131 60.96 0.16\n",
"70~85 ์กฐ๊ฑด๋ถ 139 77.20 0.91\n",
">=85 ๋ถํฉ ์ ๋ ฅ 113 90.07 1.00\n",
"์ ์ ๊ตฌ๊ฐ์ด ๋์์๋ก ์ค์ ๋ถํฉ๋ฅ ์ด ๋จ์กฐ ์ฆ๊ฐ, AUC 0.987\n",
"\n",
"[์์ธก ์์ ์ฑ] ๋ชจ๋ธ ๊ฐ ์ฐจ์ด ์์ 10% 60์ข
: ํ๊ท ์ค์ฐจ 6.6์ vs ๋๋จธ์ง 3.6์ \n",
"์ธ์ฆ์ ์๋ ํต์ฌ ๋ถํ 57์ข
: ํ๊ท ์ ์ 48.5์ -> ์ ์์ ๋ณ๊ฐ๋ก ์์ฌ ์ธ์ฆ ํ๋ณด๊ฐ ์ ํ๋์ด์ผ ํ๋ค\n",
"```\n",
" \n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## ๐ฏ ๋ฏธ์
06. ๋ถํ๋ณ ์ ์ ์์ ์๊ฐ, ๋น์ฉ ๋ฐ ์ ๋ขฐ์ฑ ํ๊ฐ ๊ฒฐ๊ณผ ๊ฐ์ํ ๋์๋ณด๋ ๊ตฌํ\n",
"**์์์๊ฐ**: ์ฝ 3.0์๊ฐ\n",
"\n",
"### ๐ ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ์ํ ์ง์นจ\n",
"#### ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ๋น์์ฑ\n",
"\n",
"๋ถํ๋ณ ์ ์ ์์ ์๊ฐ, ๋น์ฉ ๋ฐ ์ ๋ขฐ์ฑ ํ๊ฐ ๊ฐ์ํ ๋์๋ณด๋๋ฅผ ๊ตฌํํ๋ค. ์ค์ ์ ์ ํ์ฅ์์๋ ๋จ์ ํ์ง์ ๊ทธ์น์ง ์๊ณ ์ ์ํ ์ํ ํ๋จ๊ณผ ์ ๋ขฐ๋ ๋์ ์์ฌ๊ฒฐ์ ์ด ํ์์ ์ด๋ฏ๋ก, **๋ถํ๋ณ ์ ์ ์์ ์๊ฐ, ๋น์ฉ, ์ ๋ขฐ์ฑ ์ ์์ ํ๊ธฐ ์์ค์ ๋ณด์ฌ ์ฃผ๋ ๋์๋ณด๋๋ฅผ ๋ง๋ค๊ณ ์์ธก๋ง๋ค ๊ทผ๊ฑฐ๋ฅผ ์ค๋ช
ํ๋ ์ค๋ช
๊ฐ๋ฅ์ฑ(XAI) ๊ธฐ๋ฅ์ ๋ถ์ธ๋ค.** ๊ณผ์
์ ์์ํ์ฌ ์คํ์ ๋ฐฉ์งํ๊ณ ์์ ๋์๋ ฅ์ ๊ทน๋ํํด์ผ ํฉ๋๋ค.\n",
"\n",
"์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ณธ ๋ฏธ์
์์๋ **์ถ๋ ฅ ์๊ฐ-๋น์ฉ, ์ถ๋ ฅ ๋ ๊ตฌ๋งค ๋น์ฉ, ์ ์ ๋ถํฌ, ์ ์-ํ๊ธฐ ์์ค, ์ฐ์ ํ๋ณด 5์ข
, ์ ์ ์ํฐํด๋ก ๋์๋ณด๋๋ฅผ ๊ตฌ์ฑ ์ํํ๊ณ ๊ฒ์ฆํ๋** ์ค์ต์ ์ฒด๊ณ์ ์ผ๋ก ์ํํฉ๋๋ค.\n",
"\n",
"ํ์ต์๋ ์ค์ต๊ฐ์ด๋์ ์ ์๋ ์ด 4๊ฐ ์ธ๋ถ ํ๋(๊ธฐ๋ณธ ๊ธฐ๋ฅ ๊ตฌํ โ ๊ฒฐ๊ณผ ์งํ ๊ฒ์ฆ โ ํ๋ผ๋ฏธํฐ ํ๋ ๋ฐ ๋์ ๊ณผ์ )์ ์์ฐจ์ ์ผ๋ก ์งํํ๋ฉฐ ํต์ฌ ํ์ ๊ธฐ์ค์ ๋ฌ์ฑํฉ๋๋ค.\n",
"\n",
"**์ด๋ฒ ๊ฒฐ๊ณผ๋ฌผ:** `src/explainability/mnt06_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": [
"\"\"\"๋ถํ๋ณ ์ถ๋ ฅ ์์ ์๊ฐยท์ ์ ๋น์ฉยท์ ๋ขฐ์ฑ ์ ์์ ํ๊ธฐ ์์ค์ ํ ํ๋ฉด์ ๋ณด์ฌ ์ฃผ๋ ๋์๋ณด๋๋ฅผ ๊ทธ๋ฆฌ๊ณ , ๊ตฐ ๊ท๊ฒฉ ๋ถํฉ ์ ์ ์์ธก๋ง๋ค ๊ธฐ์ฌ๋ ์์ 3๊ฐ ์ธ์๋ฅผ ๊ฐ๋ฆผ(occlusion) ๋ฐฉ์(SHAP ๊ทผ์ฌ)์ผ๋ก ์๋ ์ฐ์ถํด ์ํฐํด ๊ทธ๋ฆผ๊ณผ ์์ฐ์ด ์ค๋ช
์ผ๋ก ์ ๊ณตํ๋ ์ค๋ช
๊ฐ๋ฅ์ฑ(XAI) ๋ชจ๋์ ๊ตฌํํ๋ค.\"\"\"\n",
"import time\n",
"import numpy as np\n",
"import pandas as pd\n",
"import matplotlib.pyplot as plt\n",
"from sklearn.ensemble import GradientBoostingRegressor\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# [๊ต์ก์ฉ๊ฐ์์๋ฃ] ์ผ์ 3D ํ๋ฆฐํ
๋์ฒด ์ ์ ํ๋ณด ๋ถํ ์์ฑ๊ธฐ (์ค์ ๋ถํยท์ฌ๊ณ ยท๊ท๊ฒฉ ๊ฐ ์๋)\n",
"# ํ๊ธฐ์จ์ด ๋์ ์๋ฆฌ๋ถ์ ์ค 3D ํ๋ฆฐํ
์ผ๋ก ๋์ฒด ์ ์ํ ์ ์๋์ง ํ๋จํ๊ธฐ ์ํ ๊ฐ์ ๋ถํ ์๋ฃ๋ฅผ ๋ง๋ ๋ค.\n",
"# ์์ฌ ๋ฌผ์ฑยท์น์ยทํ์ค์ ๊ต์ก์ฉ์ผ๋ก ๋จ์ํํ ๊ฐ์ด๋ฉฐ ์ค์ ์ํ ์ฑ์ ์๋ ๊ตฐ ๊ท๊ฒฉ ๊ฐ๊ณผ ๋ฌด๊ดํ๋ค.\n",
"# ---------------------------------------------------------------------------\n",
"MAT = { # ์์ฌ: (๊ณต์ , ๊ธฐ์ค ์ธ์ฅ๊ฐ๋ MPa, ๋ฐ๋ g/cm3, ์์ฌ ๋จ๊ฐ ๋ง์/kg, ์ถ๋ ฅ๋น ๋ง์/h, ์ต์ ๋ฒฝ๋๊ป mm, ์ถ๋ ฅ ์๋ cm3/h)\n",
" \"PA12\": (\"polymer\", 45, 1.0, 8, 3, 0.8, 18), \"PETG\": (\"polymer\", 50, 1.27, 5, 2, 0.8, 15),\n",
" \"CF-PA\": (\"polymer\", 70, 1.2, 14, 3, 1.0, 12), \"AlSi10Mg\": (\"metal\", 350, 2.67, 30, 25, 0.5, 4),\n",
" \"SS316L\": (\"metal\", 520, 7.9, 28, 25, 0.6, 3.5), \"Ti64\": (\"metal\", 900, 4.4, 90, 30, 0.6, 3)}\n",
"CATS = [\"bracket\", \"housing\", \"clip\", \"gear\", \"duct\", \"handle\"]\n",
"CAT_SCRAP = {\"bracket\": 0.04, \"housing\": 0.05, \"clip\": 0.12, \"gear\": 0.09, \"duct\": 0.08, \"handle\": 0.10} # ๋ถํ๊ตฐ๋ณ ๊ธฐ๋ณธ ์ ํ๊ธฐ์จ\n",
"BUILD_MM = {\"polymer\": 300, \"metal\": 200} # ๊ฐ์ ์ถ๋ ฅ ๊ฐ๋ฅ ์ต๋ ์น์\n",
"\n",
"\n",
"def make_parts(n=300, seed=0):\n",
" \"\"\"๊ฐ์ ๋ถํ n์ข
: ํ์(๋ถํผยท๋ฒฝ๋๊ปยท๋์ถ ๋น์จ), ์์ฌ, ์ฌ์ฉ ํ์ค, ํ๊ธฐ์จ๊ณผ ์กฐ๋ฌ ์ ๋ณด๋ฅผ ๋ง๋ ๋ค.\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" mat = rng.choice(list(MAT), n, p=[0.2, 0.15, 0.1, 0.2, 0.2, 0.15])\n",
" proc = np.array([MAT[m][0] for m in mat])\n",
" vol = np.clip(rng.lognormal(np.log(30), 0.9, n), 2, 800)\n",
" aspect = rng.lognormal(0.3, 0.4, n)\n",
" max_dim = 10 * vol ** (1 / 3) * aspect ** (1 / 3) * rng.uniform(1.0, 1.6, n)\n",
" wall = np.clip(rng.lognormal(np.log(2.0), 0.6, n), 0.3, 8)\n",
" ovh = rng.beta(2, 5, n)\n",
" is_poly = proc == \"polymer\"\n",
" df = pd.DataFrame({\n",
" \"part_id\": [f\"P{i:03d}\" for i in range(n)], \"cat\": rng.choice(CATS, n, p=[0.2, 0.15, 0.2, 0.1, 0.15, 0.2]), \"material\": mat, \"process\": proc,\n",
" \"volume_cm3\": vol, \"max_dim_mm\": max_dim, \"area_cm2\": 6 * vol ** (2 / 3) * rng.uniform(1.0, 2.2, n), \"min_wall_mm\": wall, \"overhang\": ovh,\n",
" \"n_holes\": rng.poisson(2.0, n), \"thread\": rng.random(n) < 0.25, \"orient_deg\": rng.uniform(0, 90, n),\n",
" \"layer_mm\": np.where(is_poly, rng.choice([0.1, 0.2, 0.3], n), rng.choice([0.03, 0.05], n)),\n",
" \"infill\": np.where(is_poly, rng.choice([40, 60, 80, 100], n, p=[0.15, 0.3, 0.3, 0.25]), 100),\n",
" \"load_N\": np.clip(rng.lognormal(np.where(is_poly, np.log(230), np.log(2300)), 0.7, n), 10, 20000), \"temp_C\": rng.uniform(-20, 90, n),\n",
" \"crit\": rng.choice([1, 2, 3], n, p=[0.35, 0.4, 0.25]), \"cert\": rng.choice([0, 1], n, p=[0.45, 0.55]), # ์์ฌ ์ธ์ฆ์ ๋ณด์ \n",
" \"import_part\": rng.random(n) < 0.4})\n",
" df[\"scrap_rate\"] = np.clip(df[\"cat\"].map(CAT_SCRAP) * rng.lognormal(0, 0.35, n) * (1 + 0.5 * (df[\"load_N\"] > 800)) * (1 + 0.3 * (df[\"temp_C\"] > 60)), 0.005, 0.5)\n",
" df[\"lead_days\"] = np.clip(rng.lognormal(np.log(25), 0.5, n) * np.where(df[\"import_part\"], 2.2, 1.0), 5, 300)\n",
" # ---- ์จ์ ์ง์ค๊ฐ(์ค์ ์ํ ๊ฒฐ๊ณผ์ ํด๋น): ๋ชจ๋ธ์ ์ ์
๋ ฅ์ผ๋ก ์ด๊ฒ์ ์์ธกํ๋ค ----\n",
" eff = np.where(is_poly, 0.55 + 0.45 * df[\"infill\"] / 100, 1 - 0.3 * ovh * 0.2)\n",
" aniso = np.where(is_poly, 1 - 0.3 * np.sin(np.radians(df[\"orient_deg\"])) ** 2, 1 - 0.08 * np.sin(np.radians(df[\"orient_deg\"])))\n",
" heat = np.where(is_poly, 1 - 0.35 * np.clip((df[\"temp_C\"] - 40) / 80, 0, 1), 0.98)\n",
" base = np.array([MAT[m][1] for m in mat])\n",
" df[\"strength_mpa\"] = base * eff * aniso * heat * rng.lognormal(0, 0.06, n)\n",
" cross = df[\"min_wall_mm\"] * np.sqrt(df[\"max_dim_mm\"]) * 1.0 # ํ์ค ์ง์ง ๋จ๋ฉด์ mm2(๋จ์ํ)\n",
" df[\"stress_mpa\"] = df[\"load_N\"] / cross\n",
" df[\"safety\"] = df[\"strength_mpa\"] / df[\"stress_mpa\"]\n",
" ratio = np.clip(df[\"stress_mpa\"] / df[\"strength_mpa\"], 0, 1.5)\n",
" df[\"log_cycles\"] = np.clip(7.5 - 6.0 * ratio + rng.normal(0, 0.25, n), 2, 7.5) # ํผ๋ก ์๋ช
log10(ํ)\n",
" support = 1 + 0.5 * ovh\n",
" df[\"build_h\"] = vol * support / np.array([MAT[m][6] for m in mat]) + 0.5\n",
" mass_kg = vol * np.array([MAT[m][2] for m in mat]) * support / 1000\n",
" df[\"print_cost\"] = mass_kg * np.array([MAT[m][3] for m in mat]) + df[\"build_h\"] * np.array([MAT[m][4] for m in mat]) + 3\n",
" df[\"buy_cost\"] = df[\"print_cost\"] * rng.lognormal(0.25, 0.45, n) + 5 # ๊ตฌ๋งค ๋จ๊ฐ(๋ง์, ๊ฐ์)\n",
" wall_ok = df[\"min_wall_mm\"] >= np.array([MAT[m][5] for m in mat]) * rng.uniform(0.95, 1.25, n)\n",
" dim_ok = df[\"max_dim_mm\"] <= np.where(is_poly, BUILD_MM[\"polymer\"], BUILD_MM[\"metal\"])\n",
" warp = (~is_poly) & (df[\"max_dim_mm\"] > 140) & (df[\"overhang\"] > 0.3) # ๊ธ์ ๋ํ ์ค๋ฒํ ๋ณํ\n",
" thr_bad = df[\"thread\"] & (df[\"min_wall_mm\"] < 1.5) & is_poly # ์์ ๋์ฌ๋ถ ํ์\n",
" df[\"printable\"] = wall_ok & dim_ok & (ovh < 0.5) & ~warp & ~thr_bad\n",
" risk = np.clip(1 - df[\"safety\"] / 3, 0, 1)\n",
" df[\"conform\"] = np.clip(100 * (0.45 * np.minimum(df[\"safety\"] / 3, 1) + 0.25 * np.clip((df[\"log_cycles\"] - 3) / 4.5, 0, 1)\n",
" + 0.15 * (1 - np.clip(ovh * 1.2, 0, 1)) + 0.15 * df[\"cert\"]) - 8 * risk * df[\"crit\"] / 3 + rng.normal(0, 2, n), 0, 100)\n",
" df[\"demand_month\"] = rng.poisson(np.where(df[\"cat\"].isin([\"clip\", \"handle\"]), 40, 15)) + 1 # ์ ์์์๋(๊ฐ์)\n",
" df[\"scrap_cost\"] = df[\"scrap_rate\"] * df[\"demand_month\"] * df[\"buy_cost\"] # ์ ํ๊ธฐ ์์ค์ก(๋ง์, ๊ฐ์)\n",
" return df\n",
"\n",
"# ---- ๋จ์ํํ ํด์(FEA ๋์ฉ) ๊ฐ๊ณผ ํ์ ํผ์ฒ: ์
๋ ฅ ์ ๋ณด๋ง์ผ๋ก ๊ณ์ฐํ๋ค(์ํ ๊ฒฐ๊ณผ ๋์ค ์์) ----\n",
"def make_features(d):\n",
" \"\"\"๊ฐ์ด ์๋ ฅ ํด์(์๋ ฅ์ง์ค๊ณ์ ํฌํจ)๊ณผ ์์ฌยท๊ณต์ ์ง์์์ ๋ง๋ ํ์ ํผ์ฒ 16๊ฐ.\"\"\"\n",
" X = pd.DataFrame(index=d.index)\n",
" base = d[\"material\"].map(lambda m: MAT[m][1]).astype(float)\n",
" X[\"base_strength\"] = base\n",
" X[\"is_metal\"] = (d[\"process\"] == \"metal\").astype(int)\n",
" X[\"cross_mm2\"] = d[\"min_wall_mm\"] * np.sqrt(d[\"max_dim_mm\"])\n",
" X[\"kt\"] = 1 + 0.15 * d[\"n_holes\"] + 0.5 * d[\"thread\"].astype(int) # ๊ตฌ๋ฉยท๋์ฌ๋ถ ์๋ ฅ์ง์ค๊ณ์(๊ฐ์ด)\n",
" X[\"fea_stress\"] = d[\"load_N\"] / X[\"cross_mm2\"] * X[\"kt\"] # ๊ฐ์ด ํด์ ์๋ ฅ(MPa)\n",
" X[\"stress_ratio\"] = X[\"fea_stress\"] / base # ๊ธฐ์ค๊ฐ๋ ๋๋น ์๋ ฅ๋น\n",
" X[\"infill_eff\"] = np.where(X[\"is_metal\"] == 1, 1.0, 0.55 + 0.45 * d[\"infill\"] / 100)\n",
" X[\"orient_sin2\"] = np.sin(np.radians(d[\"orient_deg\"])) ** 2\n",
" X[\"temp_derate\"] = np.where(X[\"is_metal\"] == 1, 0.0, np.clip((d[\"temp_C\"] - 40) / 80, 0, 1))\n",
" X[\"overhang_layer\"] = d[\"overhang\"] * d[\"layer_mm\"]\n",
" X[\"slender\"] = d[\"max_dim_mm\"] / d[\"min_wall_mm\"]\n",
" X[\"surf_vol\"] = d[\"area_cm2\"] / d[\"volume_cm3\"]\n",
" X[\"wall_ratio\"] = d[\"min_wall_mm\"] / d[\"material\"].map(lambda m: MAT[m][5])\n",
" X[\"load_per_vol\"] = d[\"load_N\"] / d[\"volume_cm3\"]\n",
" X[\"cert\"] = d[\"cert\"].astype(int)\n",
" X[\"crit\"] = d[\"crit\"]\n",
" return X\n",
"\n",
"KO = {\"base_strength\": \"์์ฌ ๊ธฐ์ค๊ฐ๋\", \"is_metal\": \"๊ธ์ ๊ณต์ \", \"cross_mm2\": \"ํ์ค ์ง์ง ๋จ๋ฉด์ \", \"kt\": \"๊ตฌ๋ฉยท๋์ฌ๋ถ ์๋ ฅ์ง์ค\", \"fea_stress\": \"๊ฐ์ด ํด์ ์๋ ฅ\",\n",
" \"stress_ratio\": \"์๋ ฅ/๊ธฐ์ค๊ฐ๋ ๋น\", \"infill_eff\": \"์ถฉ์ ์จ\", \"orient_sin2\": \"์ ์ธต ๋ฐฉํฅ ์ํฅ\", \"temp_derate\": \"๊ณ ์จ ๊ฐ๋ ์ ํ\", \"overhang_layer\": \"๋์ถx์ธต๋๊ป\",\n",
" \"slender\": \"์ธ์ฅ๋น\", \"surf_vol\": \"ํ๋ฉด์ /๋ถํผ\", \"wall_ratio\": \"๋ฒฝ๋๊ป ์ฌ์ \", \"load_per_vol\": \"๋ถํผ๋น ํ์ค\", \"cert\": \"์์ฌ ์ธ์ฆ์\", \"crit\": \"์์ ์ค์๋\"}\n",
"\n",
"\n",
"def explain(model, X, bg, top=3):\n",
" \"\"\"๊ฐ๋ฆผ(occlusion) ๋ฐฉ์ ๊ตญ์ ๊ธฐ์ฌ๋: ํผ์ฒ j๋ฅผ ๋ฐฐ๊ฒฝ ํ๋ณธ ๊ฐ์ผ๋ก ๋ฐ๊ฟจ์ ๋ ์์ธก์ด ํ๊ท ์ ์ผ๋ก ์ผ๋ง๋ ๋ฌ๋ผ์ง๋์ง(SHAP ๊ทผ์ฌ).\"\"\"\n",
" base = model.predict(X)\n",
" contrib = np.zeros(X.shape)\n",
" for j, c in enumerate(X.columns):\n",
" for b in bg:\n",
" Xb = X.copy(); Xb[c] = b[j]\n",
" contrib[:, j] += base - model.predict(Xb)\n",
" contrib /= len(bg)\n",
" C = pd.DataFrame(contrib, columns=X.columns, index=X.index)\n",
" return base, C, [list(C.loc[i].abs().sort_values(ascending=False).index[:top]) for i in C.index]\n",
"\n",
"\n",
"hist, parts = make_parts(1000, seed=11), make_parts(300, seed=21)\n",
"Xh, X = make_features(hist), make_features(parts)\n",
"model = GradientBoostingRegressor(n_estimators=150, max_depth=3, learning_rate=0.06, random_state=0).fit(Xh, hist[\"conform\"])\n",
"AVG = float(hist[\"conform\"].mean())\n",
"\n",
"# 1) ์ค๋ช
๊ฐ๋ฅ์ฑ: ์ ์ฒด ๋ถํ์ ๋ํด ์์ 3๊ฐ ์ธ์ ์๋ ์ฐ์ถ\n",
"t0 = time.perf_counter()\n",
"pred, C, tops = explain(model, X, Xh.sample(25, random_state=0).values)\n",
"sec = time.perf_counter() - t0\n",
"rate = np.mean([len(t) == 3 for t in tops])\n",
"print(f\"[๊ต์ก์ฉ๊ฐ์์๋ฃ] ๋ถํ {len(X)}์ข
์ค๋ช
์ฐ์ถ {sec:.1f}์ด, ์์ 3๊ฐ ์ธ์ ์๋ ์ฐ์ถ๋ฅ {rate:.0%} (๋ชฉํ 100%: {'๋ฌ์ฑ' if rate == 1 else '๋ฏธ๋ฌ'})\")\n",
"gap = np.abs(C.sum(axis=1) - (pred - model.predict(Xh).mean())).mean()\n",
"print(f\"๊ธฐ์ฌ๋ ํฉ๊ณผ (์์ธก - ํ๊ท ์์ธก)์ ํ๊ท ์ฐจ์ด {gap:.1f}์ -> ๊ทผ์ฌ ์ค๋ช
์ด๋ฏ๋ก ์ฐธ๊ณ ์งํ๋ก ์ด๋ค\")\n",
"freq = pd.Series([f for t in tops for f in t]).map(lambda f: KO.get(f, f)).value_counts().head(5)\n",
"print(\"์์ 3๊ฐ ์ธ์์ ์์ฃผ ๋ฑ์ฅํ ์์ธ:\", \", \".join(f\"{k} {v}๊ฑด\" for k, v in freq.items()))\n",
"\n",
"# 2) ๋์๋ณด๋์ฉ ํ: ์ถ๋ ฅ ์๊ฐยท๋น์ฉยท์ ๋ขฐ์ฑยทํ๊ธฐ ์์ค\n",
"parts[\"score\"] = np.clip(pred, 0, 100)\n",
"parts[\"save\"] = parts[\"buy_cost\"] - parts[\"print_cost\"] # ๋ถํ 1๊ฐ๋น ๊ตฌ๋งค ๋๋น ์ ๊ฐ(๋ง์)\n",
"cand = parts[(parts[\"score\"] >= 70) & parts[\"printable\"]].copy()\n",
"cand[\"priority\"] = cand[\"scrap_cost\"] * (cand[\"score\"] / 100)\n",
"print(f\"\\n[๋์๋ณด๋ ์์ฝ] ๋ถํ {len(parts)}์ข
์ค ์ ์ 70 ์ด์์ด๋ฉฐ ์ถ๋ ฅ ๊ฐ๋ฅ {len(cand)}์ข
, ์ ํ๊ธฐ ์์ค ํฉ๊ณ {cand['scrap_cost'].sum():,.0f}๋ง์(๊ฐ์)\")\n",
"top = cand.sort_values(\"priority\", ascending=False).head(5)\n",
"print(top[[\"part_id\", \"cat\", \"material\", \"build_h\", \"print_cost\", \"buy_cost\", \"score\", \"scrap_cost\"]].round(1).to_string(index=False))\n",
"\n",
"fig, ax = plt.subplots(2, 3, figsize=(15, 8))\n",
"col = {1: \"tab:gray\", 2: \"tab:orange\", 3: \"tab:red\"}\n",
"ax[0, 0].scatter(parts[\"build_h\"], parts[\"print_cost\"], c=parts[\"crit\"].map(col), s=10); ax[0, 0].set_xscale(\"log\"); ax[0, 0].set_yscale(\"log\")\n",
"ax[0, 0].set_title(\"Build time vs cost (red = critical)\"); ax[0, 0].set_xlabel(\"build hours\")\n",
"ax[0, 1].scatter(parts[\"print_cost\"], parts[\"buy_cost\"], s=8); ax[0, 1].plot([1, 5000], [1, 5000], \"r--\"); ax[0, 1].set_xscale(\"log\"); ax[0, 1].set_yscale(\"log\")\n",
"ax[0, 1].set_title(\"Print cost vs purchase cost\"); ax[0, 1].set_xlabel(\"print cost\")\n",
"ax[0, 2].hist(parts[\"score\"], bins=25); ax[0, 2].axvline(70, ls=\"--\", c=\"k\"); ax[0, 2].set_title(\"Conformity score distribution\")\n",
"ax[1, 0].scatter(parts[\"score\"], parts[\"scrap_cost\"], c=parts[\"printable\"].map({True: \"tab:blue\", False: \"tab:red\"}), s=8); ax[1, 0].set_yscale(\"log\")\n",
"ax[1, 0].set_title(\"Score vs scrap loss (blue = printable)\"); ax[1, 0].set_xlabel(\"score\")\n",
"ax[1, 1].barh(top[\"part_id\"], top[\"scrap_cost\"]); ax[1, 1].set_title(\"Top 5 candidates by scrap loss\")\n",
"i = int(parts[\"score\"].idxmin())\n",
"ci = C.loc[i].reindex(C.loc[i].abs().sort_values(ascending=False).index)[:6][::-1]\n",
"start = model.predict(Xh).mean()\n",
"ax[1, 2].barh(range(len(ci)), ci.values, left=start + np.r_[0, np.cumsum(ci.values)[:-1]], color=[\"tab:green\" if v > 0 else \"tab:red\" for v in ci.values])\n",
"ax[1, 2].set_yticks(range(len(ci)), ci.index); ax[1, 2].set_title(f\"Waterfall: {parts.loc[i, 'part_id']} (lowest score)\"); ax[1, 2].set_xlabel(\"score\")\n",
"plt.tight_layout(); plt.show()\n",
"print(\"๋์๋ณด๋ ๊ตฌ์ฑ์์ 6์ข
: ์๊ฐ-๋น์ฉ, ์ถ๋ ฅ-๊ตฌ๋งค ๋น์ฉ, ์ ์ ๋ถํฌ, ์ ์-ํ๊ธฐ์์ค, ์ฐ์ ํ๋ณด 5์ข
, ์ ์ ์ํฐํด\")\n",
"\n",
"# 3) ์์ฐ์ด ์ค๋ช
์์(์ ๋น๊ธฐ์ ๊ด ํ๋ฉด ํ๋จ)\n",
"def sentence(k):\n",
" parts_ = [f\"{KO.get(f, f)}({'+' if C.loc[k, f] > 0 else ''}{C.loc[k, f]:.1f}์ )\" for f in tops[k]]\n",
" return f\"{parts.loc[k, 'part_id']} {parts.loc[k, 'cat']}/{parts.loc[k, 'material']} ๋ถํฉ ์ ์ {pred[k]:.1f}์ (ํ๊ท ๋๋น {pred[k] - AVG:+.1f}์ ), ์ํฅ ์์ธ: \" + \", \".join(parts_)\n",
"\n",
"\n",
"print(\"\\n[์์ธก ์ค๋ช
์์]\")\n",
"for k in list(top.index[:3]) + [i]:\n",
" print(\" - \" + sentence(k))\n",
"print(\"\\n์ ์๊ฐ ๋ฎ์ ๋ถํ์ ์ธ์๋ณ ์กฐ์น(์ธ์ฆ์ ํ๋ณด, ์ถฉ์ ์จ ์ํฅ, ๋ฒฝ๋๊ป ๋ณด๊ฐ ๋ฑ)๋ฅผ ์ ๋น๊ธฐ์ ๊ด์ด ํ๋จํ๋ค. ์ค๋ช
์ ๋ชจ๋ธ ๊ทผ์ฌ์ด๋ฉฐ ์น์ธ ๊ฒฐ์ ์ ๋์ ํ์ง ์๋๋ค.\")\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"๐ก [์ฐธ๊ณ ] ๋ฏธ์
06 ์์ ์คํ ๊ฒฐ๊ณผ (Output)
\n",
"\n",
"```text\n",
"[๊ต์ก์ฉ๊ฐ์์๋ฃ] ๋ถํ 300์ข
์ค๋ช
์ฐ์ถ 2.4์ด, ์์ 3๊ฐ ์ธ์ ์๋ ์ฐ์ถ๋ฅ 100% (๋ชฉํ 100%: ๋ฌ์ฑ)\n",
"๊ธฐ์ฌ๋ ํฉ๊ณผ (์์ธก - ํ๊ท ์์ธก)์ ํ๊ท ์ฐจ์ด 6.7์ -> ๊ทผ์ฌ ์ค๋ช
์ด๋ฏ๋ก ์ฐธ๊ณ ์งํ๋ก ์ด๋ค\n",
"์์ 3๊ฐ ์ธ์์ ์์ฃผ ๋ฑ์ฅํ ์์ธ: ์์ฌ ์ธ์ฆ์ 286๊ฑด, ์๋ ฅ/๊ธฐ์ค๊ฐ๋ ๋น 282๊ฑด, ๊ตฌ๋ฉยท๋์ฌ๋ถ ์๋ ฅ์ง์ค 137๊ฑด, ๋์ถx์ธต๋๊ป 116๊ฑด, ์ ์ธต ๋ฐฉํฅ ์ํฅ 31๊ฑด\n",
"\n",
"[๋์๋ณด๋ ์์ฝ] ๋ถํ 300์ข
์ค ์ ์ 70 ์ด์์ด๋ฉฐ ์ถ๋ ฅ ๊ฐ๋ฅ 106์ข
, ์ ํ๊ธฐ ์์ค ํฉ๊ณ 222,932๋ง์(๊ฐ์)\n",
"part_id cat material build_h print_cost buy_cost score scrap_cost\n",
" P121 handle Ti64 93.7 2925.4 7443.9 71.2 45271.3\n",
" P117 handle AlSi10Mg 23.6 600.5 1218.9 83.5 17632.8\n",
" P159 handle Ti64 15.7 491.7 878.2 81.6 17124.9\n",
" P293 handle SS316L 57.4 1483.2 1960.5 92.0 15131.3\n",
" P090 handle Ti64 40.6 1268.2 3157.5 90.5 12245.6\n",
"๋์๋ณด๋ ๊ตฌ์ฑ์์ 6์ข
: ์๊ฐ-๋น์ฉ, ์ถ๋ ฅ-๊ตฌ๋งค ๋น์ฉ, ์ ์ ๋ถํฌ, ์ ์-ํ๊ธฐ์์ค, ์ฐ์ ํ๋ณด 5์ข
, ์ ์ ์ํฐํด\n",
"\n",
"[์์ธก ์ค๋ช
์์]\n",
" - P121 handle/Ti64 ๋ถํฉ ์ ์ 71.2์ (ํ๊ท ๋๋น +13.2์ ), ์ํฅ ์์ธ: ์๋ ฅ/๊ธฐ์ค๊ฐ๋ ๋น(+10.0์ ), ์์ฌ ์ธ์ฆ์(-9.3์ ), ๊ตฌ๋ฉยท๋์ฌ๋ถ ์๋ ฅ์ง์ค(+4.2์ )\n",
" - P117 handle/AlSi10Mg ๋ถํฉ ์ ์ 83.5์ (ํ๊ท ๋๋น +25.5์ ), ์ํฅ ์์ธ: ์๋ ฅ/๊ธฐ์ค๊ฐ๋ ๋น(+10.1์ ), ์์ฌ ์ธ์ฆ์(+3.8์ ), ๊ตฌ๋ฉยท๋์ฌ๋ถ ์๋ ฅ์ง์ค(+3.1์ )\n",
" - P159 handle/Ti64 ๋ถํฉ ์ ์ 81.6์ (ํ๊ท ๋๋น +23.6์ ), ์ํฅ ์์ธ: ์๋ ฅ/๊ธฐ์ค๊ฐ๋ ๋น(+10.0์ ), ๋์ถx์ธต๋๊ป(+5.0์ ), ์์ฌ ์ธ์ฆ์(+4.2์ )\n",
" - P282 bracket/PA12 ๋ถํฉ ์ ์ 7.2์ (ํ๊ท ๋๋น -50.7์ ), ์ํฅ ์์ธ: ์๋ ฅ/๊ธฐ์ค๊ฐ๋ ๋น(-39.5์ ), ์์ฌ ์ธ์ฆ์(-12.9์ ), ์ ์ธต ๋ฐฉํฅ ์ํฅ(-1.9์ )\n",
"\n",
"์ ์๊ฐ ๋ฎ์ ๋ถํ์ ์ธ์๋ณ ์กฐ์น(์ธ์ฆ์ ํ๋ณด, ์ถฉ์ ์จ ์ํฅ, ๋ฒฝ๋๊ป ๋ณด๊ฐ ๋ฑ)๋ฅผ ์ ๋น๊ธฐ์ ๊ด์ด ํ๋จํ๋ค. ์ค๋ช
์ ๋ชจ๋ธ ๊ทผ์ฌ์ด๋ฉฐ ์น์ธ ๊ฒฐ์ ์ ๋์ ํ์ง ์๋๋ค.\n",
"```\n",
" \n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## ๐ฏ ๋ฏธ์
07. ์ผ์ 3D ํ๋ฆฐํ
๋ถํ ์ค ์ฅ์ฐฉ ์น์ธ์ ์ํ ์ ๋น๊ธฐ์ ๊ด HITL ๊ฒํ ์ ์ฐจ ์ค๊ณ\n",
"**์์์๊ฐ**: ์ฝ 3.5์๊ฐ\n",
"\n",
"### ๐ ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ์ํ ์ง์นจ\n",
"#### ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ๋น์์ฑ\n",
"\n",
"์ผ์ ๋ถํ ์ค ์ฅ์ฐฉ ์น์ธ์ ์ํ ์ ๋น๊ธฐ์ ๊ด HITL ๊ฒํ ์ ์ฐจ๋ฅผ ์ค๊ณํ๋ค. ์ค์ ์ ์ ํ์ฅ์์๋ ๋จ์ ํ์ง์ ๊ทธ์น์ง ์๊ณ ์ ์ํ ์ํ ํ๋จ๊ณผ ์ ๋ขฐ๋ ๋์ ์์ฌ๊ฒฐ์ ์ด ํ์์ ์ด๋ฏ๋ก, **์ผ์ 3D ํ๋ฆฐํ
๋ถํ์ ์ค ์ฅ์ฐฉ ์น์ธ์ ์ํด ๋ชจ๋ธ ์ ์์ด ์ ๋น๊ธฐ์ ๊ด ๊ฒํ ๋ฅผ ๊ฑฐ์ณ์ผ๋ง ์ ์ฉ๋๋ HITL ์น์ธ ์ ์ฐจ๋ฅผ ์๋ฎฌ๋ ์ดํฐ๋ก ์ค๊ณํ๋ค.** ๊ณผ์
์ ์์ํ์ฌ ์คํ์ ๋ฐฉ์งํ๊ณ ์์ ๋์๋ ฅ์ ๊ทน๋ํํด์ผ ํฉ๋๋ค.\n",
"\n",
"์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ณธ ๋ฏธ์
์์๋ **์๊ฐ ์์ฒญ๋ง๋ค ์ ์์ ํ์ ๊ท์น์ผ๋ก ๋์ฒด ์ ์์ ์ ์ํด ์น์ธ ๋๊ธฐ์ด์ ์ฌ๋ฆฌ๊ณ , ๋ชจ์ ์ ๋น๊ธฐ์ ๊ด์ ์ธ์ฆ์ยท์ ์ยท์ฅ๋น ์ ์ ๋ฅผ ๊ทผ๊ฑฐ๋ก ์น์ธยท๋ฐ๋ คํ๋ฉฐ ๋ฌด์๋ต์ด๋ฉด ๊ธฐ์กด ์กฐ๋ฌ์ ์ ์ง ์ํํ๊ณ ๊ฒ์ฆํ๋** ์ค์ต์ ์ฒด๊ณ์ ์ผ๋ก ์ํํฉ๋๋ค.\n",
"\n",
"ํ์ต์๋ ์ค์ต๊ฐ์ด๋์ ์ ์๋ ์ด 4๊ฐ ์ธ๋ถ ํ๋(๊ธฐ๋ณธ ๊ธฐ๋ฅ ๊ตฌํ โ ๊ฒฐ๊ณผ ์งํ ๊ฒ์ฆ โ ํ๋ผ๋ฏธํฐ ํ๋ ๋ฐ ๋์ ๊ณผ์ )์ ์์ฐจ์ ์ผ๋ก ์งํํ๋ฉฐ ํต์ฌ ํ์ ๊ธฐ์ค์ ๋ฌ์ฑํฉ๋๋ค.\n",
"\n",
"**์ด๋ฒ ๊ฒฐ๊ณผ๋ฌผ:** `src/optimization/mnt06_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": [
"\"\"\"์ผ์ 3D ํ๋ฆฐํ
๋ถํ์ ์ค ์ฅ์ฐฉ ์น์ธ์ ์ํด, ๋ชจ๋ธ์ด ๋์ฒด ์ ์์ ์ ์ํ๋ฉด ์ ๋น๊ธฐ์ ๊ด ์น์ธ ๋๊ธฐ์ด์ ์ฌ๋ฆฌ๊ณ ์น์ธยท๋ฐ๋ คยท๋ฌด์๋ต(๊ตฌ๋งค ์ ์ง)์ ์ฌ์ ์ ํจ๊ป ํด์ ์ฒด์ธ ๊ฐ์ฌ ๋ก๊ทธ์ ๋จ๊ธฐ๋ HITL ์๋ฎฌ๋ ์ดํฐ๋ฅผ ๊ตฌํํ๋ค. ๊ตฌ๋งค ์ ์ง ๋๋น ๋น์ฉ ์ ๊ฐ๋ฅ ๊ณผ ์น์ธ ์ง์ฐยท์์ฒญ ๊ธ์ฆ์ ๋ํ ๋ฏผ๊ฐ๋, ์๋ ์น์ธ ์์ ์ํ์ ๋น๊ตํ๋ค.\"\"\"\n",
"import hashlib\n",
"import json\n",
"import numpy as np\n",
"import pandas as pd\n",
"import matplotlib.pyplot as plt\n",
"from sklearn.ensemble import GradientBoostingRegressor\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# [๊ต์ก์ฉ๊ฐ์์๋ฃ] ์ผ์ 3D ํ๋ฆฐํ
๋์ฒด ์ ์ ํ๋ณด ๋ถํ ์์ฑ๊ธฐ (์ค์ ๋ถํยท์ฌ๊ณ ยท๊ท๊ฒฉ ๊ฐ ์๋)\n",
"# ํ๊ธฐ์จ์ด ๋์ ์๋ฆฌ๋ถ์ ์ค 3D ํ๋ฆฐํ
์ผ๋ก ๋์ฒด ์ ์ํ ์ ์๋์ง ํ๋จํ๊ธฐ ์ํ ๊ฐ์ ๋ถํ ์๋ฃ๋ฅผ ๋ง๋ ๋ค.\n",
"# ์์ฌ ๋ฌผ์ฑยท์น์ยทํ์ค์ ๊ต์ก์ฉ์ผ๋ก ๋จ์ํํ ๊ฐ์ด๋ฉฐ ์ค์ ์ํ ์ฑ์ ์๋ ๊ตฐ ๊ท๊ฒฉ ๊ฐ๊ณผ ๋ฌด๊ดํ๋ค.\n",
"# ---------------------------------------------------------------------------\n",
"MAT = { # ์์ฌ: (๊ณต์ , ๊ธฐ์ค ์ธ์ฅ๊ฐ๋ MPa, ๋ฐ๋ g/cm3, ์์ฌ ๋จ๊ฐ ๋ง์/kg, ์ถ๋ ฅ๋น ๋ง์/h, ์ต์ ๋ฒฝ๋๊ป mm, ์ถ๋ ฅ ์๋ cm3/h)\n",
" \"PA12\": (\"polymer\", 45, 1.0, 8, 3, 0.8, 18), \"PETG\": (\"polymer\", 50, 1.27, 5, 2, 0.8, 15),\n",
" \"CF-PA\": (\"polymer\", 70, 1.2, 14, 3, 1.0, 12), \"AlSi10Mg\": (\"metal\", 350, 2.67, 30, 25, 0.5, 4),\n",
" \"SS316L\": (\"metal\", 520, 7.9, 28, 25, 0.6, 3.5), \"Ti64\": (\"metal\", 900, 4.4, 90, 30, 0.6, 3)}\n",
"CATS = [\"bracket\", \"housing\", \"clip\", \"gear\", \"duct\", \"handle\"]\n",
"CAT_SCRAP = {\"bracket\": 0.04, \"housing\": 0.05, \"clip\": 0.12, \"gear\": 0.09, \"duct\": 0.08, \"handle\": 0.10} # ๋ถํ๊ตฐ๋ณ ๊ธฐ๋ณธ ์ ํ๊ธฐ์จ\n",
"BUILD_MM = {\"polymer\": 300, \"metal\": 200} # ๊ฐ์ ์ถ๋ ฅ ๊ฐ๋ฅ ์ต๋ ์น์\n",
"\n",
"\n",
"def make_parts(n=300, seed=0):\n",
" \"\"\"๊ฐ์ ๋ถํ n์ข
: ํ์(๋ถํผยท๋ฒฝ๋๊ปยท๋์ถ ๋น์จ), ์์ฌ, ์ฌ์ฉ ํ์ค, ํ๊ธฐ์จ๊ณผ ์กฐ๋ฌ ์ ๋ณด๋ฅผ ๋ง๋ ๋ค.\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" mat = rng.choice(list(MAT), n, p=[0.2, 0.15, 0.1, 0.2, 0.2, 0.15])\n",
" proc = np.array([MAT[m][0] for m in mat])\n",
" vol = np.clip(rng.lognormal(np.log(30), 0.9, n), 2, 800)\n",
" aspect = rng.lognormal(0.3, 0.4, n)\n",
" max_dim = 10 * vol ** (1 / 3) * aspect ** (1 / 3) * rng.uniform(1.0, 1.6, n)\n",
" wall = np.clip(rng.lognormal(np.log(2.0), 0.6, n), 0.3, 8)\n",
" ovh = rng.beta(2, 5, n)\n",
" is_poly = proc == \"polymer\"\n",
" df = pd.DataFrame({\n",
" \"part_id\": [f\"P{i:03d}\" for i in range(n)], \"cat\": rng.choice(CATS, n, p=[0.2, 0.15, 0.2, 0.1, 0.15, 0.2]), \"material\": mat, \"process\": proc,\n",
" \"volume_cm3\": vol, \"max_dim_mm\": max_dim, \"area_cm2\": 6 * vol ** (2 / 3) * rng.uniform(1.0, 2.2, n), \"min_wall_mm\": wall, \"overhang\": ovh,\n",
" \"n_holes\": rng.poisson(2.0, n), \"thread\": rng.random(n) < 0.25, \"orient_deg\": rng.uniform(0, 90, n),\n",
" \"layer_mm\": np.where(is_poly, rng.choice([0.1, 0.2, 0.3], n), rng.choice([0.03, 0.05], n)),\n",
" \"infill\": np.where(is_poly, rng.choice([40, 60, 80, 100], n, p=[0.15, 0.3, 0.3, 0.25]), 100),\n",
" \"load_N\": np.clip(rng.lognormal(np.where(is_poly, np.log(230), np.log(2300)), 0.7, n), 10, 20000), \"temp_C\": rng.uniform(-20, 90, n),\n",
" \"crit\": rng.choice([1, 2, 3], n, p=[0.35, 0.4, 0.25]), \"cert\": rng.choice([0, 1], n, p=[0.45, 0.55]), # ์์ฌ ์ธ์ฆ์ ๋ณด์ \n",
" \"import_part\": rng.random(n) < 0.4})\n",
" df[\"scrap_rate\"] = np.clip(df[\"cat\"].map(CAT_SCRAP) * rng.lognormal(0, 0.35, n) * (1 + 0.5 * (df[\"load_N\"] > 800)) * (1 + 0.3 * (df[\"temp_C\"] > 60)), 0.005, 0.5)\n",
" df[\"lead_days\"] = np.clip(rng.lognormal(np.log(25), 0.5, n) * np.where(df[\"import_part\"], 2.2, 1.0), 5, 300)\n",
" # ---- ์จ์ ์ง์ค๊ฐ(์ค์ ์ํ ๊ฒฐ๊ณผ์ ํด๋น): ๋ชจ๋ธ์ ์ ์
๋ ฅ์ผ๋ก ์ด๊ฒ์ ์์ธกํ๋ค ----\n",
" eff = np.where(is_poly, 0.55 + 0.45 * df[\"infill\"] / 100, 1 - 0.3 * ovh * 0.2)\n",
" aniso = np.where(is_poly, 1 - 0.3 * np.sin(np.radians(df[\"orient_deg\"])) ** 2, 1 - 0.08 * np.sin(np.radians(df[\"orient_deg\"])))\n",
" heat = np.where(is_poly, 1 - 0.35 * np.clip((df[\"temp_C\"] - 40) / 80, 0, 1), 0.98)\n",
" base = np.array([MAT[m][1] for m in mat])\n",
" df[\"strength_mpa\"] = base * eff * aniso * heat * rng.lognormal(0, 0.06, n)\n",
" cross = df[\"min_wall_mm\"] * np.sqrt(df[\"max_dim_mm\"]) * 1.0 # ํ์ค ์ง์ง ๋จ๋ฉด์ mm2(๋จ์ํ)\n",
" df[\"stress_mpa\"] = df[\"load_N\"] / cross\n",
" df[\"safety\"] = df[\"strength_mpa\"] / df[\"stress_mpa\"]\n",
" ratio = np.clip(df[\"stress_mpa\"] / df[\"strength_mpa\"], 0, 1.5)\n",
" df[\"log_cycles\"] = np.clip(7.5 - 6.0 * ratio + rng.normal(0, 0.25, n), 2, 7.5) # ํผ๋ก ์๋ช
log10(ํ)\n",
" support = 1 + 0.5 * ovh\n",
" df[\"build_h\"] = vol * support / np.array([MAT[m][6] for m in mat]) + 0.5\n",
" mass_kg = vol * np.array([MAT[m][2] for m in mat]) * support / 1000\n",
" df[\"print_cost\"] = mass_kg * np.array([MAT[m][3] for m in mat]) + df[\"build_h\"] * np.array([MAT[m][4] for m in mat]) + 3\n",
" df[\"buy_cost\"] = df[\"print_cost\"] * rng.lognormal(0.25, 0.45, n) + 5 # ๊ตฌ๋งค ๋จ๊ฐ(๋ง์, ๊ฐ์)\n",
" wall_ok = df[\"min_wall_mm\"] >= np.array([MAT[m][5] for m in mat]) * rng.uniform(0.95, 1.25, n)\n",
" dim_ok = df[\"max_dim_mm\"] <= np.where(is_poly, BUILD_MM[\"polymer\"], BUILD_MM[\"metal\"])\n",
" warp = (~is_poly) & (df[\"max_dim_mm\"] > 140) & (df[\"overhang\"] > 0.3) # ๊ธ์ ๋ํ ์ค๋ฒํ ๋ณํ\n",
" thr_bad = df[\"thread\"] & (df[\"min_wall_mm\"] < 1.5) & is_poly # ์์ ๋์ฌ๋ถ ํ์\n",
" df[\"printable\"] = wall_ok & dim_ok & (ovh < 0.5) & ~warp & ~thr_bad\n",
" risk = np.clip(1 - df[\"safety\"] / 3, 0, 1)\n",
" df[\"conform\"] = np.clip(100 * (0.45 * np.minimum(df[\"safety\"] / 3, 1) + 0.25 * np.clip((df[\"log_cycles\"] - 3) / 4.5, 0, 1)\n",
" + 0.15 * (1 - np.clip(ovh * 1.2, 0, 1)) + 0.15 * df[\"cert\"]) - 8 * risk * df[\"crit\"] / 3 + rng.normal(0, 2, n), 0, 100)\n",
" df[\"demand_month\"] = rng.poisson(np.where(df[\"cat\"].isin([\"clip\", \"handle\"]), 40, 15)) + 1 # ์ ์์์๋(๊ฐ์)\n",
" df[\"scrap_cost\"] = df[\"scrap_rate\"] * df[\"demand_month\"] * df[\"buy_cost\"] # ์ ํ๊ธฐ ์์ค์ก(๋ง์, ๊ฐ์)\n",
" return df\n",
"\n",
"# ---- ๋จ์ํํ ํด์(FEA ๋์ฉ) ๊ฐ๊ณผ ํ์ ํผ์ฒ: ์
๋ ฅ ์ ๋ณด๋ง์ผ๋ก ๊ณ์ฐํ๋ค(์ํ ๊ฒฐ๊ณผ ๋์ค ์์) ----\n",
"def make_features(d):\n",
" \"\"\"๊ฐ์ด ์๋ ฅ ํด์(์๋ ฅ์ง์ค๊ณ์ ํฌํจ)๊ณผ ์์ฌยท๊ณต์ ์ง์์์ ๋ง๋ ํ์ ํผ์ฒ 16๊ฐ.\"\"\"\n",
" X = pd.DataFrame(index=d.index)\n",
" base = d[\"material\"].map(lambda m: MAT[m][1]).astype(float)\n",
" X[\"base_strength\"] = base\n",
" X[\"is_metal\"] = (d[\"process\"] == \"metal\").astype(int)\n",
" X[\"cross_mm2\"] = d[\"min_wall_mm\"] * np.sqrt(d[\"max_dim_mm\"])\n",
" X[\"kt\"] = 1 + 0.15 * d[\"n_holes\"] + 0.5 * d[\"thread\"].astype(int) # ๊ตฌ๋ฉยท๋์ฌ๋ถ ์๋ ฅ์ง์ค๊ณ์(๊ฐ์ด)\n",
" X[\"fea_stress\"] = d[\"load_N\"] / X[\"cross_mm2\"] * X[\"kt\"] # ๊ฐ์ด ํด์ ์๋ ฅ(MPa)\n",
" X[\"stress_ratio\"] = X[\"fea_stress\"] / base # ๊ธฐ์ค๊ฐ๋ ๋๋น ์๋ ฅ๋น\n",
" X[\"infill_eff\"] = np.where(X[\"is_metal\"] == 1, 1.0, 0.55 + 0.45 * d[\"infill\"] / 100)\n",
" X[\"orient_sin2\"] = np.sin(np.radians(d[\"orient_deg\"])) ** 2\n",
" X[\"temp_derate\"] = np.where(X[\"is_metal\"] == 1, 0.0, np.clip((d[\"temp_C\"] - 40) / 80, 0, 1))\n",
" X[\"overhang_layer\"] = d[\"overhang\"] * d[\"layer_mm\"]\n",
" X[\"slender\"] = d[\"max_dim_mm\"] / d[\"min_wall_mm\"]\n",
" X[\"surf_vol\"] = d[\"area_cm2\"] / d[\"volume_cm3\"]\n",
" X[\"wall_ratio\"] = d[\"min_wall_mm\"] / d[\"material\"].map(lambda m: MAT[m][5])\n",
" X[\"load_per_vol\"] = d[\"load_N\"] / d[\"volume_cm3\"]\n",
" X[\"cert\"] = d[\"cert\"].astype(int)\n",
" X[\"crit\"] = d[\"crit\"]\n",
" return X\n",
"\n",
"# ---- ๋์ฒด ์ ์ ์์ฒญ ๋น์ฉ ๋ชจ์(๊ฐ์ ๋จ๊ฐ): ๊ตฌ๋งค ๋๊ธฐ vs 3D ํ๋ฆฐํ
----\n",
"DOWN = {1: 0.5, 2: 2.0, 3: 5.0} # [๊ต์ก์ฉ๊ฐ์์๋ฃ] ๋ถํ ๋ถ์ฌ๋ก ์ธํ ์ฅ๋น ๊ฐ๋์ค๋จ ๋น์ฉ(๋ง์/์ผ), ์ค์๋๋ณ\n",
"FAIL = {1: 10.0, 2: 40.0, 3: 150.0} # [๊ต์ก์ฉ๊ฐ์์๋ฃ] ๋์ฒดํ ์ฅ์ฐฉ ํ ๋ถํฉ๊ฒฉ ์ ์ฌ์์
ยท์๋ฌด ์ํฅ ๋น์ฉ(๋ง์)\n",
"POST = 3.0 # ํ์ฒ๋ฆฌยท๊ฒ์ฌ ๋น์ฉ(๋ง์)\n",
"\n",
"\n",
"def make_requests_month(catalog, n=250, seed=0):\n",
" \"\"\"์๊ฐ ๋ถํ ์์ฒญ: ์ ์์์๋์ด ํฐ ๋ถํ์ด ๋ ์์ฃผ ์์ฒญ๋๋ค.\"\"\"\n",
" return catalog.sample(n, replace=True, weights=catalog[\"demand_month\"], random_state=seed).reset_index(drop=True)\n",
"\n",
"\n",
"def buy_cost(r):\n",
" return r[\"buy_cost\"] + r[\"lead_days\"] * DOWN[r[\"crit\"]]\n",
"\n",
"\n",
"def print_outcome(r, delay_d=1.0):\n",
" \"\"\"์ค์ ์ํ ๊ฒฐ๊ณผ(์จ์ ์ง์ค)์ ๋ฐ๋ฅธ ๋์ฒด ์ ์ ๋น์ฉ. ๋ถํฉ๊ฒฉ์ด๋ฉด ๊ตฌ๋งค๋ก ๋๋์๊ฐ๊ณ ๋ถํฉ๊ฒฉ ๋น์ฉ์ด ๋ ๋ค.\"\"\"\n",
" wait = (r[\"build_h\"] / 24 + delay_d + 2.0) * DOWN[r[\"crit\"]]\n",
" ok = bool(r[\"printable\"] and r[\"conform\"] >= 70)\n",
" c = r[\"print_cost\"] + POST + wait\n",
" return (c, True) if ok else (c + buy_cost(r) + FAIL[r[\"crit\"]], False)\n",
"\n",
"\n",
"class Audit:\n",
" def __init__(self):\n",
" self.log = []\n",
"\n",
" def add(self, **body):\n",
" prev = self.log[-1][\"hash\"] if self.log else \"GENESIS\"\n",
" body = {\"seq\": len(self.log), **body}\n",
" self.log.append({**body, \"prev\": prev, \"hash\": hashlib.sha256((prev + json.dumps(body, sort_keys=True, ensure_ascii=False)).encode()).hexdigest()})\n",
"\n",
" def verify(self):\n",
" prev = \"GENESIS\"\n",
" for e in self.log:\n",
" b = {k: v for k, v in e.items() if k not in (\"prev\", \"hash\")}\n",
" if e[\"prev\"] != prev or hashlib.sha256((prev + json.dumps(b, sort_keys=True, ensure_ascii=False)).encode()).hexdigest() != e[\"hash\"]:\n",
" return e[\"seq\"]\n",
" prev = e[\"hash\"]\n",
" return None\n",
"\n",
"\n",
"def officer(r, rng):\n",
" \"\"\"[๋ชจ์ ์ ๋น๊ธฐ์ ๊ด] ํต์ฌ ๋ถํ์ ์ธ์ฆ์ ๋ฏธ๋ณด์ ยท๋ฎ์ ์ ์ยท์ฅ์๊ฐ ์ ์ ๋ ๋ฐ๋ ค, 8%๋ ๊ธฐํ ๋ด ๋ฌด์๋ต(๊ตฌ๋งค ์ ์ง).\"\"\"\n",
" if rng.random() < 0.08:\n",
" return \"๋ฌด์๋ต\", \"๊ธฐํ ๋ด ์๋ต ์์ -> ๊ธฐ์กด ์กฐ๋ฌ ์ ์ง\"\n",
" if r[\"crit\"] == 3 and r[\"cert\"] == 0:\n",
" return \"๋ฐ๋ ค\", \"ํต์ฌ ๋ถํ ์์ฌ ์ธ์ฆ์ ์์\"\n",
" if r[\"crit\"] == 3 and r[\"score\"] < 80:\n",
" return \"๋ฐ๋ ค\", f\"ํต์ฌ ๋ถํ ์ ์ {r['score']:.0f} < 80\"\n",
" if r[\"build_h\"] > 80:\n",
" return \"๋ฐ๋ ค\", f\"์ถ๋ ฅ {r['build_h']:.0f}์๊ฐ์ผ๋ก ์ฅ๋น ์ ์ ๊ณผ๋ค\"\n",
" return \"์น์ธ\", f\"์ ์ {r['score']:.0f}, ๊ตฌ๋งค ๋๋น ์ด๋ ํ์ธ\"\n",
"\n",
"\n",
"def run(req, mode, delay=1.0, seed=0, audit=None):\n",
" \"\"\"mode: buy(๊ธฐ์กด ์กฐ๋ฌ) / hitl(์ ์ -> ์น์ธ ์ ์ฅ์ฐฉ) / auto(์ ์ ์ ๋ถ ์ ์ฉ: ๋น๊ต์ฉ ์ํ, ์ค์ ์ด์ฉ ๊ธ์ง)\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" rows = []\n",
" for k, r in req.iterrows():\n",
" c_buy, how, fail = buy_cost(r), \"๊ตฌ๋งค\", False\n",
" proposed = mode != \"buy\" and r[\"score\"] >= 70 and r[\"rule_ok\"]\n",
" if proposed:\n",
" if mode == \"auto\":\n",
" dec = \"์น์ธ\"\n",
" else:\n",
" dec, why = officer(r, rng)\n",
" audit.add(event=\"PROPOSE\", part=r[\"part_id\"], score=round(float(r[\"score\"]), 1), status=\"PENDING\")\n",
" audit.add(event=\"DECIDE\", part=r[\"part_id\"], decision=dec, reason=why, by=\"์ ๋น๊ธฐ์ ๊ด(๋ชจ์)\")\n",
" if dec == \"์น์ธ\":\n",
" c, ok = print_outcome(r, delay)\n",
" how, fail = \"3D ์ ์\", not ok\n",
" c_buy = c\n",
" rows.append({\"part_id\": r[\"part_id\"], \"crit\": r[\"crit\"], \"proposed\": proposed, \"how\": how, \"fail\": fail, \"cost\": c_buy,\n",
" \"dec\": dec if proposed else \"-\", \"scrap_cost\": r[\"scrap_cost\"]})\n",
" return pd.DataFrame(rows)\n",
"\n",
"\n",
"hist = make_parts(1000, seed=11)\n",
"model = GradientBoostingRegressor(n_estimators=150, max_depth=3, learning_rate=0.06, random_state=0).fit(make_features(hist), hist[\"conform\"])\n",
"cat = make_parts(300, seed=21)\n",
"cat[\"score\"] = np.clip(model.predict(make_features(cat)), 0, 100)\n",
"cat[\"rule_ok\"] = (cat[\"min_wall_mm\"] >= 1.0) & (cat[\"max_dim_mm\"] <= cat[\"process\"].map(BUILD_MM)) & (cat[\"overhang\"] < 0.5) # ํ์ Baseline(๋ฏธ์
3)\n",
"req = make_requests_month(cat, 250, seed=3)\n",
"\n",
"audit = Audit()\n",
"R = {m: run(req, m, seed=5, audit=audit) for m in (\"buy\", \"hitl\", \"auto\")}\n",
"T = pd.DataFrame([{\"๋ฐฉ์\": n, \"์ด๋น์ฉ(๋ง์)\": round(R[m][\"cost\"].sum()), \"3D ์ ์ ๊ฑด์\": int((R[m][\"how\"] == \"3D ์ ์\").sum()), \"์ฅ์ฐฉ ํ ๋ถํฉ๊ฒฉ\": int(R[m][\"fail\"].sum()),\n",
" \"๋ถํฉ๊ฒฉ ๋น์ฉ(๋ง์)\": int(sum(FAIL[c] for c in R[m].loc[R[m][\"fail\"], \"crit\"]))}\n",
" for m, n in [(\"buy\", \"๊ธฐ์กด ์กฐ๋ฌ(๊ตฌ๋งค)\"), (\"hitl\", \"์ ์ + ์ ๋น๊ธฐ์ ๊ด ์น์ธ\"), (\"auto\", \"์ ์ ์ ๋ถ ์ ์ฉ(๋น๊ต์ฉ)\")] for n in [n]])\n",
"T.insert(0, \"๋ฐฉ์\", T.pop(\"๋ฐฉ์\"))\n",
"print(\"[๊ต์ก์ฉ๊ฐ์์๋ฃ] ์๊ฐ ๋์ฒด ์ ์ ์์ฒญ 250๊ฑด ๋ชจ์\")\n",
"print(T.to_string(index=False))\n",
"cut = 1 - T.loc[1, \"์ด๋น์ฉ(๋ง์)\"] / T.loc[0, \"์ด๋น์ฉ(๋ง์)\"]\n",
"print(f\"HITL ์ ์ฉ ์ด๋น์ฉ ์ ๊ฐ๋ฅ {cut:.1%} (๋ชฉํ 15% ์ด์: {'๋ฌ์ฑ' if cut >= 0.15 else '๋ฏธ๋ฌ'})\")\n",
"print(f\"์ ์ ์ ๋ถ ์ ์ฉ์ ์ ๊ฐ๋ฅ {1 - T.loc[2, '์ด๋น์ฉ(๋ง์)'] / T.loc[0, '์ด๋น์ฉ(๋ง์)']:.1%}๋ก ๋ ๋์ง๋ง ์ ๋น๊ธฐ์ ๊ด ํ๋จ ์์ด ์ฅ์ฐฉํ๋ ๋ฐฉ์์ด๋ผ ์ค์ ์ด์ฉ์์๋ ๊ธ์งํ๋ค(๋น๊ต์ฉ ์ํ)\")\n",
"rj = R[\"hitl\"][R[\"hitl\"][\"dec\"] == \"๋ฐ๋ ค\"]\n",
"would_fail = sum(not print_outcome(req.loc[i])[1] for i in rj.index)\n",
"print(f\"๋ฐ๋ ค {len(rj)}๊ฑด ์ค ์ค์ ๋ก ์ฅ์ฐฉํ๋ค๋ฉด ๋ถํฉ๊ฒฉ์ด์์ ๋ถํ {would_fail}๊ฑด -> \"\n",
" + (\"์น์ธ ๋จ๊ณ๊ฐ ๋ถํฉ๊ฒฉ ์ํ์ ๋ฏธ๋ฆฌ ๊ฑธ๋ฌ ๋๋ค\" if would_fail else \"์ด๋ฒ ๋ชจ์์์๋ ๋ฐ๋ ค๊ฐ ๋ชจ๋ ๋ณด์์ ํ๋จ์ด์๋ค(๊ธฐํ์์ค). ๋ฐ๋ ค ๊ธฐ์ค์ ๋ฏธ์
9 AAR์์ ์ฌ๊ฒํ ํ๋ค\"))\n",
"\n",
"dec = R[\"hitl\"][R[\"hitl\"][\"proposed\"]][\"dec\"].value_counts()\n",
"print(f\"\\n๋ณ๊ฒฝ ์ ์ {int(dec.sum())}๊ฑด: \" + \", \".join(f\"{k} {v}๊ฑด\" for k, v in dec.items()))\n",
"rej = [e[\"reason\"] for e in audit.log if e.get(\"decision\") == \"๋ฐ๋ ค\"]\n",
"print(\"๋ฐ๋ ค ์ฌ์ ์:\", \"; \".join([x for x in (next((x for x in rej if k in x), None) for k in (\"์ธ์ฆ์\", \"์ ์\", \"์ ์ \")) if x]))\n",
"n_dec = sum(e[\"event\"] == \"DECIDE\" for e in audit.log)\n",
"print(f\"์ ์ {int(dec.sum())}๊ฑด ์ค ๊ฒฐ์ ๊ธฐ๋ก {n_dec}๊ฑด(๋ก๊น
๋ฅ {n_dec / max(int(dec.sum()), 1):.0%}), ์๋ ์น์ธ 0๊ฑด, ๋ฌด์๋ต ์ ๊ธฐ์กด ์กฐ๋ฌ ์ ์ง\")\n",
"print(f\"๊ฐ์ฌ ๋ก๊ทธ ๋ฌด๊ฒฐ์ฑ: {'์ ์' if audit.verify() is None else '์ด์'}\", end=\", \")\n",
"audit.log[5][\"reason\"] = \"์ฌํ ์์ \"\n",
"print(f\"์ฌํ ๋ณ์กฐ ์ seq {audit.verify()}์์ ํ์ง\")\n",
"\n",
"print(\"\\n[๋ฏผ๊ฐ๋] ์น์ธ ์ง์ฐ(์ผ) -> ๋น์ฉ ์ ๊ฐ๋ฅ \")\n",
"for d in (0.0, 1.0, 3.0, 7.0, 14.0):\n",
" r_ = run(req, \"hitl\", delay=d, seed=5, audit=Audit())\n",
" print(f\" {d:>4}: {1 - r_['cost'].sum() / T.loc[0, '์ด๋น์ฉ(๋ง์)']:.1%}\")\n",
"surge = make_requests_month(cat, 400, seed=3)\n",
"b, h = run(surge, \"buy\"), run(surge, \"hitl\", seed=5, audit=Audit())\n",
"print(f\"์์ฒญ 1.6๋ฐฐ ๊ธ์ฆ(400๊ฑด) ์๋๋ฆฌ์ค: ๋น์ฉ ์ ๊ฐ๋ฅ {1 - h['cost'].sum() / b['cost'].sum():.1%}, ์น์ธ ๋๊ธฐ ๊ฑด์ {int(h['proposed'].sum())}๊ฑด -> ์ ๋น๊ธฐ์ ๊ด ๊ฒํ ์ธ๋ ฅ์ด ๋ณ๋ชฉ\")\n",
"\n",
"plt.figure(figsize=(7, 4))\n",
"plt.bar([\"buy\", \"HITL\", \"auto(compare)\"], T[\"์ด๋น์ฉ(๋ง์)\"], color=[\"tab:gray\", \"tab:blue\", \"tab:orange\"]); plt.ylabel(\"total cost (virtual)\")\n",
"plt.title(\"HITL simulator: monthly cost\"); plt.tight_layout(); plt.show()\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"๐ก [์ฐธ๊ณ ] ๋ฏธ์
07 ์์ ์คํ ๊ฒฐ๊ณผ (Output)
\n",
"\n",
"```text\n",
"[๊ต์ก์ฉ๊ฐ์์๋ฃ] ์๊ฐ ๋์ฒด ์ ์ ์์ฒญ 250๊ฑด ๋ชจ์\n",
" ๋ฐฉ์ ์ด๋น์ฉ(๋ง์) 3D ์ ์ ๊ฑด์ ์ฅ์ฐฉ ํ ๋ถํฉ๊ฒฉ ๋ถํฉ๊ฒฉ ๋น์ฉ(๋ง์)\n",
" ๊ธฐ์กด ์กฐ๋ฌ(๊ตฌ๋งค) 102267 0 0 0\n",
"์ ์ + ์ ๋น๊ธฐ์ ๊ด ์น์ธ 86487 72 1 10\n",
"์ ์ ์ ๋ถ ์ ์ฉ(๋น๊ต์ฉ) 80225 96 1 10\n",
"HITL ์ ์ฉ ์ด๋น์ฉ ์ ๊ฐ๋ฅ 15.4% (๋ชฉํ 15% ์ด์: ๋ฌ์ฑ)\n",
"์ ์ ์ ๋ถ ์ ์ฉ์ ์ ๊ฐ๋ฅ 21.6%๋ก ๋ ๋์ง๋ง ์ ๋น๊ธฐ์ ๊ด ํ๋จ ์์ด ์ฅ์ฐฉํ๋ ๋ฐฉ์์ด๋ผ ์ค์ ์ด์ฉ์์๋ ๊ธ์งํ๋ค(๋น๊ต์ฉ ์ํ)\n",
"๋ฐ๋ ค 15๊ฑด ์ค ์ค์ ๋ก ์ฅ์ฐฉํ๋ค๋ฉด ๋ถํฉ๊ฒฉ์ด์์ ๋ถํ 0๊ฑด -> ์ด๋ฒ ๋ชจ์์์๋ ๋ฐ๋ ค๊ฐ ๋ชจ๋ ๋ณด์์ ํ๋จ์ด์๋ค(๊ธฐํ์์ค). ๋ฐ๋ ค ๊ธฐ์ค์ ๋ฏธ์
9 AAR์์ ์ฌ๊ฒํ ํ๋ค\n",
"\n",
"๋ณ๊ฒฝ ์ ์ 96๊ฑด: ์น์ธ 72๊ฑด, ๋ฐ๋ ค 15๊ฑด, ๋ฌด์๋ต 9๊ฑด\n",
"๋ฐ๋ ค ์ฌ์ ์: ํต์ฌ ๋ถํ ์์ฌ ์ธ์ฆ์ ์์; ํต์ฌ ๋ถํ ์ ์ 79 < 80\n",
"์ ์ 96๊ฑด ์ค ๊ฒฐ์ ๊ธฐ๋ก 96๊ฑด(๋ก๊น
๋ฅ 100%), ์๋ ์น์ธ 0๊ฑด, ๋ฌด์๋ต ์ ๊ธฐ์กด ์กฐ๋ฌ ์ ์ง\n",
"๊ฐ์ฌ ๋ก๊ทธ ๋ฌด๊ฒฐ์ฑ: ์ ์, ์ฌํ ๋ณ์กฐ ์ seq 5์์ ํ์ง\n",
"\n",
"[๋ฏผ๊ฐ๋] ์น์ธ ์ง์ฐ(์ผ) -> ๋น์ฉ ์ ๊ฐ๋ฅ \n",
" 0.0: 15.6%\n",
" 1.0: 15.4%\n",
" 3.0: 15.1%\n",
" 7.0: 14.5%\n",
" 14.0: 13.5%\n",
"์์ฒญ 1.6๋ฐฐ ๊ธ์ฆ(400๊ฑด) ์๋๋ฆฌ์ค: ๋น์ฉ ์ ๊ฐ๋ฅ 13.1%, ์น์ธ ๋๊ธฐ ๊ฑด์ 141๊ฑด -> ์ ๋น๊ธฐ์ ๊ด ๊ฒํ ์ธ๋ ฅ์ด ๋ณ๋ชฉ\n",
"```\n",
" \n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## ๐ฏ ๋ฏธ์
08. ๋ณต์ก ํ์ ๋ถํ 50์ข
์ผ๊ด ์ ๋ขฐ์ฑ ํ๊ฐ ์๋ฎฌ๋ ์ด์
๋ถํ ํ
์คํธ\n",
"**์์์๊ฐ**: ์ฝ 3.0์๊ฐ\n",
"\n",
"### ๐ ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ์ํ ์ง์นจ\n",
"#### ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ๋น์์ฑ\n",
"\n",
"๋ณต์ก ํ์ ๋ถํ 50์ข
์ผ๊ด ์ ๋ขฐ์ฑ ํ๊ฐ ์๋ฎฌ๋ ์ด์
๋ถํ ํ
์คํธ๋ฅผ ์ํํ๋ค. ์ค์ ์ ์ ํ์ฅ์์๋ ๋จ์ ํ์ง์ ๊ทธ์น์ง ์๊ณ ์ ์ํ ์ํ ํ๋จ๊ณผ ์ ๋ขฐ๋ ๋์ ์์ฌ๊ฒฐ์ ์ด ํ์์ ์ด๋ฏ๋ก, **๋ณต์ก ํ์ ๋ถํ 50์ข
(์ต๋ 500์ข
)์ ์ผ๊ด ํ๊ฐํ๋ ์ํฉ์์ ์ ๋ขฐ์ฑ ํ๊ฐ์ ๋์๋ณด๋๊ฐ ์ ๋ ๋์ํ๋์ง ๋ถํ ํ
์คํธํ๋ค.** ๊ณผ์
์ ์์ํ์ฌ ์คํ์ ๋ฐฉ์งํ๊ณ ์์ ๋์๋ ฅ์ ๊ทน๋ํํด์ผ ํฉ๋๋ค.\n",
"\n",
"์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ณธ ๋ฏธ์
์์๋ **๋ณต์ก ํ์ ๋ถํ 50ยท100ยท200ยท500์ข
์ผ๊ด ์์ฒญ์ ์์ ๋ ์ฝ๋์ ๋ชจ๋ธ ์ฅ์ ๋ฅผ ์ฃผ์
ํ๊ณ ์ฒ๋ฆฌ์ ํ๋ฉด ์์ฑ ์๊ฐ, ์ ์ค ์ฌ๋ถ, ํํฐ ์กฐํ ์๋ต์ ์ธก์ ์ํํ๊ณ ๊ฒ์ฆํ๋** ์ค์ต์ ์ฒด๊ณ์ ์ผ๋ก ์ํํฉ๋๋ค.\n",
"\n",
"ํ์ต์๋ ์ค์ต๊ฐ์ด๋์ ์ ์๋ ์ด 4๊ฐ ์ธ๋ถ ํ๋(๊ธฐ๋ณธ ๊ธฐ๋ฅ ๊ตฌํ โ ๊ฒฐ๊ณผ ์งํ ๊ฒ์ฆ โ ํ๋ผ๋ฏธํฐ ํ๋ ๋ฐ ๋์ ๊ณผ์ )์ ์์ฐจ์ ์ผ๋ก ์งํํ๋ฉฐ ํต์ฌ ํ์ ๊ธฐ์ค์ ๋ฌ์ฑํฉ๋๋ค.\n",
"\n",
"**์ด๋ฒ ๊ฒฐ๊ณผ๋ฌผ:** `src/dashboard/mnt06_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": [
"\"\"\"๋ณต์ก ํ์ ๋ถํ 50์ข
(์ต๋ 500์ข
) ์ผ๊ด ์ ๋ขฐ์ฑ ํ๊ฐ๋ฅผ ์์ฒญํ๋ฉด์ ์์ ๋ ์ฝ๋ ์ฃผ์
๊ณผ ํ๊ฐ ๋ชจ๋ธ ์ฅ์ ๋ฅผ ์ผ์ผ์ผ, ์ผ๊ด ์ฒ๋ฆฌ+ํ๋ฉด ์์ฑ ์๊ฐ, '์ ๋ขฐ์ฑ ๋ฏธ๋ฌ ๊ฒฝ๋ณด' ์ ํ๋, ์์ฒญ ์ ์ค, ์์ ๋ฐ์ดํฐ์ ์๋ ํ์ธ ๋ถ๋ฆฌ, ํํฐ ์กฐํ ์๋ต, ๋ชจ๋ธ ์ฅ์ ์ ๊ฐ์ด ๊ท์น์ผ๋ก์ ๊ธฐ๋ฅ ์ถ์ ์ด์ฉ์ ์คํธ๋ ์ค ํ
์คํธํ๋ค.\"\"\"\n",
"import time\n",
"import numpy as np\n",
"import pandas as pd\n",
"import matplotlib.pyplot as plt\n",
"from sklearn.ensemble import GradientBoostingRegressor\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# [๊ต์ก์ฉ๊ฐ์์๋ฃ] ์ผ์ 3D ํ๋ฆฐํ
๋์ฒด ์ ์ ํ๋ณด ๋ถํ ์์ฑ๊ธฐ (์ค์ ๋ถํยท์ฌ๊ณ ยท๊ท๊ฒฉ ๊ฐ ์๋)\n",
"# ํ๊ธฐ์จ์ด ๋์ ์๋ฆฌ๋ถ์ ์ค 3D ํ๋ฆฐํ
์ผ๋ก ๋์ฒด ์ ์ํ ์ ์๋์ง ํ๋จํ๊ธฐ ์ํ ๊ฐ์ ๋ถํ ์๋ฃ๋ฅผ ๋ง๋ ๋ค.\n",
"# ์์ฌ ๋ฌผ์ฑยท์น์ยทํ์ค์ ๊ต์ก์ฉ์ผ๋ก ๋จ์ํํ ๊ฐ์ด๋ฉฐ ์ค์ ์ํ ์ฑ์ ์๋ ๊ตฐ ๊ท๊ฒฉ ๊ฐ๊ณผ ๋ฌด๊ดํ๋ค.\n",
"# ---------------------------------------------------------------------------\n",
"MAT = { # ์์ฌ: (๊ณต์ , ๊ธฐ์ค ์ธ์ฅ๊ฐ๋ MPa, ๋ฐ๋ g/cm3, ์์ฌ ๋จ๊ฐ ๋ง์/kg, ์ถ๋ ฅ๋น ๋ง์/h, ์ต์ ๋ฒฝ๋๊ป mm, ์ถ๋ ฅ ์๋ cm3/h)\n",
" \"PA12\": (\"polymer\", 45, 1.0, 8, 3, 0.8, 18), \"PETG\": (\"polymer\", 50, 1.27, 5, 2, 0.8, 15),\n",
" \"CF-PA\": (\"polymer\", 70, 1.2, 14, 3, 1.0, 12), \"AlSi10Mg\": (\"metal\", 350, 2.67, 30, 25, 0.5, 4),\n",
" \"SS316L\": (\"metal\", 520, 7.9, 28, 25, 0.6, 3.5), \"Ti64\": (\"metal\", 900, 4.4, 90, 30, 0.6, 3)}\n",
"CATS = [\"bracket\", \"housing\", \"clip\", \"gear\", \"duct\", \"handle\"]\n",
"CAT_SCRAP = {\"bracket\": 0.04, \"housing\": 0.05, \"clip\": 0.12, \"gear\": 0.09, \"duct\": 0.08, \"handle\": 0.10} # ๋ถํ๊ตฐ๋ณ ๊ธฐ๋ณธ ์ ํ๊ธฐ์จ\n",
"BUILD_MM = {\"polymer\": 300, \"metal\": 200} # ๊ฐ์ ์ถ๋ ฅ ๊ฐ๋ฅ ์ต๋ ์น์\n",
"\n",
"\n",
"def make_parts(n=300, seed=0):\n",
" \"\"\"๊ฐ์ ๋ถํ n์ข
: ํ์(๋ถํผยท๋ฒฝ๋๊ปยท๋์ถ ๋น์จ), ์์ฌ, ์ฌ์ฉ ํ์ค, ํ๊ธฐ์จ๊ณผ ์กฐ๋ฌ ์ ๋ณด๋ฅผ ๋ง๋ ๋ค.\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" mat = rng.choice(list(MAT), n, p=[0.2, 0.15, 0.1, 0.2, 0.2, 0.15])\n",
" proc = np.array([MAT[m][0] for m in mat])\n",
" vol = np.clip(rng.lognormal(np.log(30), 0.9, n), 2, 800)\n",
" aspect = rng.lognormal(0.3, 0.4, n)\n",
" max_dim = 10 * vol ** (1 / 3) * aspect ** (1 / 3) * rng.uniform(1.0, 1.6, n)\n",
" wall = np.clip(rng.lognormal(np.log(2.0), 0.6, n), 0.3, 8)\n",
" ovh = rng.beta(2, 5, n)\n",
" is_poly = proc == \"polymer\"\n",
" df = pd.DataFrame({\n",
" \"part_id\": [f\"P{i:03d}\" for i in range(n)], \"cat\": rng.choice(CATS, n, p=[0.2, 0.15, 0.2, 0.1, 0.15, 0.2]), \"material\": mat, \"process\": proc,\n",
" \"volume_cm3\": vol, \"max_dim_mm\": max_dim, \"area_cm2\": 6 * vol ** (2 / 3) * rng.uniform(1.0, 2.2, n), \"min_wall_mm\": wall, \"overhang\": ovh,\n",
" \"n_holes\": rng.poisson(2.0, n), \"thread\": rng.random(n) < 0.25, \"orient_deg\": rng.uniform(0, 90, n),\n",
" \"layer_mm\": np.where(is_poly, rng.choice([0.1, 0.2, 0.3], n), rng.choice([0.03, 0.05], n)),\n",
" \"infill\": np.where(is_poly, rng.choice([40, 60, 80, 100], n, p=[0.15, 0.3, 0.3, 0.25]), 100),\n",
" \"load_N\": np.clip(rng.lognormal(np.where(is_poly, np.log(230), np.log(2300)), 0.7, n), 10, 20000), \"temp_C\": rng.uniform(-20, 90, n),\n",
" \"crit\": rng.choice([1, 2, 3], n, p=[0.35, 0.4, 0.25]), \"cert\": rng.choice([0, 1], n, p=[0.45, 0.55]), # ์์ฌ ์ธ์ฆ์ ๋ณด์ \n",
" \"import_part\": rng.random(n) < 0.4})\n",
" df[\"scrap_rate\"] = np.clip(df[\"cat\"].map(CAT_SCRAP) * rng.lognormal(0, 0.35, n) * (1 + 0.5 * (df[\"load_N\"] > 800)) * (1 + 0.3 * (df[\"temp_C\"] > 60)), 0.005, 0.5)\n",
" df[\"lead_days\"] = np.clip(rng.lognormal(np.log(25), 0.5, n) * np.where(df[\"import_part\"], 2.2, 1.0), 5, 300)\n",
" # ---- ์จ์ ์ง์ค๊ฐ(์ค์ ์ํ ๊ฒฐ๊ณผ์ ํด๋น): ๋ชจ๋ธ์ ์ ์
๋ ฅ์ผ๋ก ์ด๊ฒ์ ์์ธกํ๋ค ----\n",
" eff = np.where(is_poly, 0.55 + 0.45 * df[\"infill\"] / 100, 1 - 0.3 * ovh * 0.2)\n",
" aniso = np.where(is_poly, 1 - 0.3 * np.sin(np.radians(df[\"orient_deg\"])) ** 2, 1 - 0.08 * np.sin(np.radians(df[\"orient_deg\"])))\n",
" heat = np.where(is_poly, 1 - 0.35 * np.clip((df[\"temp_C\"] - 40) / 80, 0, 1), 0.98)\n",
" base = np.array([MAT[m][1] for m in mat])\n",
" df[\"strength_mpa\"] = base * eff * aniso * heat * rng.lognormal(0, 0.06, n)\n",
" cross = df[\"min_wall_mm\"] * np.sqrt(df[\"max_dim_mm\"]) * 1.0 # ํ์ค ์ง์ง ๋จ๋ฉด์ mm2(๋จ์ํ)\n",
" df[\"stress_mpa\"] = df[\"load_N\"] / cross\n",
" df[\"safety\"] = df[\"strength_mpa\"] / df[\"stress_mpa\"]\n",
" ratio = np.clip(df[\"stress_mpa\"] / df[\"strength_mpa\"], 0, 1.5)\n",
" df[\"log_cycles\"] = np.clip(7.5 - 6.0 * ratio + rng.normal(0, 0.25, n), 2, 7.5) # ํผ๋ก ์๋ช
log10(ํ)\n",
" support = 1 + 0.5 * ovh\n",
" df[\"build_h\"] = vol * support / np.array([MAT[m][6] for m in mat]) + 0.5\n",
" mass_kg = vol * np.array([MAT[m][2] for m in mat]) * support / 1000\n",
" df[\"print_cost\"] = mass_kg * np.array([MAT[m][3] for m in mat]) + df[\"build_h\"] * np.array([MAT[m][4] for m in mat]) + 3\n",
" df[\"buy_cost\"] = df[\"print_cost\"] * rng.lognormal(0.25, 0.45, n) + 5 # ๊ตฌ๋งค ๋จ๊ฐ(๋ง์, ๊ฐ์)\n",
" wall_ok = df[\"min_wall_mm\"] >= np.array([MAT[m][5] for m in mat]) * rng.uniform(0.95, 1.25, n)\n",
" dim_ok = df[\"max_dim_mm\"] <= np.where(is_poly, BUILD_MM[\"polymer\"], BUILD_MM[\"metal\"])\n",
" warp = (~is_poly) & (df[\"max_dim_mm\"] > 140) & (df[\"overhang\"] > 0.3) # ๊ธ์ ๋ํ ์ค๋ฒํ ๋ณํ\n",
" thr_bad = df[\"thread\"] & (df[\"min_wall_mm\"] < 1.5) & is_poly # ์์ ๋์ฌ๋ถ ํ์\n",
" df[\"printable\"] = wall_ok & dim_ok & (ovh < 0.5) & ~warp & ~thr_bad\n",
" risk = np.clip(1 - df[\"safety\"] / 3, 0, 1)\n",
" df[\"conform\"] = np.clip(100 * (0.45 * np.minimum(df[\"safety\"] / 3, 1) + 0.25 * np.clip((df[\"log_cycles\"] - 3) / 4.5, 0, 1)\n",
" + 0.15 * (1 - np.clip(ovh * 1.2, 0, 1)) + 0.15 * df[\"cert\"]) - 8 * risk * df[\"crit\"] / 3 + rng.normal(0, 2, n), 0, 100)\n",
" df[\"demand_month\"] = rng.poisson(np.where(df[\"cat\"].isin([\"clip\", \"handle\"]), 40, 15)) + 1 # ์ ์์์๋(๊ฐ์)\n",
" df[\"scrap_cost\"] = df[\"scrap_rate\"] * df[\"demand_month\"] * df[\"buy_cost\"] # ์ ํ๊ธฐ ์์ค์ก(๋ง์, ๊ฐ์)\n",
" return df\n",
"\n",
"# ---- ๋จ์ํํ ํด์(FEA ๋์ฉ) ๊ฐ๊ณผ ํ์ ํผ์ฒ: ์
๋ ฅ ์ ๋ณด๋ง์ผ๋ก ๊ณ์ฐํ๋ค(์ํ ๊ฒฐ๊ณผ ๋์ค ์์) ----\n",
"def make_features(d):\n",
" \"\"\"๊ฐ์ด ์๋ ฅ ํด์(์๋ ฅ์ง์ค๊ณ์ ํฌํจ)๊ณผ ์์ฌยท๊ณต์ ์ง์์์ ๋ง๋ ํ์ ํผ์ฒ 16๊ฐ.\"\"\"\n",
" X = pd.DataFrame(index=d.index)\n",
" base = d[\"material\"].map(lambda m: MAT[m][1]).astype(float)\n",
" X[\"base_strength\"] = base\n",
" X[\"is_metal\"] = (d[\"process\"] == \"metal\").astype(int)\n",
" X[\"cross_mm2\"] = d[\"min_wall_mm\"] * np.sqrt(d[\"max_dim_mm\"])\n",
" X[\"kt\"] = 1 + 0.15 * d[\"n_holes\"] + 0.5 * d[\"thread\"].astype(int) # ๊ตฌ๋ฉยท๋์ฌ๋ถ ์๋ ฅ์ง์ค๊ณ์(๊ฐ์ด)\n",
" X[\"fea_stress\"] = d[\"load_N\"] / X[\"cross_mm2\"] * X[\"kt\"] # ๊ฐ์ด ํด์ ์๋ ฅ(MPa)\n",
" X[\"stress_ratio\"] = X[\"fea_stress\"] / base # ๊ธฐ์ค๊ฐ๋ ๋๋น ์๋ ฅ๋น\n",
" X[\"infill_eff\"] = np.where(X[\"is_metal\"] == 1, 1.0, 0.55 + 0.45 * d[\"infill\"] / 100)\n",
" X[\"orient_sin2\"] = np.sin(np.radians(d[\"orient_deg\"])) ** 2\n",
" X[\"temp_derate\"] = np.where(X[\"is_metal\"] == 1, 0.0, np.clip((d[\"temp_C\"] - 40) / 80, 0, 1))\n",
" X[\"overhang_layer\"] = d[\"overhang\"] * d[\"layer_mm\"]\n",
" X[\"slender\"] = d[\"max_dim_mm\"] / d[\"min_wall_mm\"]\n",
" X[\"surf_vol\"] = d[\"area_cm2\"] / d[\"volume_cm3\"]\n",
" X[\"wall_ratio\"] = d[\"min_wall_mm\"] / d[\"material\"].map(lambda m: MAT[m][5])\n",
" X[\"load_per_vol\"] = d[\"load_N\"] / d[\"volume_cm3\"]\n",
" X[\"cert\"] = d[\"cert\"].astype(int)\n",
" X[\"crit\"] = d[\"crit\"]\n",
" return X\n",
"\n",
"ALERT, BAND = 70.0, 5.0 # ์ ์ 65 ๋ฏธ๋ง '๊ฒฝ๋ณด', 75 ์ด์ '์ ์', ๊ทธ ์ฌ์ด๋ ๋ชจ๋ธ ๋ถํ์ค ๊ตฌ๊ฐ์ด๋ผ '๋ณด๋ฅ(์๋ ํ์ธ)'\n",
"\n",
"\n",
"def make_batch(n, seed):\n",
" \"\"\"[๊ต์ก์ฉ๊ฐ์์๋ฃ] ๋ณต์ก ํ์(๋์ถ ๋ง๊ณ ๊ตฌ๋ฉ 3๊ฐ ์ด์) ๋ถํ์ n์ข
๋ฝ์ ํ๊บผ๋ฒ์ ํ๊ฐ๋ฅผ ์์ฒญํ๋ค.\"\"\"\n",
" pool = make_parts(max(4 * n, 600), seed=seed)\n",
" cx = pool[(pool[\"overhang\"] > 0.3) & (pool[\"n_holes\"] >= 3)]\n",
" return cx.sample(n, replace=len(cx) < n, random_state=seed).reset_index(drop=True)\n",
"\n",
"\n",
"def validate(r):\n",
" errs = []\n",
" if not r[\"min_wall_mm\"] > 0 or not r[\"max_dim_mm\"] > 0:\n",
" errs.append(\"์น์ ์ค๋ฅ\")\n",
" if r[\"material\"] not in MAT:\n",
" errs.append(\"์์ฌ ์ฝ๋ ์ค๋ฅ\")\n",
" if not np.isfinite(r[\"load_N\"]):\n",
" errs.append(\"ํ์ค ๋๋ฝ\")\n",
" return errs\n",
"\n",
"\n",
"def fallback_score(X):\n",
" \"\"\"๋ชจ๋ธ ์ฅ์ ์ ๊ฐ์ด ๊ท์น: ์๋ ฅ/๊ธฐ์ค๊ฐ๋ ๋น๋ก๋ง ์ ์ํ(๋ณด์์ ).\"\"\"\n",
" return np.clip(100 * (1 - 1.8 * X[\"stress_ratio\"]), 0, 100).values\n",
"\n",
"\n",
"def evaluate(batch, model, fail_model=False):\n",
" ok_mask = np.array([not validate(r) for r in batch.to_dict(\"records\")])\n",
" good, manual = batch[ok_mask].copy(), batch[~ok_mask]\n",
" X = make_features(good)\n",
" try:\n",
" if fail_model:\n",
" raise RuntimeError(\"model down\")\n",
" good[\"score\"], good[\"src\"] = np.clip(model.predict(X), 0, 100), \"๋ชจ๋ธ\"\n",
" except Exception:\n",
" good[\"score\"], good[\"src\"] = fallback_score(X), \"๊ฐ์ด ๊ท์น\"\n",
" good[\"state\"] = np.where(good[\"score\"] < ALERT - BAND, \"๊ฒฝ๋ณด\", np.where(good[\"score\"] >= ALERT + BAND, \"์ ์\", \"๋ณด๋ฅ\"))\n",
" good[\"alert\"] = good[\"state\"] == \"๊ฒฝ๋ณด\"\n",
" return good, manual\n",
"\n",
"\n",
"def dashboard(good):\n",
" fig, ax = plt.subplots(1, 3, figsize=(14, 4))\n",
" ax[0].hist(good[\"score\"], bins=20); ax[0].axvline(ALERT, ls=\"--\", c=\"r\"); ax[0].set_title(\"Score distribution\")\n",
" ax[1].scatter(good[\"score\"], good[\"build_h\"], c=good[\"state\"].map({\"๊ฒฝ๋ณด\": \"red\", \"๋ณด๋ฅ\": \"orange\", \"์ ์\": \"gray\"}), s=12); ax[1].set_yscale(\"log\"); ax[1].set_title(\"Score vs build hours (red = alert, orange = hold)\")\n",
" good.groupby(\"cat\")[\"alert\"].sum().plot.bar(ax=ax[2]); ax[2].set_title(\"Alerts by category\")\n",
" fig.tight_layout(); fig.canvas.draw()\n",
" return fig\n",
"\n",
"\n",
"hist = make_parts(1000, seed=11)\n",
"model = GradientBoostingRegressor(n_estimators=150, max_depth=3, learning_rate=0.06, random_state=0).fit(make_features(hist), hist[\"conform\"])\n",
"rows, keep = [], {}\n",
"for n, bad, down in [(50, 0, False), (50, 4, False), (100, 6, False), (200, 10, False), (500, 20, False), (50, 4, True)]:\n",
" batch = make_batch(n, seed=n + bad)\n",
" for k in range(bad): # ์์ ๋ ์ฝ๋ ์ฃผ์
\n",
" i = k * 3\n",
" if k % 3 == 0:\n",
" batch.loc[i, \"min_wall_mm\"] = -1.0\n",
" elif k % 3 == 1:\n",
" batch.loc[i, \"material\"] = \"unknown\"\n",
" else:\n",
" batch.loc[i, \"load_N\"] = np.nan\n",
" t0 = time.perf_counter()\n",
" good, manual = evaluate(batch, model, fail_model=down)\n",
" fig = dashboard(good)\n",
" sec = time.perf_counter() - t0\n",
" truth = good[\"conform\"] < ALERT\n",
" decided = good[\"state\"] != \"๋ณด๋ฅ\"\n",
" acc = float((good[\"alert\"] == truth)[decided].mean())\n",
" rows.append({\"์ํฉ\": f\"{n}์ข
\" + (f\", ์์ {bad}๊ฑด\" if bad else \"\") + (\", ๋ชจ๋ธ ์ฅ์ \" if down else \"\"), \"ํ๊ฐ\": len(good), \"์๋ ํ์ธ\": len(manual),\n",
" \"์ ์ค\": n - len(good) - len(manual), \"์ฒ๋ฆฌ+ํ๋ฉด(์ด)\": round(sec, 2), \"๊ฒฝ๋ณด\": int(good[\"alert\"].sum()), \"๋ณด๋ฅ\": int((~decided).sum()), \"๊ฒฝ๋ณด ์ ํ๋\": round(acc, 3),\n",
" \"๋ฏธ๋ฌ ๋์นจ\": int((truth & (good[\"state\"] == \"์ ์\")).sum())})\n",
" keep[n, bad, down] = (fig, good)\n",
" if (n, bad, down) != (50, 4, False):\n",
" plt.close(fig)\n",
"T = pd.DataFrame(rows)\n",
"print(\"[๊ต์ก์ฉ๊ฐ์์๋ฃ] ๋ณต์ก ํ์ ๋ถํ ์ผ๊ด ์ ๋ขฐ์ฑ ํ๊ฐ ์คํธ๋ ์ค ํ
์คํธ\")\n",
"print(T.to_string(index=False))\n",
"m = T.iloc[1]\n",
"normal = T[~T[\"์ํฉ\"].str.contains(\"์ฅ์ \")]\n",
"print(f\"\\n50์ข
(์์ 4๊ฑด) ๊ธฐ์ค: ์ฒ๋ฆฌ+ํ๋ฉด {m['์ฒ๋ฆฌ+ํ๋ฉด(์ด)']}์ด (๋ชฉํ 2์ด ์ด๋ด: {'๋ฌ์ฑ' if normal['์ฒ๋ฆฌ+ํ๋ฉด(์ด)'].max() <= 2 else '๋ฏธ๋ฌ'}), \"\n",
" f\"ํ์ ํ์ ๊ฒฝ๋ณด ์ ํ๋ {normal['๊ฒฝ๋ณด ์ ํ๋'].min():.1%}~{normal['๊ฒฝ๋ณด ์ ํ๋'].max():.1%} (๋ชฉํ 95% ์ด์: {'๋ฌ์ฑ' if normal['๊ฒฝ๋ณด ์ ํ๋'].min() >= 0.95 else '๋ฏธ๋ฌ'}), ์ ์ค {int(T['์ ์ค'].sum())}๊ฑด\")\n",
"print(f\"๋ณด๋ฅ(65~75์ ) ๋น์จ {(T['๋ณด๋ฅ'] / T['ํ๊ฐ']).min():.0%}~{(T['๋ณด๋ฅ'] / T['ํ๊ฐ']).max():.0%}: ๋ชจ๋ธ์ด ํ๋จํ๊ธฐ ์ด๋ ค์ด ๊ตฌ๊ฐ์ ์๋ ํ์ ํ์ง ์๊ณ ์ ๋น๊ธฐ์ ๊ด ์๋ ํ์ธ์ผ๋ก ๋๊ธด๋ค\")\n",
"print(f\"ํ๊ณ์น: 500์ข
์์๋ {T.iloc[4]['์ฒ๋ฆฌ+ํ๋ฉด(์ด)']}์ด -> ์ด ๊ท๋ชจ์์๋ ์ฒ๋ฆฌ ์๊ฐ์ด ๋ณ๋ชฉ์ด ์๋๋ค. ๋ณ๋ชฉ์ ์์ ๋ ์ฝ๋ ์๋ ํ์ธ๊ณผ ์ ๋น๊ธฐ์ ๊ด ๊ฒํ ์ธ๋ ฅ์ด๋ค.\")\n",
"print(\"์์ ๋ ์ฝ๋๋ ์ญ์ ํ์ง ์๊ณ '์๋ ํ์ธ' ๋ชฉ๋ก์ผ๋ก ๋๊ฒจ ์ค๊ณ์๊ฐ CADยท์ํ ๊ธฐ๋ก์ ์ฌํ์ธํ๋ค.\")\n",
"\n",
"good = keep[50, 4, False][1]\n",
"qt = []\n",
"for c in good[\"cat\"].unique():\n",
" for mt in MAT:\n",
" t0 = time.perf_counter(); good[(good[\"cat\"] == c) & (good[\"material\"] == mt)]; qt.append((time.perf_counter() - t0) * 1000)\n",
"print(f\"์ค์๊ฐ ํํฐ(๋ถํ๊ตฐ x ์์ฌ) ํ๊ท {np.mean(qt):.2f} ms\")\n",
"fb = T.iloc[5]\n",
"print(f\"\\n๋ชจ๋ธ ์ฅ์ ์ ๊ฐ์ด ๊ท์น์ผ๋ก ์ ํ: ํ์ ํ์ ์ ํ๋ {fb['๊ฒฝ๋ณด ์ ํ๋']:.1%}, ๊ฒฝ๋ณด {fb['๊ฒฝ๋ณด']}๊ฑด(์ ์ ๋ชจ๋ธ {T.iloc[1]['๊ฒฝ๋ณด']}๊ฑด), ๋ฏธ๋ฌ ๋์นจ {fb['๋ฏธ๋ฌ ๋์นจ']}๊ฑด(์ ์ ๋ชจ๋ธ {T.iloc[1]['๋ฏธ๋ฌ ๋์นจ']}๊ฑด)\")\n",
"print(\"๊ธฐ๋ฅ ์ถ์ ์ด์ฉ: ๋ชจ๋ธ ์ฅ์ ๋๋ ์ฒ๋ฆฌ 2์ด ์ด๊ณผ ์ ์๋ ฅ/๊ธฐ์ค๊ฐ๋ ๋น ๊ท์น ์ ์๋ก ์ฆ์ ํ๊ฐํ๊ณ , ์ ๋ฐ ์ ์๋ ๋ณต๊ตฌ ํ ์ฌ๊ณ์ฐํ๋ค. ๊ท์น ์ ์๋ ๋ณด์์ ์ด๋ผ ๊ฒฝ๋ณด๊ฐ ๋์ด๋๋ค.\")\n",
"plt.show()\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"๐ก [์ฐธ๊ณ ] ๋ฏธ์
08 ์์ ์คํ ๊ฒฐ๊ณผ (Output)
\n",
"\n",
"```text\n",
"[๊ต์ก์ฉ๊ฐ์์๋ฃ] ๋ณต์ก ํ์ ๋ถํ ์ผ๊ด ์ ๋ขฐ์ฑ ํ๊ฐ ์คํธ๋ ์ค ํ
์คํธ\n",
" ์ํฉ ํ๊ฐ ์๋ ํ์ธ ์ ์ค ์ฒ๋ฆฌ+ํ๋ฉด(์ด) ๊ฒฝ๋ณด ๋ณด๋ฅ ๊ฒฝ๋ณด ์ ํ๋ ๋ฏธ๋ฌ ๋์นจ\n",
" 50์ข
50 0 0 0.88 29 9 0.976 1\n",
" 50์ข
, ์์ 4๊ฑด 46 4 0 0.67 26 11 0.971 1\n",
" 100์ข
, ์์ 6๊ฑด 94 6 0 0.78 67 10 1.000 0\n",
" 200์ข
, ์์ 10๊ฑด 190 10 0 0.57 104 35 0.987 0\n",
" 500์ข
, ์์ 20๊ฑด 480 20 0 0.50 262 110 0.978 0\n",
"50์ข
, ์์ 4๊ฑด, ๋ชจ๋ธ ์ฅ์ 46 4 0 0.61 40 4 0.810 0\n",
"\n",
"50์ข
(์์ 4๊ฑด) ๊ธฐ์ค: ์ฒ๋ฆฌ+ํ๋ฉด 0.67์ด (๋ชฉํ 2์ด ์ด๋ด: ๋ฌ์ฑ), ํ์ ํ์ ๊ฒฝ๋ณด ์ ํ๋ 97.1%~100.0% (๋ชฉํ 95% ์ด์: ๋ฌ์ฑ), ์ ์ค 0๊ฑด\n",
"๋ณด๋ฅ(65~75์ ) ๋น์จ 9%~24%: ๋ชจ๋ธ์ด ํ๋จํ๊ธฐ ์ด๋ ค์ด ๊ตฌ๊ฐ์ ์๋ ํ์ ํ์ง ์๊ณ ์ ๋น๊ธฐ์ ๊ด ์๋ ํ์ธ์ผ๋ก ๋๊ธด๋ค\n",
"ํ๊ณ์น: 500์ข
์์๋ 0.5์ด -> ์ด ๊ท๋ชจ์์๋ ์ฒ๋ฆฌ ์๊ฐ์ด ๋ณ๋ชฉ์ด ์๋๋ค. ๋ณ๋ชฉ์ ์์ ๋ ์ฝ๋ ์๋ ํ์ธ๊ณผ ์ ๋น๊ธฐ์ ๊ด ๊ฒํ ์ธ๋ ฅ์ด๋ค.\n",
"์์ ๋ ์ฝ๋๋ ์ญ์ ํ์ง ์๊ณ '์๋ ํ์ธ' ๋ชฉ๋ก์ผ๋ก ๋๊ฒจ ์ค๊ณ์๊ฐ CADยท์ํ ๊ธฐ๋ก์ ์ฌํ์ธํ๋ค.\n",
"์ค์๊ฐ ํํฐ(๋ถํ๊ตฐ x ์์ฌ) ํ๊ท 0.72 ms\n",
"\n",
"๋ชจ๋ธ ์ฅ์ ์ ๊ฐ์ด ๊ท์น์ผ๋ก ์ ํ: ํ์ ํ์ ์ ํ๋ 81.0%, ๊ฒฝ๋ณด 40๊ฑด(์ ์ ๋ชจ๋ธ 26๊ฑด), ๋ฏธ๋ฌ ๋์นจ 0๊ฑด(์ ์ ๋ชจ๋ธ 1๊ฑด)\n",
"๊ธฐ๋ฅ ์ถ์ ์ด์ฉ: ๋ชจ๋ธ ์ฅ์ ๋๋ ์ฒ๋ฆฌ 2์ด ์ด๊ณผ ์ ์๋ ฅ/๊ธฐ์ค๊ฐ๋ ๋น ๊ท์น ์ ์๋ก ์ฆ์ ํ๊ฐํ๊ณ , ์ ๋ฐ ์ ์๋ ๋ณต๊ตฌ ํ ์ฌ๊ณ์ฐํ๋ค. ๊ท์น ์ ์๋ ๋ณด์์ ์ด๋ผ ๊ฒฝ๋ณด๊ฐ ๋์ด๋๋ค.\n",
"```\n",
" \n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## ๐ฏ ๋ฏธ์
09. AAR ๊ธฐ๋ฐ ์ค๋ฌผ ํ๊ดด ์ํ ๊ฒฐ๊ณผ์ 3D ๋ชจ๋ธ ์์ธก์น ๊ฐ ์ค์ฐจ ๋ถ์ ๋ฐ ๋ณด์ \n",
"**์์์๊ฐ**: ์ฝ 3.0์๊ฐ\n",
"\n",
"### ๐ ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ์ํ ์ง์นจ\n",
"#### ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ๋น์์ฑ\n",
"\n",
"AAR ๊ธฐ๋ฐ ์ค๋ฌผ ํ๊ดด ์ํ ๊ฒฐ๊ณผ์ ์์ธก์น ๊ฐ ์ค์ฐจ๋ฅผ ๋ถ์ ๋ณด์ ํ๋ค. ์ค์ ์ ์ ํ์ฅ์์๋ ๋จ์ ํ์ง์ ๊ทธ์น์ง ์๊ณ ์ ์ํ ์ํ ํ๋จ๊ณผ ์ ๋ขฐ๋ ๋์ ์์ฌ๊ฒฐ์ ์ด ํ์์ ์ด๋ฏ๋ก, **์ค๋ฌผ ํ๊ดด ์ํ ๊ฒฐ๊ณผ์ 3D ๋ชจ๋ธ ์์ธก ๊ฐ๋์ ์ค์ฐจ๋ฅผ AAR๋ก ๋ถ์ํด ์์ธ์ ๋ถ๋ฅํ๊ณ ๋ณด์ ํด ๋์
ํจ๊ณผ๋ฅผ ์ ๋ ์งํ๋ก ์ฐ์ถํ๋ค.** ๊ณผ์
์ ์์ํ์ฌ ์คํ์ ๋ฐฉ์งํ๊ณ ์์ ๋์๋ ฅ์ ๊ทน๋ํํด์ผ ํฉ๋๋ค.\n",
"\n",
"์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ณธ ๋ฏธ์
์์๋ **์์ธก์ด ์ค์ธก๋ณด๋ค 10% ์ด์ ๋์๋ ์ํ ์ฌ๋ก๋ฅผ ์์ ๋ฒฝ ๋ฏธ์ถฉ์ , ๊ธ์ ์๋ฅ์๋ ฅ, ์ธ์ฆ์ ์๋ ์์ฌ ํธ์ฐจ, ์ํ ์ฐํฌ๋ก ๋ถ๋ฅ ์ํํ๊ณ ๊ฒ์ฆํ๋** ์ค์ต์ ์ฒด๊ณ์ ์ผ๋ก ์ํํฉ๋๋ค.\n",
"\n",
"ํ์ต์๋ ์ค์ต๊ฐ์ด๋์ ์ ์๋ ์ด 4๊ฐ ์ธ๋ถ ํ๋(๊ธฐ๋ณธ ๊ธฐ๋ฅ ๊ตฌํ โ ๊ฒฐ๊ณผ ์งํ ๊ฒ์ฆ โ ํ๋ผ๋ฏธํฐ ํ๋ ๋ฐ ๋์ ๊ณผ์ )์ ์์ฐจ์ ์ผ๋ก ์งํํ๋ฉฐ ํต์ฌ ํ์ ๊ธฐ์ค์ ๋ฌ์ฑํฉ๋๋ค.\n",
"\n",
"**์ด๋ฒ ๊ฒฐ๊ณผ๋ฌผ:** `reports/mnt06_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๋ก ์ค๋ฌผ ํ๊ดด ์ํ ๊ฒฐ๊ณผ์ 3D ๋ชจ๋ธ(๊ฐ๋ ์์ธก)์ ์ค์ฐจ๋ฅผ ๋ถ์ํด ๊ณผ๋ ์์ธก ์์ธ์ ๋ถ๋ฅํ๊ณ , ๊ณผ๊ฑฐ ์ํ์ผ๋ก ๋ณด์ ๊ณ์์ ์น์ธ ๊ธฐ์ค์ ์ฌ๋ณด์ ํด ์ดํ ์ํ์์ ๊ฒ์ฆํ๋ค. ๋น์ฉ ์ ๊ฐ์กยท๊ณต๊ธ ์๊ฐ ๋จ์ถ๋ฅ ยทํ๊ธฐ ๊ฐ์๋์ 3๋ ์ ๋ ํจ๊ณผ ์งํ์ ๋จ๊ณ๋ณ ๋์
๋ก๋๋งต์ ์ถ๋ ฅํ๋ค.\"\"\"\n",
"import numpy as np\n",
"import pandas as pd\n",
"import matplotlib.pyplot as plt\n",
"from sklearn.ensemble import GradientBoostingRegressor\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# [๊ต์ก์ฉ๊ฐ์์๋ฃ] ์ผ์ 3D ํ๋ฆฐํ
๋์ฒด ์ ์ ํ๋ณด ๋ถํ ์์ฑ๊ธฐ (์ค์ ๋ถํยท์ฌ๊ณ ยท๊ท๊ฒฉ ๊ฐ ์๋)\n",
"# ํ๊ธฐ์จ์ด ๋์ ์๋ฆฌ๋ถ์ ์ค 3D ํ๋ฆฐํ
์ผ๋ก ๋์ฒด ์ ์ํ ์ ์๋์ง ํ๋จํ๊ธฐ ์ํ ๊ฐ์ ๋ถํ ์๋ฃ๋ฅผ ๋ง๋ ๋ค.\n",
"# ์์ฌ ๋ฌผ์ฑยท์น์ยทํ์ค์ ๊ต์ก์ฉ์ผ๋ก ๋จ์ํํ ๊ฐ์ด๋ฉฐ ์ค์ ์ํ ์ฑ์ ์๋ ๊ตฐ ๊ท๊ฒฉ ๊ฐ๊ณผ ๋ฌด๊ดํ๋ค.\n",
"# ---------------------------------------------------------------------------\n",
"MAT = { # ์์ฌ: (๊ณต์ , ๊ธฐ์ค ์ธ์ฅ๊ฐ๋ MPa, ๋ฐ๋ g/cm3, ์์ฌ ๋จ๊ฐ ๋ง์/kg, ์ถ๋ ฅ๋น ๋ง์/h, ์ต์ ๋ฒฝ๋๊ป mm, ์ถ๋ ฅ ์๋ cm3/h)\n",
" \"PA12\": (\"polymer\", 45, 1.0, 8, 3, 0.8, 18), \"PETG\": (\"polymer\", 50, 1.27, 5, 2, 0.8, 15),\n",
" \"CF-PA\": (\"polymer\", 70, 1.2, 14, 3, 1.0, 12), \"AlSi10Mg\": (\"metal\", 350, 2.67, 30, 25, 0.5, 4),\n",
" \"SS316L\": (\"metal\", 520, 7.9, 28, 25, 0.6, 3.5), \"Ti64\": (\"metal\", 900, 4.4, 90, 30, 0.6, 3)}\n",
"CATS = [\"bracket\", \"housing\", \"clip\", \"gear\", \"duct\", \"handle\"]\n",
"CAT_SCRAP = {\"bracket\": 0.04, \"housing\": 0.05, \"clip\": 0.12, \"gear\": 0.09, \"duct\": 0.08, \"handle\": 0.10} # ๋ถํ๊ตฐ๋ณ ๊ธฐ๋ณธ ์ ํ๊ธฐ์จ\n",
"BUILD_MM = {\"polymer\": 300, \"metal\": 200} # ๊ฐ์ ์ถ๋ ฅ ๊ฐ๋ฅ ์ต๋ ์น์\n",
"\n",
"\n",
"def make_parts(n=300, seed=0):\n",
" \"\"\"๊ฐ์ ๋ถํ n์ข
: ํ์(๋ถํผยท๋ฒฝ๋๊ปยท๋์ถ ๋น์จ), ์์ฌ, ์ฌ์ฉ ํ์ค, ํ๊ธฐ์จ๊ณผ ์กฐ๋ฌ ์ ๋ณด๋ฅผ ๋ง๋ ๋ค.\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" mat = rng.choice(list(MAT), n, p=[0.2, 0.15, 0.1, 0.2, 0.2, 0.15])\n",
" proc = np.array([MAT[m][0] for m in mat])\n",
" vol = np.clip(rng.lognormal(np.log(30), 0.9, n), 2, 800)\n",
" aspect = rng.lognormal(0.3, 0.4, n)\n",
" max_dim = 10 * vol ** (1 / 3) * aspect ** (1 / 3) * rng.uniform(1.0, 1.6, n)\n",
" wall = np.clip(rng.lognormal(np.log(2.0), 0.6, n), 0.3, 8)\n",
" ovh = rng.beta(2, 5, n)\n",
" is_poly = proc == \"polymer\"\n",
" df = pd.DataFrame({\n",
" \"part_id\": [f\"P{i:03d}\" for i in range(n)], \"cat\": rng.choice(CATS, n, p=[0.2, 0.15, 0.2, 0.1, 0.15, 0.2]), \"material\": mat, \"process\": proc,\n",
" \"volume_cm3\": vol, \"max_dim_mm\": max_dim, \"area_cm2\": 6 * vol ** (2 / 3) * rng.uniform(1.0, 2.2, n), \"min_wall_mm\": wall, \"overhang\": ovh,\n",
" \"n_holes\": rng.poisson(2.0, n), \"thread\": rng.random(n) < 0.25, \"orient_deg\": rng.uniform(0, 90, n),\n",
" \"layer_mm\": np.where(is_poly, rng.choice([0.1, 0.2, 0.3], n), rng.choice([0.03, 0.05], n)),\n",
" \"infill\": np.where(is_poly, rng.choice([40, 60, 80, 100], n, p=[0.15, 0.3, 0.3, 0.25]), 100),\n",
" \"load_N\": np.clip(rng.lognormal(np.where(is_poly, np.log(230), np.log(2300)), 0.7, n), 10, 20000), \"temp_C\": rng.uniform(-20, 90, n),\n",
" \"crit\": rng.choice([1, 2, 3], n, p=[0.35, 0.4, 0.25]), \"cert\": rng.choice([0, 1], n, p=[0.45, 0.55]), # ์์ฌ ์ธ์ฆ์ ๋ณด์ \n",
" \"import_part\": rng.random(n) < 0.4})\n",
" df[\"scrap_rate\"] = np.clip(df[\"cat\"].map(CAT_SCRAP) * rng.lognormal(0, 0.35, n) * (1 + 0.5 * (df[\"load_N\"] > 800)) * (1 + 0.3 * (df[\"temp_C\"] > 60)), 0.005, 0.5)\n",
" df[\"lead_days\"] = np.clip(rng.lognormal(np.log(25), 0.5, n) * np.where(df[\"import_part\"], 2.2, 1.0), 5, 300)\n",
" # ---- ์จ์ ์ง์ค๊ฐ(์ค์ ์ํ ๊ฒฐ๊ณผ์ ํด๋น): ๋ชจ๋ธ์ ์ ์
๋ ฅ์ผ๋ก ์ด๊ฒ์ ์์ธกํ๋ค ----\n",
" eff = np.where(is_poly, 0.55 + 0.45 * df[\"infill\"] / 100, 1 - 0.3 * ovh * 0.2)\n",
" aniso = np.where(is_poly, 1 - 0.3 * np.sin(np.radians(df[\"orient_deg\"])) ** 2, 1 - 0.08 * np.sin(np.radians(df[\"orient_deg\"])))\n",
" heat = np.where(is_poly, 1 - 0.35 * np.clip((df[\"temp_C\"] - 40) / 80, 0, 1), 0.98)\n",
" base = np.array([MAT[m][1] for m in mat])\n",
" df[\"strength_mpa\"] = base * eff * aniso * heat * rng.lognormal(0, 0.06, n)\n",
" cross = df[\"min_wall_mm\"] * np.sqrt(df[\"max_dim_mm\"]) * 1.0 # ํ์ค ์ง์ง ๋จ๋ฉด์ mm2(๋จ์ํ)\n",
" df[\"stress_mpa\"] = df[\"load_N\"] / cross\n",
" df[\"safety\"] = df[\"strength_mpa\"] / df[\"stress_mpa\"]\n",
" ratio = np.clip(df[\"stress_mpa\"] / df[\"strength_mpa\"], 0, 1.5)\n",
" df[\"log_cycles\"] = np.clip(7.5 - 6.0 * ratio + rng.normal(0, 0.25, n), 2, 7.5) # ํผ๋ก ์๋ช
log10(ํ)\n",
" support = 1 + 0.5 * ovh\n",
" df[\"build_h\"] = vol * support / np.array([MAT[m][6] for m in mat]) + 0.5\n",
" mass_kg = vol * np.array([MAT[m][2] for m in mat]) * support / 1000\n",
" df[\"print_cost\"] = mass_kg * np.array([MAT[m][3] for m in mat]) + df[\"build_h\"] * np.array([MAT[m][4] for m in mat]) + 3\n",
" df[\"buy_cost\"] = df[\"print_cost\"] * rng.lognormal(0.25, 0.45, n) + 5 # ๊ตฌ๋งค ๋จ๊ฐ(๋ง์, ๊ฐ์)\n",
" wall_ok = df[\"min_wall_mm\"] >= np.array([MAT[m][5] for m in mat]) * rng.uniform(0.95, 1.25, n)\n",
" dim_ok = df[\"max_dim_mm\"] <= np.where(is_poly, BUILD_MM[\"polymer\"], BUILD_MM[\"metal\"])\n",
" warp = (~is_poly) & (df[\"max_dim_mm\"] > 140) & (df[\"overhang\"] > 0.3) # ๊ธ์ ๋ํ ์ค๋ฒํ ๋ณํ\n",
" thr_bad = df[\"thread\"] & (df[\"min_wall_mm\"] < 1.5) & is_poly # ์์ ๋์ฌ๋ถ ํ์\n",
" df[\"printable\"] = wall_ok & dim_ok & (ovh < 0.5) & ~warp & ~thr_bad\n",
" risk = np.clip(1 - df[\"safety\"] / 3, 0, 1)\n",
" df[\"conform\"] = np.clip(100 * (0.45 * np.minimum(df[\"safety\"] / 3, 1) + 0.25 * np.clip((df[\"log_cycles\"] - 3) / 4.5, 0, 1)\n",
" + 0.15 * (1 - np.clip(ovh * 1.2, 0, 1)) + 0.15 * df[\"cert\"]) - 8 * risk * df[\"crit\"] / 3 + rng.normal(0, 2, n), 0, 100)\n",
" df[\"demand_month\"] = rng.poisson(np.where(df[\"cat\"].isin([\"clip\", \"handle\"]), 40, 15)) + 1 # ์ ์์์๋(๊ฐ์)\n",
" df[\"scrap_cost\"] = df[\"scrap_rate\"] * df[\"demand_month\"] * df[\"buy_cost\"] # ์ ํ๊ธฐ ์์ค์ก(๋ง์, ๊ฐ์)\n",
" return df\n",
"\n",
"# ---- ๋จ์ํํ ํด์(FEA ๋์ฉ) ๊ฐ๊ณผ ํ์ ํผ์ฒ: ์
๋ ฅ ์ ๋ณด๋ง์ผ๋ก ๊ณ์ฐํ๋ค(์ํ ๊ฒฐ๊ณผ ๋์ค ์์) ----\n",
"def make_features(d):\n",
" \"\"\"๊ฐ์ด ์๋ ฅ ํด์(์๋ ฅ์ง์ค๊ณ์ ํฌํจ)๊ณผ ์์ฌยท๊ณต์ ์ง์์์ ๋ง๋ ํ์ ํผ์ฒ 16๊ฐ.\"\"\"\n",
" X = pd.DataFrame(index=d.index)\n",
" base = d[\"material\"].map(lambda m: MAT[m][1]).astype(float)\n",
" X[\"base_strength\"] = base\n",
" X[\"is_metal\"] = (d[\"process\"] == \"metal\").astype(int)\n",
" X[\"cross_mm2\"] = d[\"min_wall_mm\"] * np.sqrt(d[\"max_dim_mm\"])\n",
" X[\"kt\"] = 1 + 0.15 * d[\"n_holes\"] + 0.5 * d[\"thread\"].astype(int) # ๊ตฌ๋ฉยท๋์ฌ๋ถ ์๋ ฅ์ง์ค๊ณ์(๊ฐ์ด)\n",
" X[\"fea_stress\"] = d[\"load_N\"] / X[\"cross_mm2\"] * X[\"kt\"] # ๊ฐ์ด ํด์ ์๋ ฅ(MPa)\n",
" X[\"stress_ratio\"] = X[\"fea_stress\"] / base # ๊ธฐ์ค๊ฐ๋ ๋๋น ์๋ ฅ๋น\n",
" X[\"infill_eff\"] = np.where(X[\"is_metal\"] == 1, 1.0, 0.55 + 0.45 * d[\"infill\"] / 100)\n",
" X[\"orient_sin2\"] = np.sin(np.radians(d[\"orient_deg\"])) ** 2\n",
" X[\"temp_derate\"] = np.where(X[\"is_metal\"] == 1, 0.0, np.clip((d[\"temp_C\"] - 40) / 80, 0, 1))\n",
" X[\"overhang_layer\"] = d[\"overhang\"] * d[\"layer_mm\"]\n",
" X[\"slender\"] = d[\"max_dim_mm\"] / d[\"min_wall_mm\"]\n",
" X[\"surf_vol\"] = d[\"area_cm2\"] / d[\"volume_cm3\"]\n",
" X[\"wall_ratio\"] = d[\"min_wall_mm\"] / d[\"material\"].map(lambda m: MAT[m][5])\n",
" X[\"load_per_vol\"] = d[\"load_N\"] / d[\"volume_cm3\"]\n",
" X[\"cert\"] = d[\"cert\"].astype(int)\n",
" X[\"crit\"] = d[\"crit\"]\n",
" return X\n",
"\n",
"# ---- ๋์ฒด ์ ์ ์์ฒญ ๋น์ฉ ๋ชจ์(๊ฐ์ ๋จ๊ฐ): ๊ตฌ๋งค ๋๊ธฐ vs 3D ํ๋ฆฐํ
----\n",
"DOWN = {1: 0.5, 2: 2.0, 3: 5.0} # [๊ต์ก์ฉ๊ฐ์์๋ฃ] ๋ถํ ๋ถ์ฌ๋ก ์ธํ ์ฅ๋น ๊ฐ๋์ค๋จ ๋น์ฉ(๋ง์/์ผ), ์ค์๋๋ณ\n",
"FAIL = {1: 10.0, 2: 40.0, 3: 150.0} # [๊ต์ก์ฉ๊ฐ์์๋ฃ] ๋์ฒดํ ์ฅ์ฐฉ ํ ๋ถํฉ๊ฒฉ ์ ์ฌ์์
ยท์๋ฌด ์ํฅ ๋น์ฉ(๋ง์)\n",
"POST = 3.0 # ํ์ฒ๋ฆฌยท๊ฒ์ฌ ๋น์ฉ(๋ง์)\n",
"\n",
"\n",
"def make_requests_month(catalog, n=250, seed=0):\n",
" \"\"\"์๊ฐ ๋ถํ ์์ฒญ: ์ ์์์๋์ด ํฐ ๋ถํ์ด ๋ ์์ฃผ ์์ฒญ๋๋ค.\"\"\"\n",
" return catalog.sample(n, replace=True, weights=catalog[\"demand_month\"], random_state=seed).reset_index(drop=True)\n",
"\n",
"\n",
"def buy_cost(r):\n",
" return r[\"buy_cost\"] + r[\"lead_days\"] * DOWN[r[\"crit\"]]\n",
"\n",
"\n",
"def print_outcome(r, delay_d=1.0):\n",
" \"\"\"์ค์ ์ํ ๊ฒฐ๊ณผ(์จ์ ์ง์ค)์ ๋ฐ๋ฅธ ๋์ฒด ์ ์ ๋น์ฉ. ๋ถํฉ๊ฒฉ์ด๋ฉด ๊ตฌ๋งค๋ก ๋๋์๊ฐ๊ณ ๋ถํฉ๊ฒฉ ๋น์ฉ์ด ๋ ๋ค.\"\"\"\n",
" wait = (r[\"build_h\"] / 24 + delay_d + 2.0) * DOWN[r[\"crit\"]]\n",
" ok = bool(r[\"printable\"] and r[\"conform\"] >= 70)\n",
" c = r[\"print_cost\"] + POST + wait\n",
" return (c, True) if ok else (c + buy_cost(r) + FAIL[r[\"crit\"]], False)\n",
"\n",
"SF_MIN = 1.5 # ์ต์ ์์ ์จ\n",
"SCRAP_CUT = 0.30 # [๊ต์ก์ฉ๊ฐ์์๋ฃ] ๋์ฒด ๊ณต๊ธ ์ฑ๊ณต ๋ถํ์ ํ๊ธฐ ์๋์ด ์ค์ด๋ ๋ค๊ณ ๊ฐ์ ํ๋ ๋น์จ\n",
"\n",
"\n",
"def destructive_test(d, seed):\n",
" \"\"\"[๊ต์ก์ฉ๊ฐ์์๋ฃ] ์ค๋ฌผ ํ๊ดด ์ํ ๊ฐ๋: ๋ชจ๋ธ์ด ๋ชจ๋ฅด๋ ์ํฅ(์์ ๋ฒฝ ๋ฏธ์ถฉ์ ยท๊ธ์ ์๋ฅ์๋ ฅยท์ธ์ฆ์ ์๋ ์์ฌ ํธ์ฐจ)์ด ์์ธ๋ค.\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" f = np.ones(len(d))\n",
" f *= np.where((d[\"process\"] == \"polymer\") & (d[\"min_wall_mm\"] < 1.5), 0.86, 1.0)\n",
" f *= np.where((d[\"process\"] == \"metal\") & (d[\"overhang\"] > 0.3), 0.88, 1.0)\n",
" f *= np.where(d[\"cert\"] == 0, 0.94, 1.0)\n",
" return d[\"strength_mpa\"] * f * rng.lognormal(0, 0.03, len(d))\n",
"\n",
"\n",
"hist = make_parts(1000, seed=11)\n",
"Xh = make_features(hist)\n",
"model = GradientBoostingRegressor(n_estimators=150, max_depth=3, learning_rate=0.06, random_state=0).fit(Xh, hist[\"strength_mpa\"] / Xh[\"base_strength\"])\n",
"\n",
"\n",
"def predict_strength(d):\n",
" X = make_features(d)\n",
" return model.predict(X) * X[\"base_strength\"], X\n",
"\n",
"\n",
"def group(d):\n",
" return np.select([(d[\"process\"] == \"polymer\") & (d[\"min_wall_mm\"] < 1.5), (d[\"process\"] == \"metal\") & (d[\"overhang\"] > 0.3)], [\"ํด๋ฆฌ๋จธ ์์ ๋ฒฝ\", \"๊ธ์ ๋์ถ ๋ง์\"], \"์ผ๋ฐ\")\n",
"\n",
"\n",
"cal, test = make_parts(250, seed=31), make_parts(250, seed=32) # ๊ณผ๊ฑฐ ์ํ(๋ณด์ ์ฉ) / ์ดํ ์ํ(๊ฒ์ฆ์ฉ)\n",
"for d, s in ((cal, 41), (test, 42)):\n",
" d[\"meas\"] = destructive_test(d, s); d[\"pred\"], _ = predict_strength(d); d[\"grp\"] = group(d)\n",
" d[\"err\"] = d[\"pred\"] / d[\"meas\"] - 1\n",
"\n",
"# 1) AAR: ๊ณผ๋ ์์ธก(+10% ์ด๊ณผ, ์ํํ ๋ฐฉํฅ) ์ฌ๋ก์ ์์ธ ๋ถ๋ฅ\n",
"over = cal[cal[\"err\"] > 0.10].copy()\n",
"over[\"์์ธ\"] = np.select([over[\"grp\"] == \"ํด๋ฆฌ๋จธ ์์ ๋ฒฝ\", over[\"grp\"] == \"๊ธ์ ๋์ถ ๋ง์\", over[\"cert\"] == 0], [\"์์ ๋ฒฝ ๋ฏธ์ถฉ์ (๋ชจ๋ธ ๋ฏธ๋ฐ์)\", \"๊ธ์ ์๋ฅ์๋ ฅ(๋ชจ๋ธ ๋ฏธ๋ฐ์)\", \"์ธ์ฆ์ ์๋ ์์ฌ ํธ์ฐจ\"], \"๊ธฐํ(์ํ ์ฐํฌ)\")\n",
"print(f\"[๊ต์ก์ฉ๊ฐ์์๋ฃ] AAR: ๊ณผ๊ฑฐ ์ํ {len(cal)}์ข
์ค ์์ธก์ด ์ค์ธก๋ณด๋ค 10% ์ด์ ๋์๋ ์ํ ์ฌ๋ก {len(over)}๊ฑด (ํ๊ท ์ค์ฐจ {cal['err'].mean():+.1%}, MAPE {cal['err'].abs().mean():.1%})\")\n",
"print(over[\"์์ธ\"].value_counts().to_string())\n",
"print(\"-> ์์ธ ๋๋ถ๋ถ์ด ๋ชจ๋ธ์ ์๋ ํ์ยท๊ณต์ ํจ๊ณผ๋ผ ์น์ธ ๊ธฐ์ค ๋ณด์ ์ผ๋ก ์ค์ผ ์ ์๋ค. '๊ธฐํ'๋ ์ํ ์ฐํฌ๋ผ ๋ณด์ ์ด ์๋ ์์ ์จ ์ฌ์ ๋ก ๋์ํ๋ค.\")\n",
"\n",
"# 2) ์ฌ๋ณด์ : ๊ทธ๋ฃน๋ณ ๋ณด์ ๊ณ์(์ค์๊ฐ ์ค์ธก/์์ธก) + ์น์ธ ์์ ์จ ์๊ณ๊ฐ\n",
"corr = pd.Series({g_: float(np.median((cal[\"meas\"] / cal[\"pred\"])[cal[\"grp\"] == g_])) for g_ in np.unique(cal[\"grp\"])})\n",
"print(\"\\n๋ณด์ ๊ณ์(์ค์ธก/์์ธก ์ค์๊ฐ):\", {k: round(v, 3) for k, v in corr.items()})\n",
"stress = lambda d: d[\"load_N\"] / (d[\"min_wall_mm\"] * np.sqrt(d[\"max_dim_mm\"]))\n",
"\n",
"\n",
"def sf_pred(d, use_corr):\n",
" p = d[\"pred\"] * (d[\"grp\"].map(corr) if use_corr else 1.0)\n",
" return p / stress(d)\n",
"\n",
"\n",
"def unsafe_rate(d, use_corr, thr):\n",
" ok = sf_pred(d, use_corr) >= thr\n",
" real_bad = (d[\"meas\"] / stress(d)) < SF_MIN\n",
" return float((ok & real_bad).sum() / max(ok.sum(), 1)), int(ok.sum())\n",
"\n",
"\n",
"thr_new = next((t for t in np.arange(1.5, 4.01, 0.05) if unsafe_rate(cal, True, t)[0] <= 0.02), 4.0)\n",
"rows = [{\"๋ฐฉ์\": \"ํํ(์์ธก ์์ ์จ 1.5 ์ด์)\", **dict(zip([\"์ํ ์น์ธ ๋น์จ\", \"์น์ธ ๊ฐ๋ฅ ๋ถํ\"], unsafe_rate(test, False, SF_MIN)))},\n",
" {\"๋ฐฉ์\": \"๋ณด์ ๋ง(๊ณ์ ์ ์ฉ)\", **dict(zip([\"์ํ ์น์ธ ๋น์จ\", \"์น์ธ ๊ฐ๋ฅ ๋ถํ\"], unsafe_rate(test, True, SF_MIN)))},\n",
" {\"๋ฐฉ์\": f\"์ฌ๋ณด์ (๊ณ์ + ์๊ณ {thr_new:.2f})\", **dict(zip([\"์ํ ์น์ธ ๋น์จ\", \"์น์ธ ๊ฐ๋ฅ ๋ถํ\"], unsafe_rate(test, True, thr_new)))}]\n",
"Tq = pd.DataFrame(rows)\n",
"Tq[\"์ํ ์น์ธ ๋น์จ\"] = (Tq[\"์ํ ์น์ธ ๋น์จ\"] * 100).round(1).astype(str) + \"%\"\n",
"print(f\"๊ณผ๊ฑฐ ์ํ์์ ์ํ ์น์ธ ๋น์จ 2% ์ดํ๋ฅผ ๋ง์กฑํ๋ ์ต์ ์๊ณ ์์ ์จ: {thr_new:.2f} (๊ณ์ ๋ณด์ ํ์๋ ๊ธฐ๋ณธ 1.5๋ก ์ถฉ๋ถ)\")\n",
"print(\"\\n[์ดํ ์ํ 250์ข
๊ฒ์ฆ]\")\n",
"print(Tq.to_string(index=False))\n",
"test[\"pred2\"] = test[\"pred\"] * test[\"grp\"].map(corr)\n",
"print(f\"๊ฐ๋ ์์ธก MAPE: ๋ณด์ ์ {(test['pred'] / test['meas'] - 1).abs().mean():.1%} -> ๋ณด์ ํ {(test['pred2'] / test['meas'] - 1).abs().mean():.1%}\")\n",
"\n",
"# 3) 3๋ ์ ๋ ํจ๊ณผ ์งํ: ์๊ฐ ์์ฒญ 250๊ฑด, ์น์ธ ๊ธฐ์ค ํํ vs ์ฌ๋ณด์ (๊ตฌ๋งค ๊ธฐ์ค ๋๋น)\n",
"cat = make_parts(300, seed=51)\n",
"cat[\"meas\"] = destructive_test(cat, 61); cat[\"pred\"], _ = predict_strength(cat); cat[\"grp\"] = group(cat)\n",
"cat[\"rule_ok\"] = (cat[\"min_wall_mm\"] >= 1.0) & (cat[\"max_dim_mm\"] <= cat[\"process\"].map(BUILD_MM)) & (cat[\"overhang\"] < 0.5)\n",
"req = make_requests_month(cat, 250, seed=3)\n",
"\n",
"\n",
"def outcome(r, approve):\n",
" c, days = buy_cost(r), r[\"lead_days\"]\n",
" if approve:\n",
" real_ok = bool(r[\"printable\"] and r[\"meas\"] / (r[\"load_N\"] / (r[\"min_wall_mm\"] * np.sqrt(r[\"max_dim_mm\"]))) >= SF_MIN)\n",
" pdays = r[\"build_h\"] / 24 + 3.0\n",
" cp = r[\"print_cost\"] + POST + pdays * DOWN[r[\"crit\"]]\n",
" return (cp, pdays, True, real_ok) if real_ok else (cp + c + FAIL[r[\"crit\"]], pdays + r[\"lead_days\"], True, False)\n",
" return c, days, False, True\n",
"\n",
"\n",
"def policy(use_corr, thr):\n",
" out = []\n",
" for _, r in req.iterrows():\n",
" sf = r[\"pred\"] * (corr[r[\"grp\"]] if use_corr else 1.0) / (r[\"load_N\"] / (r[\"min_wall_mm\"] * np.sqrt(r[\"max_dim_mm\"])))\n",
" out.append(outcome(r, bool(r[\"rule_ok\"] and sf >= thr)))\n",
" return pd.DataFrame(out, columns=[\"cost\", \"days\", \"printed\", \"real_ok\"]).join(req[[\"scrap_rate\", \"demand_month\", \"crit\"]])\n",
"\n",
"\n",
"base = pd.DataFrame([outcome(r, False) for _, r in req.iterrows()], columns=[\"cost\", \"days\", \"printed\", \"real_ok\"])\n",
"res = {\"ํํ ๊ธฐ์ค\": policy(False, SF_MIN), \"์ฌ๋ณด์ ๊ธฐ์ค\": policy(True, thr_new)}\n",
"print(\"\\n[์๊ฐ 250๊ฑด ๋ชจ์: ๊ตฌ๋งค ๊ธฐ์ค ๋๋น]\")\n",
"for k, d in res.items():\n",
" fail = int((d[\"printed\"] & ~d[\"real_ok\"]).sum())\n",
" save, cut_days = base[\"cost\"].sum() - d[\"cost\"].sum(), 1 - d[\"days\"].mean() / base[\"days\"].mean()\n",
" scr = (d.loc[d[\"printed\"] & d[\"real_ok\"], \"scrap_rate\"] * d.loc[d[\"printed\"] & d[\"real_ok\"], \"demand_month\"]).sum() * SCRAP_CUT\n",
" print(f\" {k}: 3D ์ ์ {int(d['printed'].sum())}๊ฑด(๋ถํฉ๊ฒฉ {fail}๊ฑด) | 1) ๋น์ฉ ์ ๊ฐ {save:,.0f}๋ง์({save / base['cost'].sum():.1%}) | 2) ๊ณต๊ธ ์๊ฐ ๋จ์ถ {cut_days:.1%}\"\n",
" f\" ({base['days'].mean():.1f}์ผ -> {d['days'].mean():.1f}์ผ) | 3) ํ๊ธฐ ๊ฐ์ {scr:.1f}๊ฐ/์(๊ฐ์ {SCRAP_CUT:.0%})\")\n",
"print(\"ROI ๋ก๋๋งต: ๋จ๊ธฐ(1~3๊ฐ์) ํด๋ฆฌ๋จธ ๋นํต์ฌ ๋ถํ ์๋ฒ -> ์ค๊ธฐ(6๊ฐ์) ํ๊ดด ์ํ ๊ฒฐ๊ณผ๋ก ๋ถ๊ธฐ๋ณ ๋ณด์ -> ์ฅ๊ธฐ(1๋
) ๊ธ์ ํต์ฌ ๋ถํ ํ๋\")\n",
"print(\"ํ๊ธฐ ๊ฐ์ ์๋์ ๋์ฒด ๊ณต๊ธ์ด ํ๊ธฐ๋ฅผ ์ค์ธ๋ค๋ ๊ฐ์ ๋น์จ์ ๋ฐ๋ฅธ ์ถ์ ์ด๋ฉฐ, ๋ชจ๋ ๊ธ์ก์ ๊ต์ก์ฉ ๊ฐ์ ๋จ๊ฐ์ ์๋ ๋น๊ต๊ฐ์ด๋ค. ์ค์ ์ฅ์ฐฉ ์น์ธ์ ์ ๋น๊ธฐ์ ๊ด์ด ํ๋ค.\")\n",
"\n",
"fig, ax = plt.subplots(1, 2, figsize=(11, 4))\n",
"for g_, c in zip([\"ํด๋ฆฌ๋จธ ์์ ๋ฒฝ\", \"๊ธ์ ๋์ถ ๋ง์\", \"์ผ๋ฐ\"], [\"tab:red\", \"tab:orange\", \"tab:blue\"]):\n",
" s = test[test[\"grp\"] == g_]\n",
" ax[0].scatter(s[\"meas\"], s[\"pred\"], s=8, c=c, label={\"ํด๋ฆฌ๋จธ ์์ ๋ฒฝ\": \"thin polymer\", \"๊ธ์ ๋์ถ ๋ง์\": \"metal overhang\", \"์ผ๋ฐ\": \"other\"}[g_])\n",
"ax[0].plot([0, 1000], [0, 1000], \"k--\"); ax[0].set_xlabel(\"measured (MPa)\"); ax[0].set_ylabel(\"predicted\"); ax[0].legend(); ax[0].set_title(\"Prediction vs destructive test\")\n",
"ax[1].bar([\"current\", \"recalibrated\"], [unsafe_rate(test, False, SF_MIN)[0] * 100, unsafe_rate(test, True, thr_new)[0] * 100]); ax[1].set_ylabel(\"unsafe approval (%)\"); ax[1].set_title(\"Unsafe approval rate (hold-out)\")\n",
"plt.tight_layout(); plt.show()\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"๐ก [์ฐธ๊ณ ] ๋ฏธ์
09 ์์ ์คํ ๊ฒฐ๊ณผ (Output)
\n",
"\n",
"```text\n",
"[๊ต์ก์ฉ๊ฐ์์๋ฃ] AAR: ๊ณผ๊ฑฐ ์ํ 250์ข
์ค ์์ธก์ด ์ค์ธก๋ณด๋ค 10% ์ด์ ๋์๋ ์ํ ์ฌ๋ก 110๊ฑด (ํ๊ท ์ค์ฐจ +9.8%, MAPE 12.0%)\n",
"์์ธ\n",
"๊ธ์ ์๋ฅ์๋ ฅ(๋ชจ๋ธ ๋ฏธ๋ฐ์) 54\n",
"์์ ๋ฒฝ ๋ฏธ์ถฉ์ (๋ชจ๋ธ ๋ฏธ๋ฐ์) 34\n",
"์ธ์ฆ์ ์๋ ์์ฌ ํธ์ฐจ 15\n",
"๊ธฐํ(์ํ ์ฐํฌ) 7\n",
"-> ์์ธ ๋๋ถ๋ถ์ด ๋ชจ๋ธ์ ์๋ ํ์ยท๊ณต์ ํจ๊ณผ๋ผ ์น์ธ ๊ธฐ์ค ๋ณด์ ์ผ๋ก ์ค์ผ ์ ์๋ค. '๊ธฐํ'๋ ์ํ ์ฐํฌ๋ผ ๋ณด์ ์ด ์๋ ์์ ์จ ์ฌ์ ๋ก ๋์ํ๋ค.\n",
"\n",
"๋ณด์ ๊ณ์(์ค์ธก/์์ธก ์ค์๊ฐ): {'๊ธ์ ๋์ถ ๋ง์': 0.831, '์ผ๋ฐ': 0.98, 'ํด๋ฆฌ๋จธ ์์ ๋ฒฝ': 0.826}\n",
"๊ณผ๊ฑฐ ์ํ์์ ์ํ ์น์ธ ๋น์จ 2% ์ดํ๋ฅผ ๋ง์กฑํ๋ ์ต์ ์๊ณ ์์ ์จ: 1.50 (๊ณ์ ๋ณด์ ํ์๋ ๊ธฐ๋ณธ 1.5๋ก ์ถฉ๋ถ)\n",
"\n",
"[์ดํ ์ํ 250์ข
๊ฒ์ฆ]\n",
" ๋ฐฉ์ ์ํ ์น์ธ ๋น์จ ์น์ธ ๊ฐ๋ฅ ๋ถํ\n",
"ํํ(์์ธก ์์ ์จ 1.5 ์ด์) 7.5% 159\n",
" ๋ณด์ ๋ง(๊ณ์ ์ ์ฉ) 2.6% 151\n",
"์ฌ๋ณด์ (๊ณ์ + ์๊ณ 1.50) 2.6% 151\n",
"๊ฐ๋ ์์ธก MAPE: ๋ณด์ ์ 11.2% -> ๋ณด์ ํ 6.5%\n",
"\n",
"[์๊ฐ 250๊ฑด ๋ชจ์: ๊ตฌ๋งค ๊ธฐ์ค ๋๋น]\n",
" ํํ ๊ธฐ์ค: 3D ์ ์ 130๊ฑด(๋ถํฉ๊ฒฉ 2๊ฑด) | 1) ๋น์ฉ ์ ๊ฐ 37,491๋ง์(29.0%) | 2) ๊ณต๊ธ ์๊ฐ ๋จ์ถ 52.5% (40.9์ผ -> 19.5์ผ) | 3) ํ๊ธฐ ๊ฐ์ 201.5๊ฐ/์(๊ฐ์ 30%)\n",
" ์ฌ๋ณด์ ๊ธฐ์ค: 3D ์ ์ 129๊ฑด(๋ถํฉ๊ฒฉ 1๊ฑด) | 1) ๋น์ฉ ์ ๊ฐ 37,822๋ง์(29.3%) | 2) ๊ณต๊ธ ์๊ฐ ๋จ์ถ 52.5% (40.9์ผ -> 19.4์ผ) | 3) ํ๊ธฐ ๊ฐ์ 201.5๊ฐ/์(๊ฐ์ 30%)\n",
"ROI ๋ก๋๋งต: ๋จ๊ธฐ(1~3๊ฐ์) ํด๋ฆฌ๋จธ ๋นํต์ฌ ๋ถํ ์๋ฒ -> ์ค๊ธฐ(6๊ฐ์) ํ๊ดด ์ํ ๊ฒฐ๊ณผ๋ก ๋ถ๊ธฐ๋ณ ๋ณด์ -> ์ฅ๊ธฐ(1๋
) ๊ธ์ ํต์ฌ ๋ถํ ํ๋\n",
"ํ๊ธฐ ๊ฐ์ ์๋์ ๋์ฒด ๊ณต๊ธ์ด ํ๊ธฐ๋ฅผ ์ค์ธ๋ค๋ ๊ฐ์ ๋น์จ์ ๋ฐ๋ฅธ ์ถ์ ์ด๋ฉฐ, ๋ชจ๋ ๊ธ์ก์ ๊ต์ก์ฉ ๊ฐ์ ๋จ๊ฐ์ ์๋ ๋น๊ต๊ฐ์ด๋ค. ์ค์ ์ฅ์ฐฉ ์น์ธ์ ์ ๋น๊ธฐ์ ๊ด์ด ํ๋ค.\n",
"```\n",
" \n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## ๐ฏ ๋ฏธ์
10. 3D ํ๋ฆฐํ
์ผ์ ๊ธด๊ธ ๋ถํ ์ ์ ํ๋น์ฑ ๋ฐ ์ ๋ขฐ์ฑ ํ๊ฐ ๋๊ตฌ ์ต์ข
์์ฑ\n",
"**์์์๊ฐ**: ์ฝ 3.5์๊ฐ\n",
"\n",
"### ๐ ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ์ํ ์ง์นจ\n",
"#### ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ๋น์์ฑ\n",
"\n",
"3D ํ๋ฆฐํ
์ผ์ ๊ธด๊ธ ๋ถํ ์ ์ ํ๋น์ฑ ๋ฐ ์ ๋ขฐ์ฑ ํ๊ฐ ๋๊ตฌ๋ฅผ ์์ฑํ๋ค. ์ค์ ์ ์ ํ์ฅ์์๋ ๋จ์ ํ์ง์ ๊ทธ์น์ง ์๊ณ ์ ์ํ ์ํ ํ๋จ๊ณผ ์ ๋ขฐ๋ ๋์ ์์ฌ๊ฒฐ์ ์ด ํ์์ ์ด๋ฏ๋ก, **์์ฒญ ๊ฒ์ฆ๋ถํฐ ์์ธก ๋ชจ๋ธ ์ฌํ์ต, ์์ ์จยท์ ์ ๊ธฐ๋ฐ ํ๋ณด ์ ๋ณ, ์น์ธ ๋๊ธฐ์ด ์์ฑ๊น์ง 3D ํ๋ฆฐํ
์ผ์ ๊ธด๊ธ ๋ถํ ์ ์ ํ๋น์ฑยท์ ๋ขฐ์ฑ ํ๊ฐ ๋๊ตฌ๋ฅผ ํตํฉํ๊ณ ์ด์ ๋ฌธ์๋ฅผ ์๋นํ๋ค.** ๊ณผ์
์ ์์ํ์ฌ ์คํ์ ๋ฐฉ์งํ๊ณ ์์ ๋์๋ ฅ์ ๊ทน๋ํํด์ผ ํฉ๋๋ค.\n",
"\n",
"์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ณธ ๋ฏธ์
์์๋ **์ ๊ท ํ๊ดด ์ํ์ผ๋ก ๋์ ์ ๋ชจ๋ธ์ ํ์ตํด ์ฑํผ์ธ ๋ชจ๋ธ๊ณผ ๊ฒ์ฆ MAPE๋ฅผ ๋น๊ตํ ๋ค ๊ต์ฒด ์ฌ๋ถ๋ฅผ ์ ํ๋ ์๋ ์ฌํ์ต ํ์ดํ๋ผ์ธ ๊ตฌํํ๊ณ ๋์์ ๊ฒ์ฆํ๋** ์ค์ต์ ์ฒด๊ณ์ ์ผ๋ก ์ํํฉ๋๋ค.\n",
"\n",
"ํ์ต์๋ ์ค์ต๊ฐ์ด๋์ ์ ์๋ ์ด 4๊ฐ ์ธ๋ถ ํ๋(๊ธฐ๋ณธ ๊ธฐ๋ฅ ๊ตฌํ โ ๊ฒฐ๊ณผ ์งํ ๊ฒ์ฆ โ ํ๋ผ๋ฏธํฐ ํ๋ ๋ฐ ๋์ ๊ณผ์ )์ ์์ฐจ์ ์ผ๋ก ์งํํ๋ฉฐ ํต์ฌ ํ์ ๊ธฐ์ค์ ๋ฌ์ฑํฉ๋๋ค.\n",
"\n",
"**์ด๋ฒ ๊ฒฐ๊ณผ๋ฌผ:** `docs/mnt06_operations_guide.md ๋ฐ pipeline/mnt06_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": [
"\"\"\"๋ถํ ์์ฒญ ๊ฒ์ฆยท์์ ๋ ์ฝ๋ ๋ถ๋ฆฌ, ๊ฐ๋ ์์ธก ๋ชจ๋ธ์ ์๋ ์ฌํ์ต(์ฑํผ์ธยท๋์ ์ ๋น๊ต ํ ๊ต์ฒด), ๊ตฐ ๊ท๊ฒฉ ๋ถํฉ ์ ์์ ์์ ์จ ๊ธฐ๋ฐ ๋์ฒด ์ ์ ํ๋ณด ์ ๋ณ, ์ ๋น๊ธฐ์ ๊ด ์น์ธ ๋๊ธฐ์ด ์์ฑ๊น์ง ์ผ์ 3D ํ๋ฆฐํ
์ ์ ํ๋น์ฑยท์ ๋ขฐ์ฑ ํ๊ฐ ๋๊ตฌ๋ฅผ ํ ํ์ผ๋ก ํตํฉ ์คํํ๊ณ , ์์ฒด ์ ๊ฒยท์ด์ ๋ฌธ์ ์๋น ์ ๊ฒยท์ ๋น๊ธฐ์ ๊ด ๋ธ๋ฆฌํ ์์ฝ์ ์ถ๋ ฅํ๋ค.\"\"\"\n",
"import time\n",
"import numpy as np\n",
"import pandas as pd\n",
"import matplotlib.pyplot as plt\n",
"from sklearn.ensemble import GradientBoostingRegressor\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# [๊ต์ก์ฉ๊ฐ์์๋ฃ] ์ผ์ 3D ํ๋ฆฐํ
๋์ฒด ์ ์ ํ๋ณด ๋ถํ ์์ฑ๊ธฐ (์ค์ ๋ถํยท์ฌ๊ณ ยท๊ท๊ฒฉ ๊ฐ ์๋)\n",
"# ํ๊ธฐ์จ์ด ๋์ ์๋ฆฌ๋ถ์ ์ค 3D ํ๋ฆฐํ
์ผ๋ก ๋์ฒด ์ ์ํ ์ ์๋์ง ํ๋จํ๊ธฐ ์ํ ๊ฐ์ ๋ถํ ์๋ฃ๋ฅผ ๋ง๋ ๋ค.\n",
"# ์์ฌ ๋ฌผ์ฑยท์น์ยทํ์ค์ ๊ต์ก์ฉ์ผ๋ก ๋จ์ํํ ๊ฐ์ด๋ฉฐ ์ค์ ์ํ ์ฑ์ ์๋ ๊ตฐ ๊ท๊ฒฉ ๊ฐ๊ณผ ๋ฌด๊ดํ๋ค.\n",
"# ---------------------------------------------------------------------------\n",
"MAT = { # ์์ฌ: (๊ณต์ , ๊ธฐ์ค ์ธ์ฅ๊ฐ๋ MPa, ๋ฐ๋ g/cm3, ์์ฌ ๋จ๊ฐ ๋ง์/kg, ์ถ๋ ฅ๋น ๋ง์/h, ์ต์ ๋ฒฝ๋๊ป mm, ์ถ๋ ฅ ์๋ cm3/h)\n",
" \"PA12\": (\"polymer\", 45, 1.0, 8, 3, 0.8, 18), \"PETG\": (\"polymer\", 50, 1.27, 5, 2, 0.8, 15),\n",
" \"CF-PA\": (\"polymer\", 70, 1.2, 14, 3, 1.0, 12), \"AlSi10Mg\": (\"metal\", 350, 2.67, 30, 25, 0.5, 4),\n",
" \"SS316L\": (\"metal\", 520, 7.9, 28, 25, 0.6, 3.5), \"Ti64\": (\"metal\", 900, 4.4, 90, 30, 0.6, 3)}\n",
"CATS = [\"bracket\", \"housing\", \"clip\", \"gear\", \"duct\", \"handle\"]\n",
"CAT_SCRAP = {\"bracket\": 0.04, \"housing\": 0.05, \"clip\": 0.12, \"gear\": 0.09, \"duct\": 0.08, \"handle\": 0.10} # ๋ถํ๊ตฐ๋ณ ๊ธฐ๋ณธ ์ ํ๊ธฐ์จ\n",
"BUILD_MM = {\"polymer\": 300, \"metal\": 200} # ๊ฐ์ ์ถ๋ ฅ ๊ฐ๋ฅ ์ต๋ ์น์\n",
"\n",
"\n",
"def make_parts(n=300, seed=0):\n",
" \"\"\"๊ฐ์ ๋ถํ n์ข
: ํ์(๋ถํผยท๋ฒฝ๋๊ปยท๋์ถ ๋น์จ), ์์ฌ, ์ฌ์ฉ ํ์ค, ํ๊ธฐ์จ๊ณผ ์กฐ๋ฌ ์ ๋ณด๋ฅผ ๋ง๋ ๋ค.\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" mat = rng.choice(list(MAT), n, p=[0.2, 0.15, 0.1, 0.2, 0.2, 0.15])\n",
" proc = np.array([MAT[m][0] for m in mat])\n",
" vol = np.clip(rng.lognormal(np.log(30), 0.9, n), 2, 800)\n",
" aspect = rng.lognormal(0.3, 0.4, n)\n",
" max_dim = 10 * vol ** (1 / 3) * aspect ** (1 / 3) * rng.uniform(1.0, 1.6, n)\n",
" wall = np.clip(rng.lognormal(np.log(2.0), 0.6, n), 0.3, 8)\n",
" ovh = rng.beta(2, 5, n)\n",
" is_poly = proc == \"polymer\"\n",
" df = pd.DataFrame({\n",
" \"part_id\": [f\"P{i:03d}\" for i in range(n)], \"cat\": rng.choice(CATS, n, p=[0.2, 0.15, 0.2, 0.1, 0.15, 0.2]), \"material\": mat, \"process\": proc,\n",
" \"volume_cm3\": vol, \"max_dim_mm\": max_dim, \"area_cm2\": 6 * vol ** (2 / 3) * rng.uniform(1.0, 2.2, n), \"min_wall_mm\": wall, \"overhang\": ovh,\n",
" \"n_holes\": rng.poisson(2.0, n), \"thread\": rng.random(n) < 0.25, \"orient_deg\": rng.uniform(0, 90, n),\n",
" \"layer_mm\": np.where(is_poly, rng.choice([0.1, 0.2, 0.3], n), rng.choice([0.03, 0.05], n)),\n",
" \"infill\": np.where(is_poly, rng.choice([40, 60, 80, 100], n, p=[0.15, 0.3, 0.3, 0.25]), 100),\n",
" \"load_N\": np.clip(rng.lognormal(np.where(is_poly, np.log(230), np.log(2300)), 0.7, n), 10, 20000), \"temp_C\": rng.uniform(-20, 90, n),\n",
" \"crit\": rng.choice([1, 2, 3], n, p=[0.35, 0.4, 0.25]), \"cert\": rng.choice([0, 1], n, p=[0.45, 0.55]), # ์์ฌ ์ธ์ฆ์ ๋ณด์ \n",
" \"import_part\": rng.random(n) < 0.4})\n",
" df[\"scrap_rate\"] = np.clip(df[\"cat\"].map(CAT_SCRAP) * rng.lognormal(0, 0.35, n) * (1 + 0.5 * (df[\"load_N\"] > 800)) * (1 + 0.3 * (df[\"temp_C\"] > 60)), 0.005, 0.5)\n",
" df[\"lead_days\"] = np.clip(rng.lognormal(np.log(25), 0.5, n) * np.where(df[\"import_part\"], 2.2, 1.0), 5, 300)\n",
" # ---- ์จ์ ์ง์ค๊ฐ(์ค์ ์ํ ๊ฒฐ๊ณผ์ ํด๋น): ๋ชจ๋ธ์ ์ ์
๋ ฅ์ผ๋ก ์ด๊ฒ์ ์์ธกํ๋ค ----\n",
" eff = np.where(is_poly, 0.55 + 0.45 * df[\"infill\"] / 100, 1 - 0.3 * ovh * 0.2)\n",
" aniso = np.where(is_poly, 1 - 0.3 * np.sin(np.radians(df[\"orient_deg\"])) ** 2, 1 - 0.08 * np.sin(np.radians(df[\"orient_deg\"])))\n",
" heat = np.where(is_poly, 1 - 0.35 * np.clip((df[\"temp_C\"] - 40) / 80, 0, 1), 0.98)\n",
" base = np.array([MAT[m][1] for m in mat])\n",
" df[\"strength_mpa\"] = base * eff * aniso * heat * rng.lognormal(0, 0.06, n)\n",
" cross = df[\"min_wall_mm\"] * np.sqrt(df[\"max_dim_mm\"]) * 1.0 # ํ์ค ์ง์ง ๋จ๋ฉด์ mm2(๋จ์ํ)\n",
" df[\"stress_mpa\"] = df[\"load_N\"] / cross\n",
" df[\"safety\"] = df[\"strength_mpa\"] / df[\"stress_mpa\"]\n",
" ratio = np.clip(df[\"stress_mpa\"] / df[\"strength_mpa\"], 0, 1.5)\n",
" df[\"log_cycles\"] = np.clip(7.5 - 6.0 * ratio + rng.normal(0, 0.25, n), 2, 7.5) # ํผ๋ก ์๋ช
log10(ํ)\n",
" support = 1 + 0.5 * ovh\n",
" df[\"build_h\"] = vol * support / np.array([MAT[m][6] for m in mat]) + 0.5\n",
" mass_kg = vol * np.array([MAT[m][2] for m in mat]) * support / 1000\n",
" df[\"print_cost\"] = mass_kg * np.array([MAT[m][3] for m in mat]) + df[\"build_h\"] * np.array([MAT[m][4] for m in mat]) + 3\n",
" df[\"buy_cost\"] = df[\"print_cost\"] * rng.lognormal(0.25, 0.45, n) + 5 # ๊ตฌ๋งค ๋จ๊ฐ(๋ง์, ๊ฐ์)\n",
" wall_ok = df[\"min_wall_mm\"] >= np.array([MAT[m][5] for m in mat]) * rng.uniform(0.95, 1.25, n)\n",
" dim_ok = df[\"max_dim_mm\"] <= np.where(is_poly, BUILD_MM[\"polymer\"], BUILD_MM[\"metal\"])\n",
" warp = (~is_poly) & (df[\"max_dim_mm\"] > 140) & (df[\"overhang\"] > 0.3) # ๊ธ์ ๋ํ ์ค๋ฒํ ๋ณํ\n",
" thr_bad = df[\"thread\"] & (df[\"min_wall_mm\"] < 1.5) & is_poly # ์์ ๋์ฌ๋ถ ํ์\n",
" df[\"printable\"] = wall_ok & dim_ok & (ovh < 0.5) & ~warp & ~thr_bad\n",
" risk = np.clip(1 - df[\"safety\"] / 3, 0, 1)\n",
" df[\"conform\"] = np.clip(100 * (0.45 * np.minimum(df[\"safety\"] / 3, 1) + 0.25 * np.clip((df[\"log_cycles\"] - 3) / 4.5, 0, 1)\n",
" + 0.15 * (1 - np.clip(ovh * 1.2, 0, 1)) + 0.15 * df[\"cert\"]) - 8 * risk * df[\"crit\"] / 3 + rng.normal(0, 2, n), 0, 100)\n",
" df[\"demand_month\"] = rng.poisson(np.where(df[\"cat\"].isin([\"clip\", \"handle\"]), 40, 15)) + 1 # ์ ์์์๋(๊ฐ์)\n",
" df[\"scrap_cost\"] = df[\"scrap_rate\"] * df[\"demand_month\"] * df[\"buy_cost\"] # ์ ํ๊ธฐ ์์ค์ก(๋ง์, ๊ฐ์)\n",
" return df\n",
"\n",
"# ---- ๋จ์ํํ ํด์(FEA ๋์ฉ) ๊ฐ๊ณผ ํ์ ํผ์ฒ: ์
๋ ฅ ์ ๋ณด๋ง์ผ๋ก ๊ณ์ฐํ๋ค(์ํ ๊ฒฐ๊ณผ ๋์ค ์์) ----\n",
"def make_features(d):\n",
" \"\"\"๊ฐ์ด ์๋ ฅ ํด์(์๋ ฅ์ง์ค๊ณ์ ํฌํจ)๊ณผ ์์ฌยท๊ณต์ ์ง์์์ ๋ง๋ ํ์ ํผ์ฒ 16๊ฐ.\"\"\"\n",
" X = pd.DataFrame(index=d.index)\n",
" base = d[\"material\"].map(lambda m: MAT[m][1]).astype(float)\n",
" X[\"base_strength\"] = base\n",
" X[\"is_metal\"] = (d[\"process\"] == \"metal\").astype(int)\n",
" X[\"cross_mm2\"] = d[\"min_wall_mm\"] * np.sqrt(d[\"max_dim_mm\"])\n",
" X[\"kt\"] = 1 + 0.15 * d[\"n_holes\"] + 0.5 * d[\"thread\"].astype(int) # ๊ตฌ๋ฉยท๋์ฌ๋ถ ์๋ ฅ์ง์ค๊ณ์(๊ฐ์ด)\n",
" X[\"fea_stress\"] = d[\"load_N\"] / X[\"cross_mm2\"] * X[\"kt\"] # ๊ฐ์ด ํด์ ์๋ ฅ(MPa)\n",
" X[\"stress_ratio\"] = X[\"fea_stress\"] / base # ๊ธฐ์ค๊ฐ๋ ๋๋น ์๋ ฅ๋น\n",
" X[\"infill_eff\"] = np.where(X[\"is_metal\"] == 1, 1.0, 0.55 + 0.45 * d[\"infill\"] / 100)\n",
" X[\"orient_sin2\"] = np.sin(np.radians(d[\"orient_deg\"])) ** 2\n",
" X[\"temp_derate\"] = np.where(X[\"is_metal\"] == 1, 0.0, np.clip((d[\"temp_C\"] - 40) / 80, 0, 1))\n",
" X[\"overhang_layer\"] = d[\"overhang\"] * d[\"layer_mm\"]\n",
" X[\"slender\"] = d[\"max_dim_mm\"] / d[\"min_wall_mm\"]\n",
" X[\"surf_vol\"] = d[\"area_cm2\"] / d[\"volume_cm3\"]\n",
" X[\"wall_ratio\"] = d[\"min_wall_mm\"] / d[\"material\"].map(lambda m: MAT[m][5])\n",
" X[\"load_per_vol\"] = d[\"load_N\"] / d[\"volume_cm3\"]\n",
" X[\"cert\"] = d[\"cert\"].astype(int)\n",
" X[\"crit\"] = d[\"crit\"]\n",
" return X\n",
"\n",
"# ---- ๋์ฒด ์ ์ ์์ฒญ ๋น์ฉ ๋ชจ์(๊ฐ์ ๋จ๊ฐ): ๊ตฌ๋งค ๋๊ธฐ vs 3D ํ๋ฆฐํ
----\n",
"DOWN = {1: 0.5, 2: 2.0, 3: 5.0} # [๊ต์ก์ฉ๊ฐ์์๋ฃ] ๋ถํ ๋ถ์ฌ๋ก ์ธํ ์ฅ๋น ๊ฐ๋์ค๋จ ๋น์ฉ(๋ง์/์ผ), ์ค์๋๋ณ\n",
"FAIL = {1: 10.0, 2: 40.0, 3: 150.0} # [๊ต์ก์ฉ๊ฐ์์๋ฃ] ๋์ฒดํ ์ฅ์ฐฉ ํ ๋ถํฉ๊ฒฉ ์ ์ฌ์์
ยท์๋ฌด ์ํฅ ๋น์ฉ(๋ง์)\n",
"POST = 3.0 # ํ์ฒ๋ฆฌยท๊ฒ์ฌ ๋น์ฉ(๋ง์)\n",
"\n",
"\n",
"def make_requests_month(catalog, n=250, seed=0):\n",
" \"\"\"์๊ฐ ๋ถํ ์์ฒญ: ์ ์์์๋์ด ํฐ ๋ถํ์ด ๋ ์์ฃผ ์์ฒญ๋๋ค.\"\"\"\n",
" return catalog.sample(n, replace=True, weights=catalog[\"demand_month\"], random_state=seed).reset_index(drop=True)\n",
"\n",
"\n",
"def buy_cost(r):\n",
" return r[\"buy_cost\"] + r[\"lead_days\"] * DOWN[r[\"crit\"]]\n",
"\n",
"\n",
"def print_outcome(r, delay_d=1.0):\n",
" \"\"\"์ค์ ์ํ ๊ฒฐ๊ณผ(์จ์ ์ง์ค)์ ๋ฐ๋ฅธ ๋์ฒด ์ ์ ๋น์ฉ. ๋ถํฉ๊ฒฉ์ด๋ฉด ๊ตฌ๋งค๋ก ๋๋์๊ฐ๊ณ ๋ถํฉ๊ฒฉ ๋น์ฉ์ด ๋ ๋ค.\"\"\"\n",
" wait = (r[\"build_h\"] / 24 + delay_d + 2.0) * DOWN[r[\"crit\"]]\n",
" ok = bool(r[\"printable\"] and r[\"conform\"] >= 70)\n",
" c = r[\"print_cost\"] + POST + wait\n",
" return (c, True) if ok else (c + buy_cost(r) + FAIL[r[\"crit\"]], False)\n",
"\n",
"CONFIG = {\"sf_min\": 1.5, \"score_min\": 70.0, \"retrain_tolerance\": 0.02, \"scrap_cut\": 0.30,\n",
" \"corr\": {\"ํด๋ฆฌ๋จธ ์์ ๋ฒฝ\": 0.83, \"๊ธ์ ๋์ถ ๋ง์\": 0.83, \"์ผ๋ฐ\": 0.98}} # ๋ฏธ์
9 ์ฌ๋ณด์ ๊ฒฐ๊ณผ(๋ชจ๋ธ์ ๊ต์ฒดํ์ง ์์ ๋๋ง ์ ์ฉ)\n",
"OPS_GUIDE = { # docs/mnt06_operations_guide.md ์์ฝ\n",
" \"1. ๋ชฉ์ ๊ณผ ๋ฒ์\": \"ํ๊ธฐ์จ์ด ๋์ ์๋ฆฌ๋ถ์ ์ค ์ผ์ 3D ํ๋ฆฐํ
๋์ฒด ์ ์ ํ๋ณด ์ ๋ณ. ์ฅ์ฐฉ ์น์ธ์ ์ ๋น๊ธฐ์ ๊ด.\",\n",
" \"2. ์
๋ ฅ ๋ฐ์ดํฐ\": \"๋ถํ(CAD ์์ฝ ์น์ยท์์ฌยทํ์คยทํ๊ธฐ์จยท์กฐ๋ฌ์ผ), ํ๊ดด ์ํ ๊ฒฐ๊ณผ. ์ค์ ๋ถํยท์ฌ๊ณ ์ ๋ณด๋ ์
๋ ฅํ์ง ์๋๋ค.\",\n",
" \"3. ์ผ์ผ ์ด์ฉ ์ ์ฐจ\": \"์์ฒญ ๊ฒ์ฆ -> ํ๋ณด ์ ๋ณ -> ์น์ธ ๋๊ธฐ์ด -> ์ ๋น๊ธฐ์ ๊ด ๊ฒฐ์ -> ์ถ๋ ฅยทํ์ฒ๋ฆฌ -> ์ํ ๊ฒฐ๊ณผ ์
๋ ฅ.\",\n",
" \"4. ์น์ธ(HITL) ๊ธฐ์ค\": \"๋ชจ๋ ๋์ฒด ์ ์์ ์ ๋น๊ธฐ์ ๊ด ์น์ธ, ํต์ฌ ๋ถํ์ ์์ฌ ์ธ์ฆ์ ํ๋ณด, ๋ฌด์๋ต ์ ๊ธฐ์กด ์กฐ๋ฌ ์ ์ง, ์ฌ์ ๊ธฐ๋ก.\",\n",
" \"5. ์ฌํ์ต ์ ์ฐจ\": \"๋ถ๊ธฐ 1ํ ์ ๊ท ํ๊ดด ์ํ์ผ๋ก ๋์ ์ ๋ชจ๋ธ ํ์ต, ๊ฒ์ฆ MAPE๊ฐ ๋๋น ์ง์ง ์์ ๋๋ง ๊ต์ฒด.\",\n",
" \"6. ์ฅ์ ๋์\": \"๋ชจ๋ธ ์ค๋ฅ ์ ์๋ ฅ/๊ธฐ์ค๊ฐ๋ ๋น ๊ฐ์ด ๊ท์น์ผ๋ก ๋์ฒด, ์์ ๋ ์ฝ๋๋ ์๋ ํ์ธ ๋ชฉ๋ก์ผ๋ก.\",\n",
" \"7. ์ฐ๋ ์ธํฐํ์ด์ค\": \"๊ตญ๋ฐฉ ๊ตฐ์ ์ ๋ณด์ฒด๊ณ์ ๋ถํ ์๋ณ์ฝ๋ยท๋ถ๋ฅ๊ตฐยทํ๊ธฐ์๋ ํ์ค ํ๋๋ฅผ CSV/API๋ก ๊ตํํ๋ ์์ ์ฐจ๊ธฐ ๊ณผ์ ๋ก ๋๋ค.\",\n",
"}\n",
"\n",
"\n",
"def destructive_test(d, seed):\n",
" \"\"\"[๊ต์ก์ฉ๊ฐ์์๋ฃ] ์ค๋ฌผ ํ๊ดด ์ํ ๊ฐ๋(๋ชจ๋ธ์ด ๋ชจ๋ฅด๋ ํ์ยท๊ณต์ ํจ๊ณผ ํฌํจ).\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" f = np.where((d[\"process\"] == \"polymer\") & (d[\"min_wall_mm\"] < 1.5), 0.86, 1.0) * np.where((d[\"process\"] == \"metal\") & (d[\"overhang\"] > 0.3), 0.88, 1.0) \\\n",
" * np.where(d[\"cert\"] == 0, 0.94, 1.0)\n",
" return d[\"strength_mpa\"] * f * rng.lognormal(0, 0.03, len(d))\n",
"\n",
"\n",
"def group(d):\n",
" return np.select([(d[\"process\"] == \"polymer\") & (d[\"min_wall_mm\"] < 1.5), (d[\"process\"] == \"metal\") & (d[\"overhang\"] > 0.3)], [\"ํด๋ฆฌ๋จธ ์์ ๋ฒฝ\", \"๊ธ์ ๋์ถ ๋ง์\"], \"์ผ๋ฐ\")\n",
"\n",
"\n",
"def validate(df):\n",
" ok = (df[\"min_wall_mm\"] > 0) & (df[\"max_dim_mm\"] > 0) & df[\"material\"].isin(list(MAT)) & df[\"load_N\"].notna()\n",
" return df[ok].copy(), df[~ok].copy()\n",
"\n",
"\n",
"def gb():\n",
" return GradientBoostingRegressor(n_estimators=150, max_depth=3, learning_rate=0.06, random_state=0)\n",
"\n",
"\n",
"def fit_strength(d):\n",
" X = make_features(d)\n",
" return gb().fit(X, d[\"meas\"] / X[\"base_strength\"])\n",
"\n",
"\n",
"def pred_strength(model, d):\n",
" X = make_features(d)\n",
" return model.predict(X) * X[\"base_strength\"]\n",
"\n",
"\n",
"def mape(a, p):\n",
" return float(np.mean(np.abs(np.asarray(a) - p) / np.asarray(a)))\n",
"\n",
"\n",
"def retrain(champion, train_df, new_df):\n",
" \"\"\"๋์ ์ ๋ชจ๋ธ์ (๊ธฐ์กด+์ ๊ท ์ 2/3)๋ก ํ์ต, ์ ๊ท ๋ค 1/3์์ ์ฑํผ์ธ๊ณผ ๋น๊ตํด ๋ ๋ซ๊ฑฐ๋ ๋น์ทํ๋ฉด ๊ต์ฒด.\"\"\"\n",
" cut = int(len(new_df) * 2 / 3)\n",
" hold = new_df.iloc[cut:]\n",
" challenger = fit_strength(pd.concat([train_df, new_df.iloc[:cut]], ignore_index=True))\n",
" m_old, m_new = mape(hold[\"meas\"], pred_strength(champion, hold)), mape(hold[\"meas\"], pred_strength(challenger, hold))\n",
" swap = m_new <= m_old + CONFIG[\"retrain_tolerance\"]\n",
" return (challenger if swap else champion), {\"์ฑํผ์ธ MAPE\": round(m_old, 3), \"๋์ ์ MAPE\": round(m_new, 3), \"๊ต์ฒด\": swap}\n",
"\n",
"\n",
"def stress(d):\n",
" return d[\"load_N\"] / (d[\"min_wall_mm\"] * np.sqrt(d[\"max_dim_mm\"]))\n",
"\n",
"\n",
"# ---------------- 1. ํตํฉ ์คํ ----------------\n",
"t_start = time.perf_counter()\n",
"past = make_parts(800, seed=11); past[\"meas\"] = past[\"strength_mpa\"] # ๊ณผ๊ฑฐ ์ํ(๋ชจ๋ธ์ด ๋ชจ๋ฅด๋ ํจ๊ณผ ์์)\n",
"new = make_parts(300, seed=12); new[\"meas\"] = destructive_test(new, 14) # ์ด๋ฒ ๋ถ๊ธฐ ์ ๊ท ํ๊ดด ์ํ\n",
"champion = fit_strength(past)\n",
"model, rt = retrain(champion, past, new)\n",
"print(f\"[๊ต์ก์ฉ๊ฐ์์๋ฃ] ์ฌํ์ต: {rt} -> {'๋์ ์ ๊ต์ฒด' if rt['๊ต์ฒด'] else '์ฑํผ์ธ ์ ์ง'}\")\n",
"Xp = make_features(past)\n",
"score_model = gb().fit(Xp, past[\"conform\"])\n",
"nxt = make_parts(250, seed=22) # ๋ค์ ๋ถ๊ธฐ ๋์ฒด ์ ์ ์์ฒญ ํ๋ณด\n",
"nxt.loc[[5, 40], \"min_wall_mm\"] = [-1.0, 0.0] # ์์ ๋ ์ฝ๋ 2๊ฑด ์ฃผ์
\n",
"ops, manual = validate(nxt)\n",
"ops = ops.reset_index(drop=True)\n",
"ops[\"pred\"] = pred_strength(model, ops)\n",
"ops[\"grp\"] = group(ops)\n",
"if not rt[\"๊ต์ฒด\"]:\n",
" ops[\"pred\"] *= ops[\"grp\"].map(CONFIG[\"corr\"])\n",
"ops[\"sf\"] = ops[\"pred\"] / stress(ops)\n",
"ops[\"score\"] = np.clip(score_model.predict(make_features(ops)), 0, 100)\n",
"ops[\"rule_ok\"] = (ops[\"min_wall_mm\"] >= 1.0) & (ops[\"max_dim_mm\"] <= ops[\"process\"].map(BUILD_MM)) & (ops[\"overhang\"] < 0.5)\n",
"cand = ops[ops[\"rule_ok\"] & (ops[\"sf\"] >= CONFIG[\"sf_min\"]) & (ops[\"score\"] >= CONFIG[\"score_min\"])].copy()\n",
"cand[\"status\"] = \"PENDING\" # ์ ๋น๊ธฐ์ ๊ด ๊ฒฐ์ ์ ๊น์ง ํ์ ํ์ง ์๋๋ค\n",
"cand[\"need_cert\"] = (cand[\"crit\"] == 3) & (cand[\"cert\"] == 0)\n",
"sec = time.perf_counter() - t_start\n",
"print(f\"ํ๋ณด ์์ฒญ {len(nxt)}๊ฑด: ํ๊ฐ {len(ops)}๊ฑด, ์๋ ํ์ธ {len(manual)}๊ฑด, ๋์ฒด ์ ์ ํ๋ณด {len(cand)}๊ฑด, ์ ์ฒด ์คํ {sec:.1f}์ด\")\n",
"\n",
"# ---------------- 2. ์์ฒด ์ ๊ฒ ----------------\n",
"try:\n",
" bad_pred = pred_strength(None, ops.head(3))\n",
" fallback = \"๊ฐ์ด ๊ท์น ์ ํ ๋ถํ์\"\n",
"except Exception:\n",
" fallback = \"๊ฐ์ด ๊ท์น ๋์ฒด\"\n",
"simple = np.clip(100 * (1 - 1.8 * make_features(ops.head(3))[\"stress_ratio\"]), 0, 100)\n",
"checks = {\n",
" \"์์ฒญ ์ ์ค 0๊ฑด\": len(ops) + len(manual) == len(nxt),\n",
" \"์์ ๋ ์ฝ๋ ์๋ ํ์ธ ๋ถ๋ฆฌ\": len(manual) == 2,\n",
" \"์ฌํ์ต ํ์ดํ๋ผ์ธ ์คํ\": \"๊ต์ฒด\" in rt,\n",
" \"์์ธก ๊ฐ๋ ์์\": bool((ops[\"pred\"] > 0).all()),\n",
" \"๋ชจ๋ธ ์ฅ์ ์ ๊ฐ์ด ๊ท์น ๋์ฒด\": fallback == \"๊ฐ์ด ๊ท์น ๋์ฒด\" and bool(np.isfinite(simple).all()),\n",
" \"๋ชจ๋ ํ๋ณด ์ ์ยท์์ ์จ ์ฐ์ถ\": bool(ops[[\"sf\", \"score\"]].notna().all().all()),\n",
" \"ํ๋ณด๋ ์์ ์จ ๊ธฐ์ค ์ด์\": bool((cand[\"sf\"] >= CONFIG[\"sf_min\"]).all()),\n",
" \"์๋ ์น์ธ 0๊ฑด(์น์ธ ๋์์ ๋๊ธฐ)\": bool((cand[\"status\"] == \"PENDING\").all()),\n",
"}\n",
"print(f\"\\n์์ฒด ์ ๊ฒ {sum(checks.values())}/{len(checks)} ํต๊ณผ, ์๋ ์ฌํ์ต ํ์ดํ๋ผ์ธ {'์ ์ ์คํ' if checks['์ฌํ์ต ํ์ดํ๋ผ์ธ ์คํ'] else '์คํจ'}\")\n",
"for k, v in checks.items():\n",
" if not v:\n",
" print(\" ์คํจ:\", k)\n",
"doc = sum(bool(v.strip()) for v in OPS_GUIDE.values()) / len(OPS_GUIDE)\n",
"print(f\"์ด์ ๋ฌธ์ {len(OPS_GUIDE)}๊ฐ ์ฅ ์์ฑ๋ฅ {doc:.0%} (๋ชฉํ 100%: {'๋ฌ์ฑ' if doc == 1 else '๋ฏธ๋ฌ'}) - \" + \", \".join(k.split(\". \")[1] for k in OPS_GUIDE))\n",
"\n",
"# ---------------- 3. ํ๋ฉด๊ณผ ๋ธ๋ฆฌํ ----------------\n",
"real_ok = (destructive_test(cand, 24) / stress(cand)) >= CONFIG[\"sf_min\"] # ๋ชจ์ ์ ๋ต: ์น์ธ ํ ์ค์ ํ๊ดด ์ํ์ ํ๋ค๋ฉด\n",
"cost_buy = cand.apply(buy_cost, axis=1).sum()\n",
"cost_print = sum(r[\"print_cost\"] + POST + (r[\"build_h\"] / 24 + 3.0) * DOWN[r[\"crit\"]] for _, r in cand.iterrows())\n",
"days_buy, days_print = cand[\"lead_days\"].mean(), (cand[\"build_h\"] / 24 + 3.0).mean()\n",
"scrap = (cand[\"scrap_rate\"] * cand[\"demand_month\"]).sum() * CONFIG[\"scrap_cut\"]\n",
"fig, ax = plt.subplots(1, 2, figsize=(11, 4))\n",
"ax[0].scatter(ops[\"score\"], ops[\"sf\"].clip(upper=10), s=8, c=np.where(ops.index.isin(cand.index), \"tab:blue\", \"lightgray\"))\n",
"ax[0].axvline(CONFIG[\"score_min\"], ls=\"--\", c=\"k\"); ax[0].axhline(CONFIG[\"sf_min\"], ls=\"--\", c=\"k\"); ax[0].set_xlabel(\"conformity score\"); ax[0].set_ylabel(\"safety factor (clipped)\")\n",
"ax[0].set_title(\"Candidates (blue) pending officer approval\")\n",
"ax[1].bar([\"purchase\", \"3D print\"], [days_buy, days_print]); ax[1].set_title(\"Mean supply days (candidates)\")\n",
"plt.tight_layout(); plt.show()\n",
"\n",
"print(\"\\n[์ ๋น๊ธฐ์ ๊ด ๋ธ๋ฆฌํ ์์ฝ] [๊ต์ก์ฉ๊ฐ์์๋ฃ]\")\n",
"print(f\"1. ๋ค์ ๋ถ๊ธฐ ์์ฒญ {len(nxt)}๊ฑด ์ค ๋์ฒด ์ ์ ํ๋ณด {len(cand)}๊ฑด(์น์ธ ๋๊ธฐ), ํ๋ณด ๋ถํ์ ์ ํ๊ธฐ ์์ค ํฉ๊ณ {cand['scrap_cost'].sum():,.0f}๋ง์(๊ฐ์)\")\n",
"print(f\"2. ๊ณต๊ธ ์๊ฐ ํ๊ท {days_buy:.1f}์ผ -> {days_print:.1f}์ผ({1 - days_print / days_buy:.0%} ๋จ์ถ), ์์ ๋น์ฉ {cost_buy:,.0f} -> {cost_print:,.0f}๋ง์(๊ฐ์ ๋จ๊ฐ)\")\n",
"print(f\"3. ํต์ฌ ๋ถํ ํ๋ณด {int((cand['crit'] == 3).sum())}๊ฑด ์ค ์์ฌ ์ธ์ฆ์ ํ๋ณด ํ์ {int(cand['need_cert'].sum())}๊ฑด, ํ๊ธฐ ๊ฐ์ ์ถ์ {scrap:.0f}๊ฐ/์(๊ฐ์ {CONFIG['scrap_cut']:.0%})\")\n",
"print(f\"4. ํ๋ณด ์ค ์ค๋ฌผ ์ํ์ ํ๋ฉด ์์ ์จ ๋ฏธ๋ฌ์ผ ๋ถํ(๋ชจ์ ์ ๋ต) {int((~real_ok).sum())}๊ฑด, ์์ ๋ ์ฝ๋ {len(manual)}๊ฑด ์๋ ํ์ธ -> ์น์ธ ์ ๋ํ ๋ถํ ํ๊ดด ์ํ์ ํธ์ฑํ๋ค\")\n",
"print(f\"5. ๊ฐ๋ ์์ธก ๋ชจ๋ธ: {'์ ๊ท ์ํ์ผ๋ก ๊ต์ฒด' if rt['๊ต์ฒด'] else '๊ธฐ์กด ์ ์ง'}(๊ฒ์ฆ MAPE {min(rt['์ฑํผ์ธ MAPE'], rt['๋์ ์ MAPE']):.1%})\")\n",
"print(\"6. ๊ฑด์: ๊ธ์ ํต์ฌ ๋ถํ์ ์ํ ํ ํ๋, ๊ตญ๋ฐฉ ๊ตฐ์ ์ ๋ณด์ฒด๊ณ์ ํ์ค ํ๋ ์ฐ๋ ์ธํฐํ์ด์ค ์ ์. ์ฅ์ฐฉ ์น์ธ์ ์ ๋น๊ธฐ์ ๊ด ๊ฒฐ์ \")\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"๐ก [์ฐธ๊ณ ] ๋ฏธ์
10 ์์ ์คํ ๊ฒฐ๊ณผ (Output)
\n",
"\n",
"```text\n",
"[๊ต์ก์ฉ๊ฐ์์๋ฃ] ์ฌํ์ต: {'์ฑํผ์ธ MAPE': 0.12, '๋์ ์ MAPE': 0.105, '๊ต์ฒด': True} -> ๋์ ์ ๊ต์ฒด\n",
"ํ๋ณด ์์ฒญ 250๊ฑด: ํ๊ฐ 248๊ฑด, ์๋ ํ์ธ 2๊ฑด, ๋์ฒด ์ ์ ํ๋ณด 86๊ฑด, ์ ์ฒด ์คํ 2.6์ด\n",
"\n",
"์์ฒด ์ ๊ฒ 8/8 ํต๊ณผ, ์๋ ์ฌํ์ต ํ์ดํ๋ผ์ธ ์ ์ ์คํ\n",
"์ด์ ๋ฌธ์ 7๊ฐ ์ฅ ์์ฑ๋ฅ 100% (๋ชฉํ 100%: ๋ฌ์ฑ) - ๋ชฉ์ ๊ณผ ๋ฒ์, ์
๋ ฅ ๋ฐ์ดํฐ, ์ผ์ผ ์ด์ฉ ์ ์ฐจ, ์น์ธ(HITL) ๊ธฐ์ค, ์ฌํ์ต ์ ์ฐจ, ์ฅ์ ๋์, ์ฐ๋ ์ธํฐํ์ด์ค\n",
"\n",
"[์ ๋น๊ธฐ์ ๊ด ๋ธ๋ฆฌํ ์์ฝ] [๊ต์ก์ฉ๊ฐ์์๋ฃ]\n",
"1. ๋ค์ ๋ถ๊ธฐ ์์ฒญ 250๊ฑด ์ค ๋์ฒด ์ ์ ํ๋ณด 86๊ฑด(์น์ธ ๋๊ธฐ), ํ๋ณด ๋ถํ์ ์ ํ๊ธฐ ์์ค ํฉ๊ณ 104,000๋ง์(๊ฐ์)\n",
"2. ๊ณต๊ธ ์๊ฐ ํ๊ท 40.1์ผ -> 3.4์ผ(92% ๋จ์ถ), ์์ ๋น์ฉ 35,338 -> 20,276๋ง์(๊ฐ์ ๋จ๊ฐ)\n",
"3. ํต์ฌ ๋ถํ ํ๋ณด 24๊ฑด ์ค ์์ฌ ์ธ์ฆ์ ํ๋ณด ํ์ 9๊ฑด, ํ๊ธฐ ๊ฐ์ ์ถ์ 102๊ฐ/์(๊ฐ์ 30%)\n",
"4. ํ๋ณด ์ค ์ค๋ฌผ ์ํ์ ํ๋ฉด ์์ ์จ ๋ฏธ๋ฌ์ผ ๋ถํ(๋ชจ์ ์ ๋ต) 0๊ฑด, ์์ ๋ ์ฝ๋ 2๊ฑด ์๋ ํ์ธ -> ์น์ธ ์ ๋ํ ๋ถํ ํ๊ดด ์ํ์ ํธ์ฑํ๋ค\n",
"5. ๊ฐ๋ ์์ธก ๋ชจ๋ธ: ์ ๊ท ์ํ์ผ๋ก ๊ต์ฒด(๊ฒ์ฆ MAPE 10.5%)\n",
"6. ๊ฑด์: ๊ธ์ ํต์ฌ ๋ถํ์ ์ํ ํ ํ๋, ๊ตญ๋ฐฉ ๊ตฐ์ ์ ๋ณด์ฒด๊ณ์ ํ์ค ํ๋ ์ฐ๋ ์ธํฐํ์ด์ค ์ ์. ์ฅ์ฐฉ ์น์ธ์ ์ ๋น๊ธฐ์ ๊ด ๊ฒฐ์ \n",
"```\n",
" \n"
]
}
],
"metadata": {
"colab": {
"provenance": []
},
"kernelspec": {
"display_name": "Python 3",
"name": "python3"
},
"language_info": {
"name": "python"
}
},
"nbformat": 4,
"nbformat_minor": 0
}