Files
pbl02/uploads/QLT05/QLT05_student.ipynb
T
2026-10-08 17:15:05 +09:00

3399 lines
269 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# 🎯 [QLT05] 외주정비 품질검사 실적·병목 분석\n",
"### Google Colab 실습 환경 (품질·규격·검사 / Level 2)\n",
"\n",
"[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/edunlab/milicontent2/blob/main/uploads/QLT05/QLT05_solution.ipynb)\n",
"\n",
"---\n",
"\n",
"## 📌 1. 프로젝트 개요 및 배경\n",
"\n",
"| 항목 | 내용 |\n",
"| :--- | :--- |\n",
"| **과정 분류** | 품질·규격·검사 (Level 2) |\n",
"| **학습 소요 시간** | 10개 미션 (5일 / 총 28.5시간) |\n",
"| **환경 사양** | Python 3.10+, numpy, pandas, matplotlib, scipy, scikit-learn |\n",
"\n",
"### 📖 프로젝트 목표\n",
"해외 사례에서 국방 감사기관이 외주정비 검사실적을 분석한 결과, 검사 대기가 길어지는 병목과 정비 직후 재발하는 결함이 품질 문제의 주요 단서로 지적된 바 있다. 야전에서도 장비 결함이 품질결함보고(QDR)의 비정형 서술로 쌓이지만 부위, 치명도, 원인이 사람마다 다르게 적혀 다발 결함과 책임 소재를 가려내기 어렵다. 본 프로젝트는 외주정비 품질검사 실적과 QDR 텍스트를 연결해 병목과 품질 취약점을 데이터로 찾는 것을 목표로, 교육용 가상 QDR와 가상 외주정비 업체 자료를 코드로 만들어 분석한다. QDR 데이터 스키마 정의, 서술 텍스트 정제와 부위·결함 라벨링, 빈도 Baseline과 파레토·상관 분석, 토픽 모델링과 월간 건수 예측, 치명도 기반 취약성 지수, 설명가능한 발생 현황 화면, 품질관 승인(HITL) 기반 하자 구상·설계 변경 시뮬레이터, 10년치 대용량 일괄 분석, 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. 야전 장비 품질결함보고(QDR, Quality Deficiency Report) 데이터 스키마 분석\n",
"**소요시간**: 약 1.5시간\n",
"\n",
"### 📋 미션 배경 및 수행 지침\n",
"#### 미션 배경 및 당위성\n",
"\n",
"야전 장비 품질결함보고(QDR) 데이터 스키마를 분석한다. 실제 전술 현장에서는 단순 탐지에 그치지 않고 신속한 위협 판단과 신뢰도 높은 의사결정이 필수적이므로, **야전 장비 품질결함보고(QDR)의 보고 정보, 결함 서술 텍스트, 외주정비·검사 실적, 조치 결과 데이터 스키마와 데이터 딕셔너리, 결함 분석 업무 프로세스 맵을 정의한다.** 과업을 완수하여 오판을 방지하고 작전 대응력을 극대화해야 합니다.\n",
"\n",
"이 문제를 해결하기 위해 본 미션에서는 **결함 발견부터 QDR 작성, 접수·검토, 검사·판정, 원인 분석, 조치, 실적 집계, 다발 결함 식별, 사후검토까지 업무 흐름을 단계별로 정리하고 보고·텍스트·검사·결과 묶음으로 피처 20개 이상 정의하고 검증하는** 실습을 체계적으로 수행합니다.\n",
"\n",
"학습자는 실습가이드에 제시된 총 4개 세부 활동(기본 기능 구현 ➔ 결과 지표 검증 ➔ 파라미터 튜닝 및 도전과제)을 순차적으로 진행하며 핵심 판정 기준을 달성합니다.\n",
"\n",
"**이번 결과물:** `docs/qlt05_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": [
"\"\"\"야전 장비 품질결함보고(QDR) 데이터(보고 정보, 결함 서술 텍스트, 외주정비·검사 실적, 조치 결과)의 스키마·데이터 딕셔너리(피처 20개 이상)와 결함 분석 업무 프로세스 맵을 정의하고, 가상 QDR 표본을 검증하며 오류 주입으로 검출 여부를 확인한다.\"\"\"\n",
"import json\n",
"import re\n",
"import numpy as np\n",
"import pandas as pd\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# [교육용가상자료] 가상 야전 장비 품질결함보고(QDR) 생성기 (실제 부대·장비·업체·정비 실적 없음)\n",
"# 가상 장비 부위 12종, 결함 유형 8종, 치명도(Critical/Major/Minor), 원인 5종, 가상 외주정비 업체 4곳 + 부대정비.\n",
"# QDR 1건마다 한국어 서술 텍스트를 만들고, 월별 건수는 계절·추세·훈련 강도(usage)·일부 제조 불량 이벤트를 따른다.\n",
"# ---------------------------------------------------------------------------\n",
"PARTS = [ # (이름, 별칭, 월 기준 건수, 계절 진폭, 정점 월, (Critical, Major 비율), 가상 단가지수, 제조사, 주요 결함 가중치)\n",
" (\"엔진 연료계통\", [\"연료계통\", \"연료 라인\", \"연료 펌프\"], 26, 0.15, 7, (0.10, 0.35), 6.0, \"MK1\", \"누유:4 마모:2 체결불량:2 오작동:2\"),\n",
" (\"변속기\", [\"변속기\", \"T/M\", \"미션\"], 22, 0.10, 8, (0.12, 0.38), 9.0, \"MK1\", \"마모:4 누유:2 과열:3 오작동:1\"),\n",
" (\"제동 장치\", [\"제동 장치\", \"브레이크\", \"제동 라인\"], 20, 0.20, 12, (0.26, 0.40), 5.0, \"MK2\", \"마모:4 균열:2 누유:2 체결불량:2\"),\n",
" (\"조향 장치\", [\"조향 장치\", \"핸들 연동부\", \"조향 로드\"], 14, 0.10, 6, (0.30, 0.40), 5.0, \"MK2\", \"체결불량:4 균열:2 마모:3 오작동:1\"),\n",
" (\"냉각 계통\", [\"냉각 계통\", \"라디에이터\", \"냉각수 라인\"], 24, 0.30, 7, (0.05, 0.30), 3.0, \"MK3\", \"누유:4 과열:4 부식:2\"),\n",
" (\"전기·배터리\", [\"배터리\", \"전기 계통\", \"발전기\"], 28, 0.35, 1, (0.04, 0.28), 2.5, \"MK3\", \"접촉불량:4 부식:2 과열:2 오작동:2\"),\n",
" (\"통신 모듈\", [\"통신 모듈\", \"무전기 연동부\", \"안테나 접속부\"], 16, 0.05, 4, (0.03, 0.25), 4.0, \"MK4\", \"접촉불량:5 오작동:4 부식:1\"),\n",
" (\"유압 계통\", [\"유압 계통\", \"유압 호스\", \"유압 실린더\"], 18, 0.10, 9, (0.10, 0.35), 4.5, \"MK2\", \"누유:5 균열:2 마모:2 체결불량:1\"),\n",
" (\"현가 장치\", [\"현가 장치\", \"서스펜션\", \"완충기\"], 15, 0.08, 5, (0.06, 0.30), 3.5, \"MK4\", \"마모:4 균열:3 체결불량:2 부식:1\"),\n",
" (\"차체 용접부\", [\"차체 용접부\", \"프레임 용접부\", \"차체 이음부\"], 10, 0.05, 10, (0.28, 0.40), 7.0, \"MK1\", \"균열:6 부식:3 체결불량:1\"),\n",
" (\"광학·조준 장비\", [\"광학 장비\", \"조준경\", \"관측 장비\"], 8, 0.05, 3, (0.03, 0.22), 8.0, \"MK4\", \"오작동:4 접촉불량:3 부식:2 체결불량:1\"),\n",
" (\"공기 정화 필터\", [\"공기 필터\", \"흡기 필터\", \"정화 장치\"], 12, 0.25, 4, (0.01, 0.12), 1.0, \"MK3\", \"마모:5 부식:2 체결불량:3\")]\n",
"NP = len(PARTS)\n",
"DEFECTS = { # 결함 유형: 증상 표현(동의어)\n",
" \"균열\": [\"균열이 발견됨\", \"크랙이 확인됨\", \"갈라짐이 있음\"], \"누유\": [\"누유가 발생함\", \"오일이 새어 나옴\", \"누설 흔적이 있음\"],\n",
" \"마모\": [\"마모가 심함\", \"과도한 마모가 확인됨\", \"닳은 흔적이 있음\"], \"부식\": [\"부식이 진행됨\", \"녹이 발생함\", \"산화 흔적이 있음\"],\n",
" \"접촉불량\": [\"접촉 불량이 발생함\", \"커넥터 단선이 의심됨\", \"신호가 끊김\"], \"체결불량\": [\"볼트 풀림이 확인됨\", \"체결 상태가 불량함\", \"고정부가 헐거움\"],\n",
" \"과열\": [\"과열 경고가 발생함\", \"온도가 과도하게 상승함\", \"열이 심하게 남\"], \"오작동\": [\"오작동이 반복됨\", \"작동이 불안정함\", \"간헐적으로 멈춤\"]}\n",
"DNAMES = list(DEFECTS)\n",
"SEV = [\"Minor\", \"Major\", \"Critical\"]\n",
"SEV_CUE = {0: [\"경미한 수준임\", \"운용에는 지장이 없음\", \"육안 점검상 사소한 이상임\", \"기능에는 영향이 없음\"],\n",
" 1: [\"성능 저하가 있음\", \"조속한 정비가 필요함\", \"출력이 떨어지는 상태임\", \"기능이 부분적으로 제한됨\"],\n",
" 2: [\"즉시 운용 중지함\", \"안전에 위험이 있어 사용 불가\", \"작동 정지로 임무 수행이 어려움\", \"인명 안전 위협이 우려됨\"]}\n",
"CAUSES = [\"제조 불량\", \"정비 품질\", \"운용 부주의\", \"설계 취약\", \"자연 노후\"]\n",
"CAUSE_CUE = {\n",
" \"제조 불량\": [\"신품 인수 직후 발생함\", \"출고 상태에서 이미 이상이 있었음\", \"인수 후 얼마 지나지 않아 발생함\"],\n",
" \"정비 품질\": [\"외주 정비 후 얼마 되지 않아 재발함\", \"정비 후 재조립 상태가 불량함\", \"정비 직후 같은 부위에서 다시 발생함\"],\n",
" \"운용 부주의\": [\"험지 기동 중 충격 이후 발생함\", \"과부하 운용 후 발생함\", \"점검 없이 연속 운용한 뒤 발생함\"],\n",
" \"설계 취약\": [\"동일 증상이 다른 차량에서도 반복됨\", \"구조적으로 취약한 부위로 보임\", \"여러 부대에서 같은 형태로 보고됨\"],\n",
" \"자연 노후\": [\"사용 연한이 도래함\", \"장기간 사용으로 노후됨\", \"수명 주기 말기에 발생함\"]}\n",
"VENDORS = [\"외주A\", \"외주B\", \"외주C\", \"외주D\", \"부대정비\"]\n",
"ACTIONS = [\"정비소 입고를 요청함\", \"현장 임시 조치 후 운용 중임\", \"부품 교환을 요청함\", \"재검사를 요청함\", \"원인 확인이 필요함\"]\n",
"MAKER_OF = [p[7] for p in PARTS]\n",
"PRICE = np.array([p[6] for p in PARTS])\n",
"\n",
"\n",
"def usage_index(months, seed=0):\n",
" \"\"\"월별 훈련 강도(미리 알 수 있는 계획 값): 연 1회 정점을 갖는 계절 + 작은 잡음\"\"\"\n",
" # [TODO : 학생 실습 1] 야전 장비 품질결함보고(QDR, Quality Deficiency Report) 데이터 스키마 분석 관련 핵심 로직을 구현하세요.\n",
" # 힌트: 상단 주석 및 데이터 스키마/지침을 참고하여 작성합니다.\n",
" # <<< 여기에 코드를 작성하세요 >>>\n",
" pass # 작성 후 pass를 제거하세요.\n",
"def make_qdr(months=36, seed=1, scale=1.0, surge=True):\n",
" \"\"\"반환: QDR DataFrame(건당 1행), 월별 usage, 급증 이벤트 목록\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" use = usage_index(months, seed)\n",
" events = []\n",
" if surge:\n",
" for _ in range(max(2, months // 12 * 2)):\n",
" events.append((int(rng.integers(NP)), int(rng.integers(6, months))))\n",
" rows = []\n",
" wdef = [np.array([float(x.split(\":\")[1]) for x in p[8].split()]) for p in PARTS]\n",
" ddef = [[x.split(\":\")[0] for x in p[8].split()] for p in PARTS]\n",
" n = 0\n",
" for m in range(months):\n",
" for pi, p in enumerate(PARTS):\n",
" lam = p[2] * scale * (1 + 0.010 * m) * use[m] * (1 + p[3] * np.cos(2 * np.pi * (m % 12 - p[4]) / 12))\n",
" ev = (pi, m) in events\n",
" k = rng.poisson(lam * (2.6 if ev else 1.0))\n",
" for _ in range(k):\n",
" rows.append((n, m, pi, ev)); n += 1\n",
" N = len(rows)\n",
" A = np.array(rows)\n",
" month, part, ev = A[:, 1], A[:, 2], A[:, 3].astype(bool)\n",
" defect = np.array([rng.choice(ddef[i], p=wdef[i] / wdef[i].sum()) for i in part])\n",
" cr = np.array([PARTS[i][5][0] for i in part]); ma = np.array([PARTS[i][5][1] for i in part])\n",
" u = rng.random(N)\n",
" sev = np.where(u < cr, 2, np.where(u < cr + ma, 1, 0))\n",
" vendor = rng.choice(VENDORS, N, p=[0.25, 0.2, 0.2, 0.15, 0.2])\n",
" # 원인: 기본 비율에 부위(제조사)·업체·이벤트 효과를 곱한다\n",
" base = np.array([0.22, 0.20, 0.20, 0.16, 0.22])\n",
" P = np.tile(base, (N, 1))\n",
" P[vendor == \"외주B\", 1] *= 2.6\n",
" P[ev, 0] *= 5.0\n",
" design = np.isin(part, [2, 3, 7]); P[design, 3] *= 2.2\n",
" P[np.isin(part, [4, 5, 11]), 0] *= 2.0 # 제조사 MK3 납품 부위는 제조 불량 비중이 높다\n",
" P[defect == \"마모\", 4] *= 1.6; P[defect == \"누유\", 4] *= 1.3\n",
" P /= P.sum(axis=1, keepdims=True)\n",
" cause = np.array([CAUSES[rng.choice(5, p=P[i])] for i in range(N)])\n",
" since = np.where(cause == \"정비 품질\", rng.exponential(35, N), rng.exponential(160, N)).clip(1, 720).round().astype(int)\n",
" wait = (rng.lognormal(1.3, 0.5, N) * np.where(vendor == \"외주B\", 1.7, np.where(vendor == \"부대정비\", 0.6, 1.0)) * (0.7 + 0.5 * use[month])).round(1)\n",
" reins = rng.random(N) < np.where(cause == \"정비 품질\", 0.6, 0.08)\n",
" texts = []\n",
" for i in range(N):\n",
" pt = PARTS[part[i]]\n",
" cl = [f\"{rng.choice(pt[1])}에서 {rng.choice(DEFECTS[defect[i]])}\"]\n",
" if rng.random() < 0.7: cl.append(rng.choice(SEV_CUE[sev[i]]))\n",
" if rng.random() < 0.65: # 원인 단서: 8%는 실제 원인과 다른 단서가 적힌다(보고자의 추정)\n",
" cc = cause[i] if rng.random() > 0.08 else rng.choice(CAUSES)\n",
" cl.append(rng.choice(CAUSE_CUE[cc]))\n",
" cl.append(rng.choice(ACTIONS))\n",
" if rng.random() < 0.5: cl.insert(0, f\"VU-{rng.integers(1, 31):02d} 소속\")\n",
" texts.append(\". \".join(cl) + \".\")\n",
" df = pd.DataFrame({\"qdr_id\": [f\"Q{seed}{i:06d}\" for i in range(N)], \"month\": month, \"unit\": [f\"VU-{x:02d}\" for x in rng.integers(1, 31, N)],\n",
" \"part\": part, \"defect\": defect, \"sev\": sev, \"cause\": cause, \"vendor\": vendor, \"maker\": [MAKER_OF[i] for i in part],\n",
" \"since_maint\": since, \"inspect_wait\": wait, \"reinspect\": reins, \"event\": ev, \"text\": texts})\n",
" df[\"cost\"] = PRICE[df[\"part\"]] * np.array([1.0, 2.0, 4.0])[df[\"sev\"]] # [교육용가상자료] 수리·조치 비용지수\n",
" return df, use, events\n",
"\n",
"# 1) 업무 프로세스 맵: 결함 발생부터 원인 환류까지 (각 단계에서 생기는 데이터)\n",
"PROCESS = [(\"결함 발견\", \"발견 부대, 장비, 부위, 서술 텍스트\"), (\"QDR 작성\", \"QDR ID, 작성일, 치명도(Critical/Major/Minor)\"),\n",
" (\"접수·검토\", \"텍스트 정제, 부위·결함 유형 라벨\"), (\"검사·판정\", \"검사 대기일, 재검사 여부, 외주정비 업체\"),\n",
" (\"원인 분석\", \"원인 구분(제조·정비·운용·설계·노후), 제조사\"), (\"조치\", \"수리·교환·하자 구상 여부, 조치 비용\"),\n",
" (\"실적 집계\", \"부위별·월별 건수, 취약성 지수\"), (\"다발 결함 식별\", \"파레토(ABC), 이상 신호\"), (\"사후검토(AAR)\", \"원인 오분류 사례, 룰 조정 이력\")]\n",
"\n",
"# 2) 스키마(데이터 딕셔너리): 그룹 / 필드 / 자료형 / 규칙 / 설명\n",
"PART_NAMES = [p[0] for p in PARTS]\n",
"S = {\n",
" \"report\": {\n",
" \"qdr_id\": (str, {\"pattern\": r\"^Q\\d{7,8}$\"}, \"QDR ID\"),\n",
" \"month\": (int, {\"min\": 0, \"max\": 143}, \"보고 월(모의 시작 기준 경과 월)\"),\n",
" \"unit\": (str, {\"pattern\": r\"^VU-\\d{2}$\"}, \"보고 부대 코드(가상)\"),\n",
" \"equip_code\": (str, {\"pattern\": r\"^VX-\\d{4}-\\d{4}$\"}, \"장비 식별코드(국방 표준 코드 형식을 모사한 가상 코드)\"),\n",
" \"fsc_group\": (str, {\"pattern\": r\"^\\d{4}$\"}, \"보급 분류군(가상 4자리)\"),\n",
" \"part\": (str, {\"enum\": PART_NAMES}, \"결함 부위(장비 계통)\"),\n",
" \"severity\": (str, {\"enum\": SEV}, \"결함 치명도\"),\n",
" },\n",
" \"text\": {\n",
" \"text\": (str, {\"min_len\": 5}, \"결함 서술 텍스트(비정형)\"),\n",
" \"defect\": (str, {\"enum\": DNAMES + [\"미분류\"]}, \"결함 유형 라벨\"),\n",
" \"n_tokens\": (int, {\"min\": 1, \"max\": 200}, \"정제 후 토큰 수\"),\n",
" \"part_label\": (str, {\"enum\": PART_NAMES + [\"미분류\"]}, \"서술에서 추출한 부위 라벨\"),\n",
" },\n",
" \"inspection\": {\n",
" \"vendor\": (str, {\"enum\": VENDORS}, \"직전 정비 수행처(외주 업체 또는 부대정비)\"),\n",
" \"since_maint\": (int, {\"min\": 0, \"max\": 1500}, \"직전 정비 후 경과일\"),\n",
" \"inspect_wait\": (float, {\"min\": 0, \"max\": 60}, \"검사 대기일(검사 병목 지표)\"),\n",
" \"reinspect\": (bool, {}, \"재검사 여부\"),\n",
" },\n",
" \"result\": {\n",
" \"cause\": (str, {\"enum\": CAUSES}, \"원인 구분 - 예측대상 1\"),\n",
" \"maker\": (str, {\"pattern\": r\"^MK\\d$\"}, \"제조사 코드(가상)\"),\n",
" \"cost\": (float, {\"min\": 0, \"max\": 100}, \"조치 비용지수 - 예측대상 2\"),\n",
" \"monthly_count\": (int, {\"min\": 0, \"max\": 500}, \"부위별 월간 QDR 건수 - 예측대상 3\"),\n",
" \"vuln_index\": (float, {\"min\": 0, \"max\": 1000}, \"치명도 가중 취약성 지수 - 예측대상 4\"),\n",
" \"event_flag\": (bool, {}, \"제조 LOT 불량 등 급증 이벤트 월 여부\"),\n",
" \"hitl_status\": (str, {\"enum\": [\"PENDING\", \"APPROVED\", \"REJECTED\", \"NO_RESPONSE\"]}, \"품질관 검토 상태\"),\n",
" },\n",
"}\n",
"\n",
"\n",
"def check(name, v, typ, rule):\n",
" # [TODO : 학생 실습 1] 야전 장비 품질결함보고(QDR, Quality Deficiency Report) 데이터 스키마 분석 관련 핵심 로직을 구현하세요.\n",
" # 힌트: 상단 주석 및 데이터 스키마/지침을 참고하여 작성합니다.\n",
" # <<< 여기에 코드를 작성하세요 >>>\n",
" pass # 작성 후 pass를 제거하세요.\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",
"df, use, events = make_qdr(36, seed=1)\n",
"rng = np.random.default_rng(7)\n",
"mc = df.groupby([\"part\", \"month\"]).size()\n",
"recs = []\n",
"for r in df.sample(20, random_state=1).itertuples():\n",
" t = r.text\n",
" recs.append({\n",
" \"report\": {\"qdr_id\": r.qdr_id, \"month\": int(r.month), \"unit\": r.unit, \"equip_code\": f\"VX-{rng.integers(1000, 9999)}-{rng.integers(1000, 9999)}\",\n",
" \"fsc_group\": f\"{rng.integers(1000, 9999)}\", \"part\": PART_NAMES[r.part], \"severity\": SEV[r.sev]},\n",
" \"text\": {\"text\": t, \"defect\": r.defect, \"n_tokens\": max(len(t.split()), 1), \"part_label\": PART_NAMES[r.part]},\n",
" \"inspection\": {\"vendor\": r.vendor, \"since_maint\": int(r.since_maint), \"inspect_wait\": float(r.inspect_wait), \"reinspect\": bool(r.reinspect)},\n",
" \"result\": {\"cause\": r.cause, \"maker\": r.maker, \"cost\": float(r.cost), \"monthly_count\": int(mc[(r.part, r.month)]),\n",
" \"vuln_index\": float(sum(np.array([1, 3, 9])[df.loc[(df.part == r.part) & (df.month == r.month), \"sev\"]])), \"event_flag\": bool(r.event), \"hitl_status\": \"PENDING\"}})\n",
"ok = sum(not validate(r) for r in recs)\n",
"print(f\"[교육용가상자료] 가상 QDR {len(df)}건 생성, 표본 20건 중 검증 통과 {ok}건, 오류 {len(recs) - ok}건\")\n",
"print(f\"전체 데이터 부위·치명도·원인 허용값 검증: {'통과' if df['part'].between(0, NP - 1).all() and df['sev'].isin([0, 1, 2]).all() and df['cause'].isin(CAUSES).all() else '실패'}\")\n",
"\n",
"# 4) 오류 주입 시험\n",
"bad = json.loads(json.dumps(recs[0]))\n",
"bad[\"report\"][\"qdr_id\"] = \"QDR-7\" # ID 형식 오류\n",
"bad[\"report\"][\"severity\"] = \"Severe\" # 허용되지 않은 치명도\n",
"bad[\"text\"][\"text\"] = \"고장\" # 서술 너무 짧음\n",
"del bad[\"inspection\"][\"vendor\"] # 필수 필드 누락\n",
"bad[\"inspection\"][\"inspect_wait\"] = \"3일\" # 자료형 오류\n",
"bad[\"result\"][\"cost\"] = 250.0 # 범위 초과\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)[:34] 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"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"<details>\n",
"<summary><b>💡 [참고] 미션 01 예상 실행 결과 (Output)</b></summary>\n",
"\n",
"```text\n",
"[교육용가상자료] 가상 QDR 9221건 생성, 표본 20건 중 검증 통과 20건, 오류 0건\n",
"전체 데이터 부위·치명도·원인 허용값 검증: 통과\n",
"\n",
"[오류 주입 결과]\n",
" - report.qdr_id: 형식 불일치 'QDR-7'\n",
" - report.severity: 허용되지 않은 값 'Severe'\n",
" - text.text: 서술이 너무 짧음\n",
" - inspection.vendor: 필수 필드 누락\n",
" - inspection.inspect_wait: 자료형 오류(str)\n",
" - result.cost: 최대값 100 초과\n",
"\n",
"[업무 프로세스 맵]\n",
"1.결함 발견 -> 2.QDR 작성 -> 3.접수·검토 -> 4.검사·판정 -> 5.원인 분석 -> 6.조치 -> 7.실적 집계 -> 8.다발 결함 식별 -> 9.사후검토(AAR)\n",
" 결함 발견 : 발견 부대, 장비, 부위, 서술 텍스트\n",
" QDR 작성 : QDR ID, 작성일, 치명도(Critical/Major/Minor)\n",
" 접수·검토 : 텍스트 정제, 부위·결함 유형 라벨\n",
" 검사·판정 : 검사 대기일, 재검사 여부, 외주정비 업체\n",
" 원인 분석 : 원인 구분(제조·정비·운용·설계·노후), 제조사\n",
" 조치 : 수리·교환·하자 구상 여부, 조치 비용\n",
" 실적 집계 : 부위별·월별 건수, 취약성 지수\n",
" 다발 결함 식별 : 파레토(ABC), 이상 신호\n",
" 사후검토(AAR) : 원인 오분류 사례, 룰 조정 이력\n",
"\n",
"[데이터 딕셔너리] 피처 22개 (목표 20개 이상: 달성)\n",
" 그룹 필드 자료형 설명\n",
" report qdr_id str QDR ID\n",
" report month int 보고 월(모의 시작 기준 경과 월)\n",
" report unit str 보고 부대 코드(가상)\n",
" report equip_code str 장비 식별코드(국방 표준 코드 형식을 모사한 가상 코드)\n",
" report fsc_group str 보급 분류군(가상 4자리)\n",
" report part str 결함 부위(장비 계통)\n",
" report severity str 결함 치명도\n",
" text text str 결함 서술 텍스트(비정형)\n",
" text defect str 결함 유형 라벨\n",
" text n_tokens int 정제 후 토큰 수\n",
" text part_label str 서술에서 추출한 부위 라벨\n",
"inspection vendor str 직전 정비 수행처(외주 업체 또는 부대정비)\n",
"inspection since_maint int 직전 정비 후 경과일\n",
"inspection inspect_wait float 검사 대기일(검사 병목 지표)\n",
"inspection reinspect bool 재검사 여부\n",
"... (이하 생략)\n",
"```\n",
"</details>\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## 🎯 미션 02. 비정형 결함 서술 텍스트 정제, 장비 부위별/결함 유형별 라벨링 전처리 구현\n",
"**소요시간**: 약 2.0시간\n",
"\n",
"### 📋 미션 배경 및 수행 지침\n",
"#### 미션 배경 및 당위성\n",
"\n",
"비정형 결함 서술 텍스트 정제 및 부위별/유형별 라벨링 전처리 모듈을 구현한다. 실제 전술 현장에서는 단순 탐지에 그치지 않고 신속한 위협 판단과 신뢰도 높은 의사결정이 필수적이므로, **비정형 결함 서술 텍스트를 정제하고 장비 부위별·결함 유형별로 라벨링하는 전처리 모듈을 구현한다.** 과업을 완수하여 오판을 방지하고 작전 대응력을 극대화해야 합니다.\n",
"\n",
"이 문제를 해결하기 위해 본 미션에서는 **중복 QDR 제거, 부대 코드와 일자 표기 통일, 전각·특수문자·공백 정규화, 약어(T/M 등) 확장, 사전 기반 오타 교정, 음수 대기일 결측 처리, KNN 결측 보정, 부위·결함 유형 키워드 라벨링 등 정제 규칙 10종을 적용 수행하고 검증하는** 실습을 체계적으로 수행합니다.\n",
"\n",
"학습자는 실습가이드에 제시된 총 4개 세부 활동(기본 기능 구현 ➔ 결과 지표 검증 ➔ 파라미터 튜닝 및 도전과제)을 순차적으로 진행하며 핵심 판정 기준을 달성합니다.\n",
"\n",
"**이번 결과물:** `src/etl/qlt05_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": [
"\"\"\"표기 불일치·중복·전각·특수문자·약어·오타·불가능값·결측이 섞인 가상 QDR 원천 자료에 정제 규칙 10종(텍스트 정규화, 약어 확장, 오타 교정, KNN 결측 보정, 부위·결함 유형 키워드 라벨링 포함)을 적용하고, 긴급 보고는 보존하며 정합성 검증 통과율과 라벨링 정확도, 정제 전후 통계를 확인한다.\"\"\"\n",
"import re\n",
"import unicodedata\n",
"import numpy as np\n",
"import pandas as pd\n",
"from sklearn.impute import KNNImputer\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# [교육용가상자료] 가상 야전 장비 품질결함보고(QDR) 생성기 (실제 부대·장비·업체·정비 실적 없음)\n",
"# 가상 장비 부위 12종, 결함 유형 8종, 치명도(Critical/Major/Minor), 원인 5종, 가상 외주정비 업체 4곳 + 부대정비.\n",
"# QDR 1건마다 한국어 서술 텍스트를 만들고, 월별 건수는 계절·추세·훈련 강도(usage)·일부 제조 불량 이벤트를 따른다.\n",
"# ---------------------------------------------------------------------------\n",
"PARTS = [ # (이름, 별칭, 월 기준 건수, 계절 진폭, 정점 월, (Critical, Major 비율), 가상 단가지수, 제조사, 주요 결함 가중치)\n",
" (\"엔진 연료계통\", [\"연료계통\", \"연료 라인\", \"연료 펌프\"], 26, 0.15, 7, (0.10, 0.35), 6.0, \"MK1\", \"누유:4 마모:2 체결불량:2 오작동:2\"),\n",
" (\"변속기\", [\"변속기\", \"T/M\", \"미션\"], 22, 0.10, 8, (0.12, 0.38), 9.0, \"MK1\", \"마모:4 누유:2 과열:3 오작동:1\"),\n",
" (\"제동 장치\", [\"제동 장치\", \"브레이크\", \"제동 라인\"], 20, 0.20, 12, (0.26, 0.40), 5.0, \"MK2\", \"마모:4 균열:2 누유:2 체결불량:2\"),\n",
" (\"조향 장치\", [\"조향 장치\", \"핸들 연동부\", \"조향 로드\"], 14, 0.10, 6, (0.30, 0.40), 5.0, \"MK2\", \"체결불량:4 균열:2 마모:3 오작동:1\"),\n",
" (\"냉각 계통\", [\"냉각 계통\", \"라디에이터\", \"냉각수 라인\"], 24, 0.30, 7, (0.05, 0.30), 3.0, \"MK3\", \"누유:4 과열:4 부식:2\"),\n",
" (\"전기·배터리\", [\"배터리\", \"전기 계통\", \"발전기\"], 28, 0.35, 1, (0.04, 0.28), 2.5, \"MK3\", \"접촉불량:4 부식:2 과열:2 오작동:2\"),\n",
" (\"통신 모듈\", [\"통신 모듈\", \"무전기 연동부\", \"안테나 접속부\"], 16, 0.05, 4, (0.03, 0.25), 4.0, \"MK4\", \"접촉불량:5 오작동:4 부식:1\"),\n",
" (\"유압 계통\", [\"유압 계통\", \"유압 호스\", \"유압 실린더\"], 18, 0.10, 9, (0.10, 0.35), 4.5, \"MK2\", \"누유:5 균열:2 마모:2 체결불량:1\"),\n",
" (\"현가 장치\", [\"현가 장치\", \"서스펜션\", \"완충기\"], 15, 0.08, 5, (0.06, 0.30), 3.5, \"MK4\", \"마모:4 균열:3 체결불량:2 부식:1\"),\n",
" (\"차체 용접부\", [\"차체 용접부\", \"프레임 용접부\", \"차체 이음부\"], 10, 0.05, 10, (0.28, 0.40), 7.0, \"MK1\", \"균열:6 부식:3 체결불량:1\"),\n",
" (\"광학·조준 장비\", [\"광학 장비\", \"조준경\", \"관측 장비\"], 8, 0.05, 3, (0.03, 0.22), 8.0, \"MK4\", \"오작동:4 접촉불량:3 부식:2 체결불량:1\"),\n",
" (\"공기 정화 필터\", [\"공기 필터\", \"흡기 필터\", \"정화 장치\"], 12, 0.25, 4, (0.01, 0.12), 1.0, \"MK3\", \"마모:5 부식:2 체결불량:3\")]\n",
"NP = len(PARTS)\n",
"DEFECTS = { # 결함 유형: 증상 표현(동의어)\n",
" \"균열\": [\"균열이 발견됨\", \"크랙이 확인됨\", \"갈라짐이 있음\"], \"누유\": [\"누유가 발생함\", \"오일이 새어 나옴\", \"누설 흔적이 있음\"],\n",
" \"마모\": [\"마모가 심함\", \"과도한 마모가 확인됨\", \"닳은 흔적이 있음\"], \"부식\": [\"부식이 진행됨\", \"녹이 발생함\", \"산화 흔적이 있음\"],\n",
" \"접촉불량\": [\"접촉 불량이 발생함\", \"커넥터 단선이 의심됨\", \"신호가 끊김\"], \"체결불량\": [\"볼트 풀림이 확인됨\", \"체결 상태가 불량함\", \"고정부가 헐거움\"],\n",
" \"과열\": [\"과열 경고가 발생함\", \"온도가 과도하게 상승함\", \"열이 심하게 남\"], \"오작동\": [\"오작동이 반복됨\", \"작동이 불안정함\", \"간헐적으로 멈춤\"]}\n",
"DNAMES = list(DEFECTS)\n",
"SEV = [\"Minor\", \"Major\", \"Critical\"]\n",
"SEV_CUE = {0: [\"경미한 수준임\", \"운용에는 지장이 없음\", \"육안 점검상 사소한 이상임\", \"기능에는 영향이 없음\"],\n",
" 1: [\"성능 저하가 있음\", \"조속한 정비가 필요함\", \"출력이 떨어지는 상태임\", \"기능이 부분적으로 제한됨\"],\n",
" 2: [\"즉시 운용 중지함\", \"안전에 위험이 있어 사용 불가\", \"작동 정지로 임무 수행이 어려움\", \"인명 안전 위협이 우려됨\"]}\n",
"CAUSES = [\"제조 불량\", \"정비 품질\", \"운용 부주의\", \"설계 취약\", \"자연 노후\"]\n",
"CAUSE_CUE = {\n",
" \"제조 불량\": [\"신품 인수 직후 발생함\", \"출고 상태에서 이미 이상이 있었음\", \"인수 후 얼마 지나지 않아 발생함\"],\n",
" \"정비 품질\": [\"외주 정비 후 얼마 되지 않아 재발함\", \"정비 후 재조립 상태가 불량함\", \"정비 직후 같은 부위에서 다시 발생함\"],\n",
" \"운용 부주의\": [\"험지 기동 중 충격 이후 발생함\", \"과부하 운용 후 발생함\", \"점검 없이 연속 운용한 뒤 발생함\"],\n",
" \"설계 취약\": [\"동일 증상이 다른 차량에서도 반복됨\", \"구조적으로 취약한 부위로 보임\", \"여러 부대에서 같은 형태로 보고됨\"],\n",
" \"자연 노후\": [\"사용 연한이 도래함\", \"장기간 사용으로 노후됨\", \"수명 주기 말기에 발생함\"]}\n",
"VENDORS = [\"외주A\", \"외주B\", \"외주C\", \"외주D\", \"부대정비\"]\n",
"ACTIONS = [\"정비소 입고를 요청함\", \"현장 임시 조치 후 운용 중임\", \"부품 교환을 요청함\", \"재검사를 요청함\", \"원인 확인이 필요함\"]\n",
"MAKER_OF = [p[7] for p in PARTS]\n",
"PRICE = np.array([p[6] for p in PARTS])\n",
"\n",
"\n",
"def usage_index(months, seed=0):\n",
" \"\"\"월별 훈련 강도(미리 알 수 있는 계획 값): 연 1회 정점을 갖는 계절 + 작은 잡음\"\"\"\n",
" # [TODO : 학생 실습 2] 비정형 결함 서술 텍스트 정제, 장비 부위별/결함 유형별 라벨링 전처리 구현 관련 핵심 로직을 구현하세요.\n",
" # 힌트: 상단 주석 및 데이터 스키마/지침을 참고하여 작성합니다.\n",
" # <<< 여기에 코드를 작성하세요 >>>\n",
" pass # 작성 후 pass를 제거하세요.\n",
"def make_qdr(months=36, seed=1, scale=1.0, surge=True):\n",
" \"\"\"반환: QDR DataFrame(건당 1행), 월별 usage, 급증 이벤트 목록\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" use = usage_index(months, seed)\n",
" events = []\n",
" if surge:\n",
" for _ in range(max(2, months // 12 * 2)):\n",
" events.append((int(rng.integers(NP)), int(rng.integers(6, months))))\n",
" rows = []\n",
" wdef = [np.array([float(x.split(\":\")[1]) for x in p[8].split()]) for p in PARTS]\n",
" ddef = [[x.split(\":\")[0] for x in p[8].split()] for p in PARTS]\n",
" n = 0\n",
" for m in range(months):\n",
" for pi, p in enumerate(PARTS):\n",
" lam = p[2] * scale * (1 + 0.010 * m) * use[m] * (1 + p[3] * np.cos(2 * np.pi * (m % 12 - p[4]) / 12))\n",
" ev = (pi, m) in events\n",
" k = rng.poisson(lam * (2.6 if ev else 1.0))\n",
" for _ in range(k):\n",
" rows.append((n, m, pi, ev)); n += 1\n",
" N = len(rows)\n",
" A = np.array(rows)\n",
" month, part, ev = A[:, 1], A[:, 2], A[:, 3].astype(bool)\n",
" defect = np.array([rng.choice(ddef[i], p=wdef[i] / wdef[i].sum()) for i in part])\n",
" cr = np.array([PARTS[i][5][0] for i in part]); ma = np.array([PARTS[i][5][1] for i in part])\n",
" u = rng.random(N)\n",
" sev = np.where(u < cr, 2, np.where(u < cr + ma, 1, 0))\n",
" vendor = rng.choice(VENDORS, N, p=[0.25, 0.2, 0.2, 0.15, 0.2])\n",
" # 원인: 기본 비율에 부위(제조사)·업체·이벤트 효과를 곱한다\n",
" base = np.array([0.22, 0.20, 0.20, 0.16, 0.22])\n",
" P = np.tile(base, (N, 1))\n",
" P[vendor == \"외주B\", 1] *= 2.6\n",
" P[ev, 0] *= 5.0\n",
" design = np.isin(part, [2, 3, 7]); P[design, 3] *= 2.2\n",
" P[np.isin(part, [4, 5, 11]), 0] *= 2.0 # 제조사 MK3 납품 부위는 제조 불량 비중이 높다\n",
" P[defect == \"마모\", 4] *= 1.6; P[defect == \"누유\", 4] *= 1.3\n",
" P /= P.sum(axis=1, keepdims=True)\n",
" cause = np.array([CAUSES[rng.choice(5, p=P[i])] for i in range(N)])\n",
" since = np.where(cause == \"정비 품질\", rng.exponential(35, N), rng.exponential(160, N)).clip(1, 720).round().astype(int)\n",
" wait = (rng.lognormal(1.3, 0.5, N) * np.where(vendor == \"외주B\", 1.7, np.where(vendor == \"부대정비\", 0.6, 1.0)) * (0.7 + 0.5 * use[month])).round(1)\n",
" reins = rng.random(N) < np.where(cause == \"정비 품질\", 0.6, 0.08)\n",
" texts = []\n",
" for i in range(N):\n",
" pt = PARTS[part[i]]\n",
" cl = [f\"{rng.choice(pt[1])}에서 {rng.choice(DEFECTS[defect[i]])}\"]\n",
" if rng.random() < 0.7: cl.append(rng.choice(SEV_CUE[sev[i]]))\n",
" if rng.random() < 0.65: # 원인 단서: 8%는 실제 원인과 다른 단서가 적힌다(보고자의 추정)\n",
" cc = cause[i] if rng.random() > 0.08 else rng.choice(CAUSES)\n",
" cl.append(rng.choice(CAUSE_CUE[cc]))\n",
" cl.append(rng.choice(ACTIONS))\n",
" if rng.random() < 0.5: cl.insert(0, f\"VU-{rng.integers(1, 31):02d} 소속\")\n",
" texts.append(\". \".join(cl) + \".\")\n",
" df = pd.DataFrame({\"qdr_id\": [f\"Q{seed}{i:06d}\" for i in range(N)], \"month\": month, \"unit\": [f\"VU-{x:02d}\" for x in rng.integers(1, 31, N)],\n",
" \"part\": part, \"defect\": defect, \"sev\": sev, \"cause\": cause, \"vendor\": vendor, \"maker\": [MAKER_OF[i] for i in part],\n",
" \"since_maint\": since, \"inspect_wait\": wait, \"reinspect\": reins, \"event\": ev, \"text\": texts})\n",
" df[\"cost\"] = PRICE[df[\"part\"]] * np.array([1.0, 2.0, 4.0])[df[\"sev\"]] # [교육용가상자료] 수리·조치 비용지수\n",
" return df, use, events\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# 공통 텍스트 조각: 정규화, 간이 토큰화(조사 제거), 부위·결함·치명도·원인 키워드 라벨링\n",
"# ---------------------------------------------------------------------------\n",
"TYPO = {\"누윳\": \"누유\", \"균렬\": \"균열\", \"접축 불량\": \"접촉 불량\", \"부씩\": \"부식\", \"마묘\": \"마모\", \"과렬\": \"과열\", \"오작똥\": \"오작동\", \"재동\": \"제동\"}\n",
"ABBR = {\"T/M\": \"변속기\", \"BAT\": \"배터리\"}\n",
"PART_KW = sorted([(a, i) for i, p in enumerate(PARTS) for a in p[1]], key=lambda x: -len(x[0]))\n",
"DEF_KW = {\"균열\": [\"균열\", \"크랙\", \"갈라\"], \"누유\": [\"누유\", \"새어\", \"누설\"], \"마모\": [\"마모\", \"닳은\"], \"부식\": [\"부식\", \"녹이\", \"산화\"],\n",
" \"접촉불량\": [\"접촉 불량\", \"단선\", \"끊김\"], \"체결불량\": [\"볼트\", \"체결\", \"헐거\"], \"과열\": [\"과열\", \"온도\", \"열이\"], \"오작동\": [\"오작동\", \"불안정\", \"멈춤\"]}\n",
"SEV_KW = [(2, [\"즉시 운용 중지\", \"사용 불가\", \"위험\"]), (1, [\"성능 저하\", \"조속한\"]), (0, [\"경미\", \"지장이 없\"])]\n",
"CAUSE_KW = {\"제조 불량\": [\"신품\", \"출고\", \"인수\"], \"정비 품질\": [\"정비 후\", \"재조립\", \"정비 직후\"], \"운용 부주의\": [\"충격\", \"과부하\", \"점검 없이\"],\n",
" \"설계 취약\": [\"동일 증상\", \"구조적\", \"여러 부대\"], \"자연 노후\": [\"사용 연한\", \"노후\", \"수명\"]}\n",
"PARTICLES = (\"에서\", \"으로\", \"이\", \"가\", \"은\", \"는\", \"을\", \"를\", \"에\", \"의\", \"로\", \"와\", \"과\", \"도\", \"함\", \"음\", \"됨\")\n",
"\n",
"\n",
"def norm(t):\n",
" \"\"\"정규화: 전각->반각, 특수문자·중복 공백 제거, 약어 확장, 오타 교정\"\"\"\n",
" # [TODO : 학생 실습 2] 비정형 결함 서술 텍스트 정제, 장비 부위별/결함 유형별 라벨링 전처리 구현 관련 핵심 로직을 구현하세요.\n",
" # 힌트: 상단 주석 및 데이터 스키마/지침을 참고하여 작성합니다.\n",
" # <<< 여기에 코드를 작성하세요 >>>\n",
" pass # 작성 후 pass를 제거하세요.\n",
"def tok(t):\n",
" out = []\n",
" for w in re.split(r\"[\\s.,]+\", norm(t)):\n",
" if not w or re.fullmatch(r\"[\\d\\-A-Za-z]+\", w):\n",
" continue\n",
" for p in PARTICLES:\n",
" if len(w) > len(p) + 1 and w.endswith(p):\n",
" w = w[:-len(p)]\n",
" break\n",
" out.append(w)\n",
" return out\n",
"\n",
"\n",
"def label_part(t):\n",
" for a, i in PART_KW:\n",
" if a in t:\n",
" return i\n",
" return -1\n",
"\n",
"\n",
"def label_defect(t):\n",
" for d, ks in DEF_KW.items():\n",
" if any(k in t for k in ks):\n",
" return d\n",
" return \"미분류\"\n",
"\n",
"\n",
"def label_sev(t):\n",
" for s, ks in SEV_KW:\n",
" if any(k in t for k in ks):\n",
" return s\n",
" return -1\n",
"\n",
"\n",
"def label_cause(t):\n",
" for c, ks in CAUSE_KW.items():\n",
" if any(k in t for k in ks):\n",
" return c\n",
" return \"미상\"\n",
"\n",
"# 1) [교육용가상자료] 원천 자료: 생성 자료에 일부러 오류를 섞는다\n",
"df0, use, events = make_qdr(36, seed=1)\n",
"rng = np.random.default_rng(2)\n",
"raw = df0[[\"qdr_id\", \"month\", \"unit\", \"vendor\", \"since_maint\", \"inspect_wait\", \"text\", \"part\", \"defect\"]].copy()\n",
"raw[\"date\"] = [f\"2024-{m % 12 + 1:02d}-{rng.integers(1, 28):02d}\" for m in raw[\"month\"]]\n",
"raw[\"urgent\"] = False\n",
"n = len(raw)\n",
"urg = rng.choice(n, 40, replace=False)\n",
"raw.loc[urg, \"inspect_wait\"] = 0.0; raw.loc[urg, \"urgent\"] = True # 긴급 보고: 대기일 0은 정상 예외\n",
"raw.loc[urg, \"text\"] = \"긴급 보고. \" + raw.loc[urg, \"text\"]\n",
"\n",
"\n",
"def pick(k):\n",
" return rng.choice(n, k, replace=False)\n",
"\n",
"\n",
"i2 = pick(300)\n",
"raw.loc[i2, \"unit\"] = [u.lower() if j % 3 == 0 else u.replace(\"-\", \"\") if j % 3 == 1 else \"VU-\" + str(int(u[-2:])) for j, u in enumerate(raw.loc[i2, \"unit\"])]\n",
"i3 = pick(400); raw.loc[i3, \"date\"] = [d.replace(\"-\", \".\") if j % 2 else d[2:].replace(\"-\", \"/\") for j, d in enumerate(raw.loc[i3, \"date\"])]\n",
"i4 = pick(600); raw.loc[i4, \"text\"] = [t.replace(\". \", \". \").replace(\"에서\", \"에서 \").replace(\"함.\", \"함!!\") + \" ##\" if j % 2 else unicodedata.normalize(\"NFKC\", t).replace(\".\", \".\") for j, t in enumerate(raw.loc[i4, \"text\"])]\n",
"i5 = pick(300); raw.loc[i5, \"text\"] = [t.replace(\"변속기\", \"T/M\").replace(\"배터리\", \"BAT\") for t in raw.loc[i5, \"text\"]]\n",
"i6 = rng.choice(np.where(raw[\"text\"].str.contains(\"누유|균열|부식|제동|과열\"))[0], 400, replace=False); raw.loc[i6, \"text\"] = [t.replace(\"누유\", \"누윳\").replace(\"균열\", \"균렬\").replace(\"부식\", \"부씩\").replace(\"제동\", \"재동\").replace(\"과열\", \"과렬\") for t in raw.loc[i6, \"text\"]]\n",
"i7 = pick(80); raw.loc[i7, \"inspect_wait\"] = -1.0\n",
"i8 = pick(400); raw.loc[i8, \"since_maint\"] = np.nan\n",
"dup = raw.sample(150, random_state=3); raw = pd.concat([raw, dup], ignore_index=True)\n",
"print(f\"원천 자료 {len(raw)}행 (정상 {n}행 + 긴급 보고 {len(urg)}건 + 중복 {len(dup)}행, 오류 주입 {len(i2) + len(i3) + len(i4) + len(i5) + len(i6) + len(i7) + len(i8)}행)\")\n",
"\n",
"# 2) 정제 규칙 10종\n",
"log = []\n",
"\n",
"\n",
"def rule(name, cnt):\n",
" log.append((name, int(cnt)))\n",
"\n",
"\n",
"df = raw.copy()\n",
"before_len = len(df)\n",
"df = df.drop_duplicates(\"qdr_id\").reset_index(drop=True); rule(\"R1 중복 QDR 제거\", before_len - len(df))\n",
"fix = lambda u: \"VU-%02d\" % int(re.sub(r\"\\D\", \"\", u))\n",
"bad = ~df[\"unit\"].str.fullmatch(r\"VU-\\d{2}\"); df[\"unit\"] = df[\"unit\"].map(fix); rule(\"R2 부대 코드 표기 통일\", bad.sum())\n",
"bad = ~df[\"date\"].str.fullmatch(r\"\\d{4}-\\d{2}-\\d{2}\")\n",
"df[\"date\"] = [pd.to_datetime(d.replace(\".\", \"-\"), format=\"%Y-%m-%d\" if len(d) == 10 else \"%y-%m-%d\", errors=\"coerce\") if \"/\" not in d else pd.to_datetime(d, format=\"%y/%m/%d\") for d in df[\"date\"]]\n",
"rule(\"R3 일자 형식 통일\", bad.sum())\n",
"t0 = df[\"text\"].copy()\n",
"df[\"text\"] = df[\"text\"].map(lambda t: re.sub(r\"\\s+\", \" \", re.sub(r\"[!#~*@^]+\", \" \", unicodedata.normalize(\"NFKC\", t))).strip()); rule(\"R4 텍스트 정규화(전각·특수문자·공백)\", (df[\"text\"] != t0).sum())\n",
"c = df[\"text\"].str.contains(\"T/M|BAT\"); df[\"text\"] = df[\"text\"].map(norm); rule(\"R5 약어 확장(T/M, BAT)\", c.sum())\n",
"c = t0.map(lambda t: any(k in t for k in TYPO))\n",
"rule(\"R6 오타 교정(사전 기반)\", c.sum())\n",
"bad = df[\"inspect_wait\"] < 0; df.loc[bad, \"inspect_wait\"] = np.nan; rule(\"R7 불가능값(음수 대기일) 결측 처리\", bad.sum())\n",
"miss = df[\"since_maint\"].isna() | df[\"inspect_wait\"].isna()\n",
"X = pd.DataFrame({\"part\": df[\"part\"], \"vendor\": df[\"vendor\"].astype(\"category\").cat.codes * 5, \"wait\": df[\"inspect_wait\"], \"since\": df[\"since_maint\"], \"urgent\": df[\"urgent\"] * 20})\n",
"df[[\"inspect_wait\", \"since_maint\"]] = KNNImputer(n_neighbors=5).fit_transform(X)[:, 2:4]; rule(\"R8 대기일·정비후경과일 KNN 보정\", miss.sum())\n",
"df[\"part_label\"] = df[\"text\"].map(label_part); df[\"defect_label\"] = df[\"text\"].map(label_defect); rule(\"R9 부위·결함 유형 키워드 라벨링\", len(df))\n",
"flag = (df[\"inspect_wait\"] > 40) | ((df[\"inspect_wait\"] == 0) & ~df[\"urgent\"])\n",
"df[\"flag\"] = flag\n",
"rule(\"R10 긴급 보고 보존(일반 이상값은 표시만)\", int(df[\"urgent\"].sum()))\n",
"print(\"\\n[적용 규칙과 처리 건수]\")\n",
"for nm, c in log:\n",
" print(f\" {nm:40s} {c:6d}건\")\n",
"\n",
"# 3) 정합성 검증 + 라벨링 정확도\n",
"truth = df.merge(df0[[\"qdr_id\", \"part\", \"defect\"]], on=\"qdr_id\", suffixes=(\"\", \"_t\"))\n",
"rawlab = raw.drop_duplicates(\"qdr_id\").assign(pl=lambda d: d[\"text\"].map(label_part), dl=lambda d: d[\"text\"].map(label_defect))\n",
"rawacc_p, rawacc_d = (rawlab[\"pl\"] == rawlab[\"part\"]).mean(), (rawlab[\"dl\"] == rawlab[\"defect\"]).mean()\n",
"acc_p, acc_d = (truth[\"part_label\"] == truth[\"part_t\"]).mean(), (truth[\"defect_label\"] == truth[\"defect_t\"]).mean()\n",
"checks = {\n",
" \"R1 ID 유일\": df[\"qdr_id\"].is_unique, \"R2 부대 코드 형식\": df[\"unit\"].str.fullmatch(r\"VU-\\d{2}\").all(), \"R3 일자 변환\": df[\"date\"].notna().all(),\n",
" \"R4 특수문자·이중 공백 없음\": not df[\"text\"].str.contains(r\"[!#]|\\s\\s|.\").any(), \"R5 약어 없음\": not df[\"text\"].str.contains(\"T/M|BAT\").any(),\n",
" \"R6 오타 없음\": not df[\"text\"].map(lambda t: any(k in t for k in TYPO)).any(), \"R7 음수 대기일 없음\": (df[\"inspect_wait\"] >= 0).all(),\n",
" \"R8 결측 없음\": df[[\"inspect_wait\", \"since_maint\"]].notna().all().all(), \"R9 부위 라벨 부여율 100%\": (df[\"part_label\"] >= 0).all(),\n",
" \"R10 긴급 보고 40건 보존\": int(df[\"urgent\"].sum()) == 40}\n",
"npass = sum(checks.values())\n",
"print(f\"\\n정합성 검증 {npass}/10 통과, 통과율 {npass / 10:.0%} (목표 100%: {'달성' if npass == 10 else '미달'})\")\n",
"print(f\"라벨링 정확도: 부위 {rawacc_p:.1%} -> {acc_p:.1%}, 결함 유형 {rawacc_d:.1%} -> {acc_d:.1%} (정제 전 -> 후)\")\n",
"print(f\"긴급 보고 {int(df['urgent'].sum())}건 보존, 일반 이상 표시 {int((df['flag'] & ~df['urgent']).sum())}건\")\n",
"\n",
"# 4) 정제 전후 기술통계\n",
"def stats(d, after):\n",
" return {\"행 수\": len(d), \"부대 표기 종류 수\": d[\"unit\"].nunique(), \"서술 평균 길이(자)\": round(d[\"text\"].str.len().mean(), 1),\n",
" \"대기일 최소\": round(d[\"inspect_wait\"].min(), 1), \"대기일 결측 수\": int(d[\"inspect_wait\"].isna().sum()), \"정비후경과일 결측 수\": int(d[\"since_maint\"].isna().sum())}\n",
"print(\"\\n[정제 전후 기술통계]\")\n",
"print(pd.DataFrame({\"정제 전\": stats(raw, False), \"정제 후\": stats(df, True)}).to_string())\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"<details>\n",
"<summary><b>💡 [참고] 미션 02 예상 실행 결과 (Output)</b></summary>\n",
"\n",
"```text\n",
"원천 자료 9371행 (정상 9221행 + 긴급 보고 40건 + 중복 150행, 오류 주입 2480행)\n",
"\n",
"[적용 규칙과 처리 건수]\n",
" R1 중복 QDR 제거 150건\n",
" R2 부대 코드 표기 통일 238건\n",
" R3 일자 형식 통일 400건\n",
" R4 텍스트 정규화(전각·특수문자·공백) 600건\n",
" R5 약어 확장(T/M, BAT) 294건\n",
" R6 오타 교정(사전 기반) 400건\n",
" R7 불가능값(음수 대기일) 결측 처리 80건\n",
" R8 대기일·정비후경과일 KNN 보정 474건\n",
" R9 부위·결함 유형 키워드 라벨링 9221건\n",
" R10 긴급 보고 보존(일반 이상값은 표시만) 40건\n",
"\n",
"정합성 검증 10/10 통과, 통과율 100% (목표 100%: 달성)\n",
"라벨링 정확도: 부위 98.5% -> 100.0%, 결함 유형 96.8% -> 100.0% (정제 전 -> 후)\n",
"긴급 보고 40건 보존, 일반 이상 표시 1건\n",
"\n",
"[정제 전후 기술통계]\n",
" 정제 전 정제 후\n",
"행 수 9371.0 9221.0\n",
"부대 표기 종류 수 98.0 30.0\n",
"서술 평균 길이(자) 56.4 56.1\n",
"대기일 최소 -1.0 0.0\n",
"대기일 결측 수 0.0 0.0\n",
"정비후경과일 결측 수 406.0 0.0\n",
"```\n",
"</details>\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## 🎯 미션 03. 빈도 분석 기반 1차 다발 결함 부품 도출 Baseline 알고리즘 구현\n",
"**소요시간**: 약 2.5시간\n",
"\n",
"### 📋 미션 배경 및 수행 지침\n",
"#### 미션 배경 및 당위성\n",
"\n",
"빈도 분석 기반 다발 결함 부품 도출 Baseline 알고리즘을 구현한다. 실제 전술 현장에서는 단순 탐지에 그치지 않고 신속한 위협 판단과 신뢰도 높은 의사결정이 필수적이므로, **빈도 분석 기반 1차 다발 결함 부위 도출 Baseline을 구현하고, 탐색적 분석으로 빈도 기준의 한계와 외주정비 검사 병목, 핵심 관리 대상을 찾는다.** 과업을 완수하여 오판을 방지하고 작전 대응력을 극대화해야 합니다.\n",
"\n",
"이 문제를 해결하기 위해 본 미션에서는 **학습 기간 건수 상위 5개 부위를 다발 결함 부위로 지정해 검증 기간 적중률과 치명도 가중 지수 순위와의 차이 확인하고 유효성을 점검하는** 실습을 체계적으로 수행합니다.\n",
"\n",
"학습자는 실습가이드에 제시된 총 4개 세부 활동(기본 기능 구현 ➔ 결과 지표 검증 ➔ 파라미터 튜닝 및 도전과제)을 순차적으로 진행하며 핵심 판정 기준을 달성합니다.\n",
"\n",
"**이번 결과물:** `notebooks/qlt05_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": [
"\"\"\"빈도 분석 기반 1차 다발 결함 부위 도출 Baseline(학습 기간 건수 상위 5개 부위)의 적중률과 한계(치명도·비용 순위와의 차이)를 확인하고, 탐색적 분석(EDA 차트 8종, 월간 QDR 건수와 피처의 상관계수·p-value, 외주정비 업체별 검사 대기·원인, 건수·비용 파레토 ABC 분류)으로 핵심 관리 대상을 식별한다.\"\"\"\n",
"import numpy as np\n",
"import pandas as pd\n",
"import matplotlib.pyplot as plt\n",
"from scipy.stats import spearmanr\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# [교육용가상자료] 가상 야전 장비 품질결함보고(QDR) 생성기 (실제 부대·장비·업체·정비 실적 없음)\n",
"# 가상 장비 부위 12종, 결함 유형 8종, 치명도(Critical/Major/Minor), 원인 5종, 가상 외주정비 업체 4곳 + 부대정비.\n",
"# QDR 1건마다 한국어 서술 텍스트를 만들고, 월별 건수는 계절·추세·훈련 강도(usage)·일부 제조 불량 이벤트를 따른다.\n",
"# ---------------------------------------------------------------------------\n",
"PARTS = [ # (이름, 별칭, 월 기준 건수, 계절 진폭, 정점 월, (Critical, Major 비율), 가상 단가지수, 제조사, 주요 결함 가중치)\n",
" (\"엔진 연료계통\", [\"연료계통\", \"연료 라인\", \"연료 펌프\"], 26, 0.15, 7, (0.10, 0.35), 6.0, \"MK1\", \"누유:4 마모:2 체결불량:2 오작동:2\"),\n",
" (\"변속기\", [\"변속기\", \"T/M\", \"미션\"], 22, 0.10, 8, (0.12, 0.38), 9.0, \"MK1\", \"마모:4 누유:2 과열:3 오작동:1\"),\n",
" (\"제동 장치\", [\"제동 장치\", \"브레이크\", \"제동 라인\"], 20, 0.20, 12, (0.26, 0.40), 5.0, \"MK2\", \"마모:4 균열:2 누유:2 체결불량:2\"),\n",
" (\"조향 장치\", [\"조향 장치\", \"핸들 연동부\", \"조향 로드\"], 14, 0.10, 6, (0.30, 0.40), 5.0, \"MK2\", \"체결불량:4 균열:2 마모:3 오작동:1\"),\n",
" (\"냉각 계통\", [\"냉각 계통\", \"라디에이터\", \"냉각수 라인\"], 24, 0.30, 7, (0.05, 0.30), 3.0, \"MK3\", \"누유:4 과열:4 부식:2\"),\n",
" (\"전기·배터리\", [\"배터리\", \"전기 계통\", \"발전기\"], 28, 0.35, 1, (0.04, 0.28), 2.5, \"MK3\", \"접촉불량:4 부식:2 과열:2 오작동:2\"),\n",
" (\"통신 모듈\", [\"통신 모듈\", \"무전기 연동부\", \"안테나 접속부\"], 16, 0.05, 4, (0.03, 0.25), 4.0, \"MK4\", \"접촉불량:5 오작동:4 부식:1\"),\n",
" (\"유압 계통\", [\"유압 계통\", \"유압 호스\", \"유압 실린더\"], 18, 0.10, 9, (0.10, 0.35), 4.5, \"MK2\", \"누유:5 균열:2 마모:2 체결불량:1\"),\n",
" (\"현가 장치\", [\"현가 장치\", \"서스펜션\", \"완충기\"], 15, 0.08, 5, (0.06, 0.30), 3.5, \"MK4\", \"마모:4 균열:3 체결불량:2 부식:1\"),\n",
" (\"차체 용접부\", [\"차체 용접부\", \"프레임 용접부\", \"차체 이음부\"], 10, 0.05, 10, (0.28, 0.40), 7.0, \"MK1\", \"균열:6 부식:3 체결불량:1\"),\n",
" (\"광학·조준 장비\", [\"광학 장비\", \"조준경\", \"관측 장비\"], 8, 0.05, 3, (0.03, 0.22), 8.0, \"MK4\", \"오작동:4 접촉불량:3 부식:2 체결불량:1\"),\n",
" (\"공기 정화 필터\", [\"공기 필터\", \"흡기 필터\", \"정화 장치\"], 12, 0.25, 4, (0.01, 0.12), 1.0, \"MK3\", \"마모:5 부식:2 체결불량:3\")]\n",
"NP = len(PARTS)\n",
"DEFECTS = { # 결함 유형: 증상 표현(동의어)\n",
" \"균열\": [\"균열이 발견됨\", \"크랙이 확인됨\", \"갈라짐이 있음\"], \"누유\": [\"누유가 발생함\", \"오일이 새어 나옴\", \"누설 흔적이 있음\"],\n",
" \"마모\": [\"마모가 심함\", \"과도한 마모가 확인됨\", \"닳은 흔적이 있음\"], \"부식\": [\"부식이 진행됨\", \"녹이 발생함\", \"산화 흔적이 있음\"],\n",
" \"접촉불량\": [\"접촉 불량이 발생함\", \"커넥터 단선이 의심됨\", \"신호가 끊김\"], \"체결불량\": [\"볼트 풀림이 확인됨\", \"체결 상태가 불량함\", \"고정부가 헐거움\"],\n",
" \"과열\": [\"과열 경고가 발생함\", \"온도가 과도하게 상승함\", \"열이 심하게 남\"], \"오작동\": [\"오작동이 반복됨\", \"작동이 불안정함\", \"간헐적으로 멈춤\"]}\n",
"DNAMES = list(DEFECTS)\n",
"SEV = [\"Minor\", \"Major\", \"Critical\"]\n",
"SEV_CUE = {0: [\"경미한 수준임\", \"운용에는 지장이 없음\", \"육안 점검상 사소한 이상임\", \"기능에는 영향이 없음\"],\n",
" 1: [\"성능 저하가 있음\", \"조속한 정비가 필요함\", \"출력이 떨어지는 상태임\", \"기능이 부분적으로 제한됨\"],\n",
" 2: [\"즉시 운용 중지함\", \"안전에 위험이 있어 사용 불가\", \"작동 정지로 임무 수행이 어려움\", \"인명 안전 위협이 우려됨\"]}\n",
"CAUSES = [\"제조 불량\", \"정비 품질\", \"운용 부주의\", \"설계 취약\", \"자연 노후\"]\n",
"CAUSE_CUE = {\n",
" \"제조 불량\": [\"신품 인수 직후 발생함\", \"출고 상태에서 이미 이상이 있었음\", \"인수 후 얼마 지나지 않아 발생함\"],\n",
" \"정비 품질\": [\"외주 정비 후 얼마 되지 않아 재발함\", \"정비 후 재조립 상태가 불량함\", \"정비 직후 같은 부위에서 다시 발생함\"],\n",
" \"운용 부주의\": [\"험지 기동 중 충격 이후 발생함\", \"과부하 운용 후 발생함\", \"점검 없이 연속 운용한 뒤 발생함\"],\n",
" \"설계 취약\": [\"동일 증상이 다른 차량에서도 반복됨\", \"구조적으로 취약한 부위로 보임\", \"여러 부대에서 같은 형태로 보고됨\"],\n",
" \"자연 노후\": [\"사용 연한이 도래함\", \"장기간 사용으로 노후됨\", \"수명 주기 말기에 발생함\"]}\n",
"VENDORS = [\"외주A\", \"외주B\", \"외주C\", \"외주D\", \"부대정비\"]\n",
"ACTIONS = [\"정비소 입고를 요청함\", \"현장 임시 조치 후 운용 중임\", \"부품 교환을 요청함\", \"재검사를 요청함\", \"원인 확인이 필요함\"]\n",
"MAKER_OF = [p[7] for p in PARTS]\n",
"PRICE = np.array([p[6] for p in PARTS])\n",
"\n",
"\n",
"def usage_index(months, seed=0):\n",
" \"\"\"월별 훈련 강도(미리 알 수 있는 계획 값): 연 1회 정점을 갖는 계절 + 작은 잡음\"\"\"\n",
" # [TODO : 학생 실습 3] 빈도 분석 기반 1차 다발 결함 부품 도출 Baseline 알고리즘 구현 관련 핵심 로직을 구현하세요.\n",
" # 힌트: 상단 주석 및 데이터 스키마/지침을 참고하여 작성합니다.\n",
" # <<< 여기에 코드를 작성하세요 >>>\n",
" pass # 작성 후 pass를 제거하세요.\n",
"def make_qdr(months=36, seed=1, scale=1.0, surge=True):\n",
" \"\"\"반환: QDR DataFrame(건당 1행), 월별 usage, 급증 이벤트 목록\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" use = usage_index(months, seed)\n",
" events = []\n",
" if surge:\n",
" for _ in range(max(2, months // 12 * 2)):\n",
" events.append((int(rng.integers(NP)), int(rng.integers(6, months))))\n",
" rows = []\n",
" wdef = [np.array([float(x.split(\":\")[1]) for x in p[8].split()]) for p in PARTS]\n",
" ddef = [[x.split(\":\")[0] for x in p[8].split()] for p in PARTS]\n",
" n = 0\n",
" for m in range(months):\n",
" for pi, p in enumerate(PARTS):\n",
" lam = p[2] * scale * (1 + 0.010 * m) * use[m] * (1 + p[3] * np.cos(2 * np.pi * (m % 12 - p[4]) / 12))\n",
" ev = (pi, m) in events\n",
" k = rng.poisson(lam * (2.6 if ev else 1.0))\n",
" for _ in range(k):\n",
" rows.append((n, m, pi, ev)); n += 1\n",
" N = len(rows)\n",
" A = np.array(rows)\n",
" month, part, ev = A[:, 1], A[:, 2], A[:, 3].astype(bool)\n",
" defect = np.array([rng.choice(ddef[i], p=wdef[i] / wdef[i].sum()) for i in part])\n",
" cr = np.array([PARTS[i][5][0] for i in part]); ma = np.array([PARTS[i][5][1] for i in part])\n",
" u = rng.random(N)\n",
" sev = np.where(u < cr, 2, np.where(u < cr + ma, 1, 0))\n",
" vendor = rng.choice(VENDORS, N, p=[0.25, 0.2, 0.2, 0.15, 0.2])\n",
" # 원인: 기본 비율에 부위(제조사)·업체·이벤트 효과를 곱한다\n",
" base = np.array([0.22, 0.20, 0.20, 0.16, 0.22])\n",
" P = np.tile(base, (N, 1))\n",
" P[vendor == \"외주B\", 1] *= 2.6\n",
" P[ev, 0] *= 5.0\n",
" design = np.isin(part, [2, 3, 7]); P[design, 3] *= 2.2\n",
" P[np.isin(part, [4, 5, 11]), 0] *= 2.0 # 제조사 MK3 납품 부위는 제조 불량 비중이 높다\n",
" P[defect == \"마모\", 4] *= 1.6; P[defect == \"누유\", 4] *= 1.3\n",
" P /= P.sum(axis=1, keepdims=True)\n",
" cause = np.array([CAUSES[rng.choice(5, p=P[i])] for i in range(N)])\n",
" since = np.where(cause == \"정비 품질\", rng.exponential(35, N), rng.exponential(160, N)).clip(1, 720).round().astype(int)\n",
" wait = (rng.lognormal(1.3, 0.5, N) * np.where(vendor == \"외주B\", 1.7, np.where(vendor == \"부대정비\", 0.6, 1.0)) * (0.7 + 0.5 * use[month])).round(1)\n",
" reins = rng.random(N) < np.where(cause == \"정비 품질\", 0.6, 0.08)\n",
" texts = []\n",
" for i in range(N):\n",
" pt = PARTS[part[i]]\n",
" cl = [f\"{rng.choice(pt[1])}에서 {rng.choice(DEFECTS[defect[i]])}\"]\n",
" if rng.random() < 0.7: cl.append(rng.choice(SEV_CUE[sev[i]]))\n",
" if rng.random() < 0.65: # 원인 단서: 8%는 실제 원인과 다른 단서가 적힌다(보고자의 추정)\n",
" cc = cause[i] if rng.random() > 0.08 else rng.choice(CAUSES)\n",
" cl.append(rng.choice(CAUSE_CUE[cc]))\n",
" cl.append(rng.choice(ACTIONS))\n",
" if rng.random() < 0.5: cl.insert(0, f\"VU-{rng.integers(1, 31):02d} 소속\")\n",
" texts.append(\". \".join(cl) + \".\")\n",
" df = pd.DataFrame({\"qdr_id\": [f\"Q{seed}{i:06d}\" for i in range(N)], \"month\": month, \"unit\": [f\"VU-{x:02d}\" for x in rng.integers(1, 31, N)],\n",
" \"part\": part, \"defect\": defect, \"sev\": sev, \"cause\": cause, \"vendor\": vendor, \"maker\": [MAKER_OF[i] for i in part],\n",
" \"since_maint\": since, \"inspect_wait\": wait, \"reinspect\": reins, \"event\": ev, \"text\": texts})\n",
" df[\"cost\"] = PRICE[df[\"part\"]] * np.array([1.0, 2.0, 4.0])[df[\"sev\"]] # [교육용가상자료] 수리·조치 비용지수\n",
" return df, use, events\n",
"\n",
"df, use, events = make_qdr(36, seed=1)\n",
"names = [p[0] for p in PARTS]\n",
"tr, va = df[df[\"month\"] < 24], df[df[\"month\"] >= 24]\n",
"print(f\"[교육용가상자료] QDR {len(df)}건, 부위 {NP}종, 36개월 (학습 24개월 {len(tr)}건, 검증 12개월 {len(va)}건)\")\n",
"\n",
"# 1) Baseline: 학습 기간 건수 상위 5개 부위를 다발 결함 부위로 지정\n",
"cnt_tr = tr.groupby(\"part\").size().sort_values(ascending=False)\n",
"top_base = list(cnt_tr.index[:5])\n",
"cnt_va = va.groupby(\"part\").size().sort_values(ascending=False)\n",
"vuln = lambda d: d.groupby(\"part\")[\"sev\"].apply(lambda s: np.array([1, 3, 9])[s].sum()).sort_values(ascending=False)\n",
"v_va = vuln(va)\n",
"monthly_hit = np.mean([len(set(top_base) & set(g.groupby(\"part\").size().sort_values(ascending=False).index[:5])) / 5 for _, g in va.groupby(\"month\")])\n",
"print(f\"Baseline 상위 5 부위: {', '.join(names[i] for i in top_base)}\")\n",
"print(f\" 검증 기간 건수 상위 5와 일치 {len(set(top_base) & set(cnt_va.index[:5]))}/5, 월별 평균 적중률 {monthly_hit:.0%}\")\n",
"print(f\" 치명도 가중 취약성 지수 상위 5와 일치 {len(set(top_base) & set(v_va.index[:5]))}/5 -> 건수만 보면 놓치는 부위: \" + \", \".join(names[i] for i in set(v_va.index[:5]) - set(top_base)))\n",
"crit = va[va[\"sev\"] == 2]\n",
"miss = (~crit[\"part\"].isin(top_base)).mean()\n",
"print(f\" Critical 결함 {len(crit)}건 중 Baseline 대상 밖 부위 비중 {miss:.0%}\")\n",
"\n",
"# 2) 파레토(ABC): 건수와 조치 비용\n",
"par = pd.DataFrame({\"부위\": [names[i] for i in cnt_tr.index], \"건수\": cnt_tr.values}).set_index(\"부위\")\n",
"par[\"비용지수\"] = [df.loc[df[\"part\"] == i, \"cost\"].sum() for i in cnt_tr.index]\n",
"pc = par.sort_values(\"비용지수\", ascending=False)\n",
"pc[\"누적비율\"] = pc[\"비용지수\"].cumsum() / pc[\"비용지수\"].sum()\n",
"pc[\"등급\"] = np.where(pc[\"누적비율\"] <= 0.7, \"A\", np.where(pc[\"누적비율\"] <= 0.9, \"B\", \"C\")); pc.iloc[0, 3] = \"A\"\n",
"print(\"\\n[파레토] 조치 비용 기준(가상 단가지수)\")\n",
"print(pc.assign(비용지수=lambda d: d[\"비용지수\"].round(0), 누적비율=lambda d: d[\"누적비율\"].map(\"{:.0%}\".format)).head(6).to_string())\n",
"top20 = pc.head(int(np.ceil(NP * 0.2)))\n",
"print(f\"상위 20% 부위({', '.join(top20.index)})가 전체 조치 비용의 {top20['비용지수'].sum() / pc['비용지수'].sum():.0%}, A등급 {int((pc['등급'] == 'A').sum())}종\")\n",
"\n",
"# 3) 상관분석: 월간 QDR 건수와 월별 피처\n",
"mon = df.groupby(\"month\").agg(n=(\"qdr_id\", \"size\"), wait=(\"inspect_wait\", \"mean\"), re=(\"reinspect\", \"mean\"), crit=(\"sev\", lambda s: (s == 2).mean()),\n",
" ven_b=(\"vendor\", lambda s: (s == \"외주B\").mean()), short=(\"since_maint\", lambda s: (s < 60).mean()),\n",
" mfg=(\"cause\", lambda s: (s == \"제조 불량\").mean()), cost=(\"cost\", \"mean\"), units=(\"unit\", \"nunique\"),\n",
" short_n=(\"since_maint\", lambda s: (s < 60).sum()))\n",
"mon[\"usage\"] = use; mon[\"trend\"] = np.arange(36); mon[\"season\"] = np.cos(2 * np.pi * (mon[\"trend\"] % 12 - 7) / 12); mon[\"events\"] = [sum(1 for e in events if e[1] == m) for m in range(36)]\n",
"rows = []\n",
"for c, lab in [(\"usage\", \"훈련 강도\"), (\"trend\", \"경과 월(추세)\"), (\"season\", \"계절 요인\"), (\"wait\", \"평균 검사 대기일\"), (\"re\", \"재검사율\"), (\"short\", \"정비 후 60일 이내 비중\"),\n",
" (\"mfg\", \"제조 불량 비중\"), (\"cost\", \"건당 평균 조치 비용\"), (\"ven_b\", \"외주B 비중\"), (\"crit\", \"Critical 비중\"), (\"units\", \"보고 부대 수\"), (\"short_n\", \"정비 후 60일 이내 건수\")]:\n",
" r, p = spearmanr(mon[c], mon[\"n\"]); rows.append((lab, r, p))\n",
"cor = pd.DataFrame(rows, columns=[\"피처\", \"rho\", \"p\"]); cor[\"유의\"] = (cor[\"p\"] < 0.05) & (cor[\"rho\"].abs() >= 0.3)\n",
"print(\"\\n[상관분석] 피처 | 스피어만 rho | p-value | 유의(p<0.05, |rho|>=0.3)\")\n",
"for x in cor.sort_values(\"rho\", key=abs, ascending=False).head(7).itertuples():\n",
" print(f\" {x.피처:16s} {x.rho:+.2f} {x.p:.4f} {'유의' if x.유의 else '-'}\")\n",
"nsig = int(cor[\"유의\"].sum())\n",
"\n",
"# 4) 외주정비 업체별 검사 병목과 원인\n",
"ven = df.groupby(\"vendor\").agg(건수=(\"qdr_id\", \"size\"), 대기일=(\"inspect_wait\", \"mean\"), 재검사율=(\"reinspect\", \"mean\"), 정비품질원인=(\"cause\", lambda s: (s == \"정비 품질\").mean()))\n",
"print(\"\\n[업체별 검사 실적] 직전 정비 수행처 기준\")\n",
"print(ven.round(2).to_string())\n",
"b, o = df[df[\"vendor\"] == \"외주B\"], df[~df[\"vendor\"].isin([\"외주B\"])]\n",
"print(f\"외주B: 대기일 {b['inspect_wait'].mean():.1f}일(타 수행처 {o['inspect_wait'].mean():.1f}일), '정비 품질' 원인 {np.mean(b['cause'] == '정비 품질'):.0%}(타 {np.mean(o['cause'] == '정비 품질'):.0%}), 정비 후 60일 이내 재발 {np.mean(b['since_maint'] < 60):.0%}\")\n",
"\n",
"# 5) EDA 차트 8종\n",
"fig, ax = plt.subplots(2, 4, figsize=(16, 7))\n",
"H = df.pivot_table(index=\"part\", columns=\"month\", values=\"qdr_id\", aggfunc=\"size\", fill_value=0)\n",
"ax[0, 0].imshow(H.values, aspect=\"auto\", cmap=\"Reds\"); ax[0, 0].set_title(\"1 QDR heatmap (part x month)\")\n",
"ax[0, 1].plot(mon[\"n\"].values, label=\"QDR\"); ax[0, 1].plot(mon[\"usage\"].values * mon[\"n\"].mean(), label=\"usage x mean\"); ax[0, 1].legend(fontsize=6); ax[0, 1].set_title(\"2 Monthly QDR vs usage\")\n",
"ax[0, 2].bar(range(NP), pc[\"비용지수\"].values); ax[0, 2].plot(range(NP), pc[\"누적비율\"].values * pc[\"비용지수\"].max(), \"r-o\"); ax[0, 2].set_title(\"3 Pareto of repair cost\")\n",
"sv = pd.crosstab(df[\"part\"], df[\"sev\"], normalize=\"index\"); ax[0, 3].bar(range(NP), sv[0], label=\"Minor\"); ax[0, 3].bar(range(NP), sv[1], bottom=sv[0], label=\"Major\"); ax[0, 3].bar(range(NP), sv[2], bottom=sv[0] + sv[1], label=\"Critical\"); ax[0, 3].legend(fontsize=6); ax[0, 3].set_title(\"4 Severity share by part\")\n",
"ax[1, 0].boxplot([df.loc[df[\"vendor\"] == v, \"inspect_wait\"] for v in VENDORS], labels=[v[-1] for v in VENDORS]); ax[1, 0].set_title(\"5 Inspection wait by vendor\")\n",
"cv = pd.crosstab(df[\"vendor\"], df[\"cause\"], normalize=\"index\"); ax[1, 1].imshow(cv.values, cmap=\"Blues\"); ax[1, 1].set_title(\"6 Cause share by vendor\")\n",
"ax[1, 2].hist([df.loc[df[\"cause\"] == c, \"since_maint\"].clip(0, 400) for c in CAUSES[:2]], bins=20, label=[\"mfg\", \"maint\"]); ax[1, 2].legend(fontsize=6); ax[1, 2].set_title(\"7 Days since maintenance\")\n",
"ax[1, 3].scatter(mon[\"wait\"], mon[\"n\"], s=12); ax[1, 3].set_title(\"8 Wait vs monthly QDR\")\n",
"plt.tight_layout(); plt.show()\n",
"\n",
"print(f\"\\nEDA 차트 8종 생성(목표 8종 이상: 달성), 유의한 피처 {nsig}개 (목표 5개 이상: {'달성' if nsig >= 5 else '미달'})\")\n",
"print(\"핵심 발견: 1) 건수 상위만 보면 조향 장치처럼 치명도 높은 부위를 놓침 2) 외주B는 검사 대기가 길고 정비 후 조기 재발 비중이 높음 3) 월별 QDR는 훈련 강도와 계절에 연동\")\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"<details>\n",
"<summary><b>💡 [참고] 미션 03 예상 실행 결과 (Output)</b></summary>\n",
"\n",
"```text\n",
"[교육용가상자료] QDR 9221건, 부위 12종, 36개월 (학습 24개월 5906건, 검증 12개월 3315건)\n",
"Baseline 상위 5 부위: 전기·배터리, 엔진 연료계통, 냉각 계통, 제동 장치, 변속기\n",
" 검증 기간 건수 상위 5와 일치 5/5, 월별 평균 적중률 80%\n",
" 치명도 가중 취약성 지수 상위 5와 일치 4/5 -> 건수만 보면 놓치는 부위: 조향 장치\n",
" Critical 결함 366건 중 Baseline 대상 밖 부위 비중 46%\n",
"\n",
"[파레토] 조치 비용 기준(가상 단가지수)\n",
" 건수 비용지수 누적비율 등급\n",
"부위 \n",
"변속기 558 13464.0 18% A\n",
"엔진 연료계통 770 11550.0 33% A\n",
"제동 장치 596 10220.0 47% A\n",
"조향 장치 397 7155.0 56% A\n",
"차체 용접부 277 6776.0 65% A\n",
"유압 계통 462 5607.0 73% B\n",
"상위 20% 부위(변속기, 엔진 연료계통, 제동 장치)가 전체 조치 비용의 47%, A등급 5종\n",
"\n",
"[상관분석] 피처 | 스피어만 rho | p-value | 유의(p<0.05, |rho|>=0.3)\n",
" 정비 후 60일 이내 건수 +0.94 0.0000 유의\n",
" 평균 검사 대기일 +0.83 0.0000 유의\n",
" 훈련 강도 +0.82 0.0000 유의\n",
" 계절 요인 +0.78 0.0000 유의\n",
" 경과 월(추세) +0.39 0.0200 유의\n",
" 외주B 비중 -0.22 0.1928 -\n",
" Critical 비중 +0.16 0.3583 -\n",
"\n",
"[업체별 검사 실적] 직전 정비 수행처 기준\n",
" 건수 대기일 재검사율 정비품질원인\n",
"vendor \n",
"부대정비 1811 3.11 0.18 0.18\n",
"외주A 2231 5.00 0.17 0.18\n",
"외주B 1916 8.44 0.27 0.36\n",
"외주C 1864 5.08 0.16 0.17\n",
"외주D 1399 4.98 0.17 0.17\n",
"외주B: 대기일 8.4일(타 수행처 4.5일), '정비 품질' 원인 36%(타 18%), 정비 후 60일 이내 재발 49%\n",
"\n",
"EDA 차트 8종 생성(목표 8종 이상: 달성), 유의한 피처 5개 (목표 5개 이상: 달성)\n",
"핵심 발견: 1) 건수 상위만 보면 조향 장치처럼 치명도 높은 부위를 놓침 2) 외주B는 검사 대기가 길고 정비 후 조기 재발 비중이 높음 3) 월별 QDR는 훈련 강도와 계절에 연동\n",
"```\n",
"</details>\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## 🎯 미션 04. 텍스트 임베딩 및 토픽 모델링 기반 장비 구조적 품질 취약점 자동 군집화 모델 고도화\n",
"**소요시간**: 약 3.0시간\n",
"\n",
"### 📋 미션 배경 및 수행 지침\n",
"#### 미션 배경 및 당위성\n",
"\n",
"토픽 모델링 기반 장비 구조적 품질 취약점 자동 군집화 모델을 고도화한다. 실제 전술 현장에서는 단순 탐지에 그치지 않고 신속한 위협 판단과 신뢰도 높은 의사결정이 필수적이므로, **텍스트 임베딩(TF-IDF)과 토픽 모델링(NMF)으로 장비 구조적 품질 취약점을 자동 군집화하고, 부위별 월간 QDR 건수 기준 모델을 파생 피처로 고도화한다.** 과업을 완수하여 오판을 방지하고 작전 대응력을 극대화해야 합니다.\n",
"\n",
"이 문제를 해결하기 위해 본 미션에서는 **결함 서술의 핵심 문장을 TF-IDF로 벡터화하고 NMF 토픽 36개로 군집화해 부위·결함 라벨과의 일치도(NMI) 확인하고 유효성을 점검하는** 실습을 체계적으로 수행합니다.\n",
"\n",
"학습자는 실습가이드에 제시된 총 4개 세부 활동(기본 기능 구현 ➔ 결과 지표 검증 ➔ 파라미터 튜닝 및 도전과제)을 순차적으로 진행하며 핵심 판정 기준을 달성합니다.\n",
"\n",
"**이번 결과물:** `src/features/qlt05_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": [
"\"\"\"QDR 서술 텍스트를 TF-IDF로 벡터화하고 NMF 토픽 모델링으로 장비 구조적 품질 취약점을 자동 군집화해 부위·결함 라벨과의 일치도(NMI)와 취약 군집(치명도·설계 취약 비중)을 확인하고, 토픽 비중 등 파생 피처 14개로 부위별 월간 QDR 건수 기준 모델을 만들어 시계열 교차검증 MAPE를 평가하고 순열 중요도로 피처를 줄인다.\"\"\"\n",
"import re\n",
"import unicodedata\n",
"import numpy as np\n",
"import pandas as pd\n",
"from sklearn.decomposition import NMF\n",
"from sklearn.ensemble import GradientBoostingRegressor\n",
"from sklearn.feature_extraction.text import TfidfVectorizer\n",
"from sklearn.inspection import permutation_importance\n",
"from sklearn.metrics import normalized_mutual_info_score\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# [교육용가상자료] 가상 야전 장비 품질결함보고(QDR) 생성기 (실제 부대·장비·업체·정비 실적 없음)\n",
"# 가상 장비 부위 12종, 결함 유형 8종, 치명도(Critical/Major/Minor), 원인 5종, 가상 외주정비 업체 4곳 + 부대정비.\n",
"# QDR 1건마다 한국어 서술 텍스트를 만들고, 월별 건수는 계절·추세·훈련 강도(usage)·일부 제조 불량 이벤트를 따른다.\n",
"# ---------------------------------------------------------------------------\n",
"PARTS = [ # (이름, 별칭, 월 기준 건수, 계절 진폭, 정점 월, (Critical, Major 비율), 가상 단가지수, 제조사, 주요 결함 가중치)\n",
" (\"엔진 연료계통\", [\"연료계통\", \"연료 라인\", \"연료 펌프\"], 26, 0.15, 7, (0.10, 0.35), 6.0, \"MK1\", \"누유:4 마모:2 체결불량:2 오작동:2\"),\n",
" (\"변속기\", [\"변속기\", \"T/M\", \"미션\"], 22, 0.10, 8, (0.12, 0.38), 9.0, \"MK1\", \"마모:4 누유:2 과열:3 오작동:1\"),\n",
" (\"제동 장치\", [\"제동 장치\", \"브레이크\", \"제동 라인\"], 20, 0.20, 12, (0.26, 0.40), 5.0, \"MK2\", \"마모:4 균열:2 누유:2 체결불량:2\"),\n",
" (\"조향 장치\", [\"조향 장치\", \"핸들 연동부\", \"조향 로드\"], 14, 0.10, 6, (0.30, 0.40), 5.0, \"MK2\", \"체결불량:4 균열:2 마모:3 오작동:1\"),\n",
" (\"냉각 계통\", [\"냉각 계통\", \"라디에이터\", \"냉각수 라인\"], 24, 0.30, 7, (0.05, 0.30), 3.0, \"MK3\", \"누유:4 과열:4 부식:2\"),\n",
" (\"전기·배터리\", [\"배터리\", \"전기 계통\", \"발전기\"], 28, 0.35, 1, (0.04, 0.28), 2.5, \"MK3\", \"접촉불량:4 부식:2 과열:2 오작동:2\"),\n",
" (\"통신 모듈\", [\"통신 모듈\", \"무전기 연동부\", \"안테나 접속부\"], 16, 0.05, 4, (0.03, 0.25), 4.0, \"MK4\", \"접촉불량:5 오작동:4 부식:1\"),\n",
" (\"유압 계통\", [\"유압 계통\", \"유압 호스\", \"유압 실린더\"], 18, 0.10, 9, (0.10, 0.35), 4.5, \"MK2\", \"누유:5 균열:2 마모:2 체결불량:1\"),\n",
" (\"현가 장치\", [\"현가 장치\", \"서스펜션\", \"완충기\"], 15, 0.08, 5, (0.06, 0.30), 3.5, \"MK4\", \"마모:4 균열:3 체결불량:2 부식:1\"),\n",
" (\"차체 용접부\", [\"차체 용접부\", \"프레임 용접부\", \"차체 이음부\"], 10, 0.05, 10, (0.28, 0.40), 7.0, \"MK1\", \"균열:6 부식:3 체결불량:1\"),\n",
" (\"광학·조준 장비\", [\"광학 장비\", \"조준경\", \"관측 장비\"], 8, 0.05, 3, (0.03, 0.22), 8.0, \"MK4\", \"오작동:4 접촉불량:3 부식:2 체결불량:1\"),\n",
" (\"공기 정화 필터\", [\"공기 필터\", \"흡기 필터\", \"정화 장치\"], 12, 0.25, 4, (0.01, 0.12), 1.0, \"MK3\", \"마모:5 부식:2 체결불량:3\")]\n",
"NP = len(PARTS)\n",
"DEFECTS = { # 결함 유형: 증상 표현(동의어)\n",
" \"균열\": [\"균열이 발견됨\", \"크랙이 확인됨\", \"갈라짐이 있음\"], \"누유\": [\"누유가 발생함\", \"오일이 새어 나옴\", \"누설 흔적이 있음\"],\n",
" \"마모\": [\"마모가 심함\", \"과도한 마모가 확인됨\", \"닳은 흔적이 있음\"], \"부식\": [\"부식이 진행됨\", \"녹이 발생함\", \"산화 흔적이 있음\"],\n",
" \"접촉불량\": [\"접촉 불량이 발생함\", \"커넥터 단선이 의심됨\", \"신호가 끊김\"], \"체결불량\": [\"볼트 풀림이 확인됨\", \"체결 상태가 불량함\", \"고정부가 헐거움\"],\n",
" \"과열\": [\"과열 경고가 발생함\", \"온도가 과도하게 상승함\", \"열이 심하게 남\"], \"오작동\": [\"오작동이 반복됨\", \"작동이 불안정함\", \"간헐적으로 멈춤\"]}\n",
"DNAMES = list(DEFECTS)\n",
"SEV = [\"Minor\", \"Major\", \"Critical\"]\n",
"SEV_CUE = {0: [\"경미한 수준임\", \"운용에는 지장이 없음\", \"육안 점검상 사소한 이상임\", \"기능에는 영향이 없음\"],\n",
" 1: [\"성능 저하가 있음\", \"조속한 정비가 필요함\", \"출력이 떨어지는 상태임\", \"기능이 부분적으로 제한됨\"],\n",
" 2: [\"즉시 운용 중지함\", \"안전에 위험이 있어 사용 불가\", \"작동 정지로 임무 수행이 어려움\", \"인명 안전 위협이 우려됨\"]}\n",
"CAUSES = [\"제조 불량\", \"정비 품질\", \"운용 부주의\", \"설계 취약\", \"자연 노후\"]\n",
"CAUSE_CUE = {\n",
" \"제조 불량\": [\"신품 인수 직후 발생함\", \"출고 상태에서 이미 이상이 있었음\", \"인수 후 얼마 지나지 않아 발생함\"],\n",
" \"정비 품질\": [\"외주 정비 후 얼마 되지 않아 재발함\", \"정비 후 재조립 상태가 불량함\", \"정비 직후 같은 부위에서 다시 발생함\"],\n",
" \"운용 부주의\": [\"험지 기동 중 충격 이후 발생함\", \"과부하 운용 후 발생함\", \"점검 없이 연속 운용한 뒤 발생함\"],\n",
" \"설계 취약\": [\"동일 증상이 다른 차량에서도 반복됨\", \"구조적으로 취약한 부위로 보임\", \"여러 부대에서 같은 형태로 보고됨\"],\n",
" \"자연 노후\": [\"사용 연한이 도래함\", \"장기간 사용으로 노후됨\", \"수명 주기 말기에 발생함\"]}\n",
"VENDORS = [\"외주A\", \"외주B\", \"외주C\", \"외주D\", \"부대정비\"]\n",
"ACTIONS = [\"정비소 입고를 요청함\", \"현장 임시 조치 후 운용 중임\", \"부품 교환을 요청함\", \"재검사를 요청함\", \"원인 확인이 필요함\"]\n",
"MAKER_OF = [p[7] for p in PARTS]\n",
"PRICE = np.array([p[6] for p in PARTS])\n",
"\n",
"\n",
"def usage_index(months, seed=0):\n",
" \"\"\"월별 훈련 강도(미리 알 수 있는 계획 값): 연 1회 정점을 갖는 계절 + 작은 잡음\"\"\"\n",
" # [TODO : 학생 실습 4] 텍스트 임베딩 및 토픽 모델링 기반 장비 구조적 품질 취약점 자동 군집화 모델 고도화 관련 핵심 로직을 구현하세요.\n",
" # 힌트: 상단 주석 및 데이터 스키마/지침을 참고하여 작성합니다.\n",
" # <<< 여기에 코드를 작성하세요 >>>\n",
" pass # 작성 후 pass를 제거하세요.\n",
"def make_qdr(months=36, seed=1, scale=1.0, surge=True):\n",
" \"\"\"반환: QDR DataFrame(건당 1행), 월별 usage, 급증 이벤트 목록\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" use = usage_index(months, seed)\n",
" events = []\n",
" if surge:\n",
" for _ in range(max(2, months // 12 * 2)):\n",
" events.append((int(rng.integers(NP)), int(rng.integers(6, months))))\n",
" rows = []\n",
" wdef = [np.array([float(x.split(\":\")[1]) for x in p[8].split()]) for p in PARTS]\n",
" ddef = [[x.split(\":\")[0] for x in p[8].split()] for p in PARTS]\n",
" n = 0\n",
" for m in range(months):\n",
" for pi, p in enumerate(PARTS):\n",
" lam = p[2] * scale * (1 + 0.010 * m) * use[m] * (1 + p[3] * np.cos(2 * np.pi * (m % 12 - p[4]) / 12))\n",
" ev = (pi, m) in events\n",
" k = rng.poisson(lam * (2.6 if ev else 1.0))\n",
" for _ in range(k):\n",
" rows.append((n, m, pi, ev)); n += 1\n",
" N = len(rows)\n",
" A = np.array(rows)\n",
" month, part, ev = A[:, 1], A[:, 2], A[:, 3].astype(bool)\n",
" defect = np.array([rng.choice(ddef[i], p=wdef[i] / wdef[i].sum()) for i in part])\n",
" cr = np.array([PARTS[i][5][0] for i in part]); ma = np.array([PARTS[i][5][1] for i in part])\n",
" u = rng.random(N)\n",
" sev = np.where(u < cr, 2, np.where(u < cr + ma, 1, 0))\n",
" vendor = rng.choice(VENDORS, N, p=[0.25, 0.2, 0.2, 0.15, 0.2])\n",
" # 원인: 기본 비율에 부위(제조사)·업체·이벤트 효과를 곱한다\n",
" base = np.array([0.22, 0.20, 0.20, 0.16, 0.22])\n",
" P = np.tile(base, (N, 1))\n",
" P[vendor == \"외주B\", 1] *= 2.6\n",
" P[ev, 0] *= 5.0\n",
" design = np.isin(part, [2, 3, 7]); P[design, 3] *= 2.2\n",
" P[np.isin(part, [4, 5, 11]), 0] *= 2.0 # 제조사 MK3 납품 부위는 제조 불량 비중이 높다\n",
" P[defect == \"마모\", 4] *= 1.6; P[defect == \"누유\", 4] *= 1.3\n",
" P /= P.sum(axis=1, keepdims=True)\n",
" cause = np.array([CAUSES[rng.choice(5, p=P[i])] for i in range(N)])\n",
" since = np.where(cause == \"정비 품질\", rng.exponential(35, N), rng.exponential(160, N)).clip(1, 720).round().astype(int)\n",
" wait = (rng.lognormal(1.3, 0.5, N) * np.where(vendor == \"외주B\", 1.7, np.where(vendor == \"부대정비\", 0.6, 1.0)) * (0.7 + 0.5 * use[month])).round(1)\n",
" reins = rng.random(N) < np.where(cause == \"정비 품질\", 0.6, 0.08)\n",
" texts = []\n",
" for i in range(N):\n",
" pt = PARTS[part[i]]\n",
" cl = [f\"{rng.choice(pt[1])}에서 {rng.choice(DEFECTS[defect[i]])}\"]\n",
" if rng.random() < 0.7: cl.append(rng.choice(SEV_CUE[sev[i]]))\n",
" if rng.random() < 0.65: # 원인 단서: 8%는 실제 원인과 다른 단서가 적힌다(보고자의 추정)\n",
" cc = cause[i] if rng.random() > 0.08 else rng.choice(CAUSES)\n",
" cl.append(rng.choice(CAUSE_CUE[cc]))\n",
" cl.append(rng.choice(ACTIONS))\n",
" if rng.random() < 0.5: cl.insert(0, f\"VU-{rng.integers(1, 31):02d} 소속\")\n",
" texts.append(\". \".join(cl) + \".\")\n",
" df = pd.DataFrame({\"qdr_id\": [f\"Q{seed}{i:06d}\" for i in range(N)], \"month\": month, \"unit\": [f\"VU-{x:02d}\" for x in rng.integers(1, 31, N)],\n",
" \"part\": part, \"defect\": defect, \"sev\": sev, \"cause\": cause, \"vendor\": vendor, \"maker\": [MAKER_OF[i] for i in part],\n",
" \"since_maint\": since, \"inspect_wait\": wait, \"reinspect\": reins, \"event\": ev, \"text\": texts})\n",
" df[\"cost\"] = PRICE[df[\"part\"]] * np.array([1.0, 2.0, 4.0])[df[\"sev\"]] # [교육용가상자료] 수리·조치 비용지수\n",
" return df, use, events\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# 공통 텍스트 조각: 정규화, 간이 토큰화(조사 제거), 부위·결함·치명도·원인 키워드 라벨링\n",
"# ---------------------------------------------------------------------------\n",
"TYPO = {\"누윳\": \"누유\", \"균렬\": \"균열\", \"접축 불량\": \"접촉 불량\", \"부씩\": \"부식\", \"마묘\": \"마모\", \"과렬\": \"과열\", \"오작똥\": \"오작동\", \"재동\": \"제동\"}\n",
"ABBR = {\"T/M\": \"변속기\", \"BAT\": \"배터리\"}\n",
"PART_KW = sorted([(a, i) for i, p in enumerate(PARTS) for a in p[1]], key=lambda x: -len(x[0]))\n",
"DEF_KW = {\"균열\": [\"균열\", \"크랙\", \"갈라\"], \"누유\": [\"누유\", \"새어\", \"누설\"], \"마모\": [\"마모\", \"닳은\"], \"부식\": [\"부식\", \"녹이\", \"산화\"],\n",
" \"접촉불량\": [\"접촉 불량\", \"단선\", \"끊김\"], \"체결불량\": [\"볼트\", \"체결\", \"헐거\"], \"과열\": [\"과열\", \"온도\", \"열이\"], \"오작동\": [\"오작동\", \"불안정\", \"멈춤\"]}\n",
"SEV_KW = [(2, [\"즉시 운용 중지\", \"사용 불가\", \"위험\"]), (1, [\"성능 저하\", \"조속한\"]), (0, [\"경미\", \"지장이 없\"])]\n",
"CAUSE_KW = {\"제조 불량\": [\"신품\", \"출고\", \"인수\"], \"정비 품질\": [\"정비 후\", \"재조립\", \"정비 직후\"], \"운용 부주의\": [\"충격\", \"과부하\", \"점검 없이\"],\n",
" \"설계 취약\": [\"동일 증상\", \"구조적\", \"여러 부대\"], \"자연 노후\": [\"사용 연한\", \"노후\", \"수명\"]}\n",
"PARTICLES = (\"에서\", \"으로\", \"이\", \"가\", \"은\", \"는\", \"을\", \"를\", \"에\", \"의\", \"로\", \"와\", \"과\", \"도\", \"함\", \"음\", \"됨\")\n",
"\n",
"\n",
"def norm(t):\n",
" \"\"\"정규화: 전각->반각, 특수문자·중복 공백 제거, 약어 확장, 오타 교정\"\"\"\n",
" # [TODO : 학생 실습 4] 텍스트 임베딩 및 토픽 모델링 기반 장비 구조적 품질 취약점 자동 군집화 모델 고도화 관련 핵심 로직을 구현하세요.\n",
" # 힌트: 상단 주석 및 데이터 스키마/지침을 참고하여 작성합니다.\n",
" # <<< 여기에 코드를 작성하세요 >>>\n",
" pass # 작성 후 pass를 제거하세요.\n",
"def tok(t):\n",
" out = []\n",
" for w in re.split(r\"[\\s.,]+\", norm(t)):\n",
" if not w or re.fullmatch(r\"[\\d\\-A-Za-z]+\", w):\n",
" continue\n",
" for p in PARTICLES:\n",
" if len(w) > len(p) + 1 and w.endswith(p):\n",
" w = w[:-len(p)]\n",
" break\n",
" out.append(w)\n",
" return out\n",
"\n",
"\n",
"def label_part(t):\n",
" for a, i in PART_KW:\n",
" if a in t:\n",
" return i\n",
" return -1\n",
"\n",
"\n",
"def label_defect(t):\n",
" for d, ks in DEF_KW.items():\n",
" if any(k in t for k in ks):\n",
" return d\n",
" return \"미분류\"\n",
"\n",
"\n",
"def label_sev(t):\n",
" for s, ks in SEV_KW:\n",
" if any(k in t for k in ks):\n",
" return s\n",
" return -1\n",
"\n",
"\n",
"def label_cause(t):\n",
" for c, ks in CAUSE_KW.items():\n",
" if any(k in t for k in ks):\n",
" return c\n",
" return \"미상\"\n",
"\n",
"df, use, events = make_qdr(36, seed=1, scale=3.0)\n",
"names = [p[0] for p in PARTS]\n",
"print(f\"[교육용가상자료] QDR {len(df)}건, 36개월\")\n",
"print(\"(토픽 모델은 결함 서술 핵심 문장 '부위에서 증상'에 적용하고, 치명도·원인은 군집 해석에 사용)\")\n",
"\n",
"# 1) 텍스트 임베딩(TF-IDF) + NMF 토픽 모델링\n",
"STOP = set(w for a in ACTIONS for w in tok(a)) | {\"소속\"}\n",
"vec = TfidfVectorizer(tokenizer=lambda s: [w for w in tok(s) if w not in STOP], lowercase=False, token_pattern=None, min_df=5)\n",
"df[\"core\"] = df[\"text\"].map(lambda s: [x for x in s.split(\". \") if \"에서\" in x][0]) # 결함 서술 핵심 문장(부위+증상)\n",
"X = vec.fit_transform(df[\"core\"])\n",
"K = 36\n",
"nmf = NMF(n_components=K, init=\"nndsvd\", random_state=0, max_iter=400).fit(X)\n",
"Wd = nmf.transform(X)\n",
"df[\"topic\"] = Wd.argmax(axis=1)\n",
"vocab = np.array(vec.get_feature_names_out())\n",
"nmi_p = normalized_mutual_info_score(df[\"part\"], df[\"topic\"]); nmi_d = normalized_mutual_info_score(df[\"defect\"], df[\"topic\"]); nmi_pd = normalized_mutual_info_score(df[\"part\"].astype(str) + df[\"defect\"], df[\"topic\"])\n",
"print(f\"TF-IDF 어휘 {X.shape[1]}개, 토픽 {K}개: 군집-라벨 일치도(NMI) 부위 {nmi_p:.2f}, 결함 유형 {nmi_d:.2f}, 부위+결함 {nmi_pd:.2f}\")\n",
"base_acc = np.mean([label_part(t) == p for t, p in zip(df[\"text\"], df[\"part\"])])\n",
"print(f\" 참고: 키워드 규칙 부위 라벨 정확도 {base_acc:.1%}\")\n",
"g = df.groupby(\"topic\").agg(건수=(\"qdr_id\", \"size\"), critical=(\"sev\", lambda s: (s == 2).mean()), 설계취약=(\"cause\", lambda s: (s == \"설계 취약\").mean()),\n",
" 주부위=(\"part\", lambda s: names[s.mode()[0]]), 주결함=(\"defect\", lambda s: s.mode()[0]))\n",
"g[\"핵심어\"] = [\", \".join(vocab[np.argsort(-nmf.components_[k])[:4]]) for k in g.index]\n",
"g[\"취약지수\"] = g[\"건수\"] * (g[\"critical\"] * 9 + (1 - g[\"critical\"]) * 1.5)\n",
"print(\"\\n[취약 군집 상위 5] 토픽 | 건수 | Critical 비중 | 주 부위 | 주 결함 | 핵심어\")\n",
"for k, r in g.sort_values(\"취약지수\", ascending=False).head(5).iterrows():\n",
" print(f\" T{k:02d} {r['건수']:4d} {r['critical']:.0%} {r['주부위']} / {r['주결함']} [{r['핵심어']}]\")\n",
"struct = list(g[\"설계취약\"].sort_values(ascending=False).index[:8])\n",
"print(f\"설계 취약 비중 상위 토픽 {len(struct)}개(구조적 취약 후보): \" + \", \".join(f\"T{k:02d}\" for k in struct) + f\" -> 해당 토픽 QDR의 설계 취약 비중 {df.loc[df['topic'].isin(struct), 'cause'].eq('설계 취약').mean():.0%} (전체 {df['cause'].eq('설계 취약').mean():.0%})\")\n",
"\n",
"# 2) 부위별 월간 QDR 건수 기준 모델: 파생 피처 13개\n",
"top8 = list(df.groupby(\"part\").size().sort_values(ascending=False).index[:8])\n",
"mon = df.groupby([\"part\", \"month\"]).agg(n=(\"qdr_id\", \"size\"), crit=(\"sev\", lambda s: (s == 2).mean()), wait=(\"inspect_wait\", \"mean\"), vb=(\"vendor\", lambda s: (s == \"외주B\").mean()),\n",
" re=(\"reinspect\", \"mean\"), st=(\"topic\", lambda s: s.isin(struct).mean())).reset_index()\n",
"rows = []\n",
"for p in top8:\n",
" d = mon[mon[\"part\"] == p].set_index(\"month\").reindex(range(36)).fillna(0)\n",
" f = pd.DataFrame({\"part\": p, \"month\": range(36), \"n\": d[\"n\"].values})\n",
" f[\"lag1\"], f[\"lag2\"] = f[\"n\"].shift(1), f[\"n\"].shift(2)\n",
" f[\"roll3\"] = f[\"n\"].shift(1).rolling(3).mean()\n",
" f[\"exp_count\"] = (f[\"n\"] / use).shift(1).rolling(6, min_periods=3).mean() * use # 최근 6개월 평균 발생률 x 이번 달 훈련 강도\n",
" f[\"usage\"], f[\"usage_lag1\"] = use, np.r_[use[0], use[:-1]]\n",
" f[\"season_c\"], f[\"season_s\"] = np.cos(2 * np.pi * f[\"month\"] / 12), np.sin(2 * np.pi * f[\"month\"] / 12)\n",
" f[\"trend\"] = f[\"month\"]\n",
" for c in (\"crit\", \"wait\", \"vb\", \"re\", \"st\"):\n",
" f[c + \"_lag1\"] = d[c].shift(1).values\n",
" rows.append(f)\n",
"F = pd.concat(rows, ignore_index=True).dropna()\n",
"DER = [\"lag1\", \"lag2\", \"roll3\", \"exp_count\", \"usage\", \"usage_lag1\", \"season_c\", \"season_s\", \"trend\", \"crit_lag1\", \"wait_lag1\", \"vb_lag1\", \"re_lag1\", \"st_lag1\"]\n",
"print(f\"\\n월간 건수 예측: 파생 피처 {len(DER)}개 (목표 10개 이상: {'달성' if len(DER) >= 10 else '미달'}), 대상 {len(top8)}개 부위 x {F['month'].nunique()}개월\")\n",
"\n",
"\n",
"def fit(d, cols):\n",
" return GradientBoostingRegressor(n_estimators=120, max_depth=3, learning_rate=0.08, subsample=0.8, random_state=0).fit(d[cols], d[\"n\"])\n",
"\n",
"\n",
"mape = lambda y, p: float(np.mean(np.abs(y - p) / y))\n",
"folds, naive = [], []\n",
"for c in (17, 23, 29): # 확장 윈도: 과거로만 학습, 뒤 6개월 검증\n",
" trn, val = F[F[\"month\"] <= c], F[(F[\"month\"] > c) & (F[\"month\"] <= c + 6)]\n",
" folds.append(mape(val[\"n\"], fit(trn, DER).predict(val[DER]))); naive.append(mape(val[\"n\"], val[\"lag1\"]))\n",
"print(f\"시계열 교차검증 폴드별 MAPE: {', '.join(f'{x:.1%}' for x in folds)} -> 평균 {np.mean(folds):.1%} (목표 25% 이하: {'달성' if np.mean(folds) <= 0.25 else '미달'}), 전월값 기준 평균 {np.mean(naive):.1%}\")\n",
"\n",
"# 3) 피처 중요도로 차원 축소\n",
"trn, val = F[F[\"month\"] <= 29], F[F[\"month\"] > 29]\n",
"full = fit(trn, DER)\n",
"pi = permutation_importance(full, val[DER], val[\"n\"], n_repeats=5, random_state=0, scoring=\"neg_mean_absolute_error\")\n",
"imp = pd.Series(pi.importances_mean, DER).sort_values(ascending=False)\n",
"print(\"피처 중요도 상위 5: \" + \", \".join(f\"{k}({v:.2f})\" for k, v in imp.head(5).items()))\n",
"top = list(imp.index[:7])\n",
"print(f\"차원 축소 {len(DER)}개 -> {len(top)}개: 검증 MAPE {mape(val['n'], full.predict(val[DER])):.1%} -> {mape(val['n'], fit(trn, top).predict(val[top])):.1%}\")\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"<details>\n",
"<summary><b>💡 [참고] 미션 04 예상 실행 결과 (Output)</b></summary>\n",
"\n",
"```text\n",
"[교육용가상자료] QDR 27675건, 36개월\n",
"(토픽 모델은 결함 서술 핵심 문장 '부위에서 증상'에 적용하고, 치명도·원인은 군집 해석에 사용)\n",
"TF-IDF 어휘 90개, 토픽 36개: 군집-라벨 일치도(NMI) 부위 0.45, 결함 유형 0.60, 부위+결함 0.63\n",
" 참고: 키워드 규칙 부위 라벨 정확도 100.0%\n",
"\n",
"[취약 군집 상위 5] 토픽 | 건수 | Critical 비중 | 주 부위 | 주 결함 | 핵심어\n",
" T03 1711 10% 엔진 연료계통 / 누유 [나옴, 새어, 오일, 라디에이터]\n",
" T24 1631 11% 변속기 / 마모 [닳은, 흔적, 있음, 산화]\n",
" T28 935 30% 조향 장치 / 체결불량 [조향, 로드, 장치, 크랙]\n",
" T02 1430 12% 냉각 계통 / 누유 [누유, 발생, 녹이, 라디에이터]\n",
" T25 1009 18% 조향 장치 / 접촉불량 [연동부, 핸들, 무전기, 접촉]\n",
"설계 취약 비중 상위 토픽 8개(구조적 취약 후보): T05, T28, T35, T13, T11, T25, T30, T02 -> 해당 토픽 QDR의 설계 취약 비중 21% (전체 17%)\n",
"\n",
"월간 건수 예측: 파생 피처 14개 (목표 10개 이상: 달성), 대상 8개 부위 x 33개월\n",
"시계열 교차검증 폴드별 MAPE: 27.8%, 13.9%, 16.9% -> 평균 19.5% (목표 25% 이하: 달성), 전월값 기준 평균 18.9%\n",
"피처 중요도 상위 5: lag1(6.77), usage(1.86), exp_count(1.78), st_lag1(0.81), lag2(0.68)\n",
"차원 축소 14개 -> 7개: 검증 MAPE 16.9% -> 15.3%\n",
"```\n",
"</details>\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## 🎯 미션 05. 결함 치명도(Critical/Major/Minor) 기반 종합 품질 취약성 지수 산출 엔진 개발\n",
"**소요시간**: 약 3.5시간\n",
"\n",
"### 📋 미션 배경 및 수행 지침\n",
"#### 미션 배경 및 당위성\n",
"\n",
"결함 치명도 기반 종합 품질 취약성 지수 산출 엔진을 개발한다. 실제 전술 현장에서는 단순 탐지에 그치지 않고 신속한 위협 판단과 신뢰도 높은 의사결정이 필수적이므로, **결함 치명도(Critical/Major/Minor)를 분류하고 치명도 가중 종합 품질 취약성 지수를 부위별로 산출·예측하는 엔진을 개발한다.** 과업을 완수하여 오판을 방지하고 작전 대응력을 극대화해야 합니다.\n",
"\n",
"이 문제를 해결하기 위해 본 미션에서는 **키워드 규칙과 TF-IDF 로지스틱 회귀의 치명도 분류 정확도와 Critical 재현율을 비교하고 신뢰도 구간별 실제 정확도를 점검 수행하고 검증하는** 실습을 체계적으로 수행합니다.\n",
"\n",
"학습자는 실습가이드에 제시된 총 4개 세부 활동(기본 기능 구현 ➔ 결과 지표 검증 ➔ 파라미터 튜닝 및 도전과제)을 순차적으로 진행하며 핵심 판정 기준을 달성합니다.\n",
"\n",
"**이번 결과물:** `src/models/qlt05_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": [
"\"\"\"QDR 서술·부위·결함 정보로 개별 결함의 치명도(Critical/Major/Minor)를 분류하는 모델(키워드 규칙 기준 대비)과 신뢰도 보정을 확인하고, 치명도 가중 부위별 월간 종합 품질 취약성 지수를 앙상블(릿지에 그래디언트 부스팅·랜덤 포레스트 잔차 보정을 더한 모델)로 예측해 학습 기간 평균 기준 모델과 MAE·RMSE·R2를 비교하는 취약성 지수 산출 엔진을 구현한다.\"\"\"\n",
"import re\n",
"import unicodedata\n",
"import numpy as np\n",
"import pandas as pd\n",
"import scipy.sparse as sp\n",
"from sklearn.ensemble import GradientBoostingRegressor, RandomForestRegressor\n",
"from sklearn.feature_extraction.text import TfidfVectorizer\n",
"from sklearn.linear_model import LogisticRegression, Ridge\n",
"from sklearn.metrics import mean_absolute_error, mean_squared_error, r2_score\n",
"from sklearn.pipeline import make_pipeline\n",
"from sklearn.preprocessing import StandardScaler\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# [교육용가상자료] 가상 야전 장비 품질결함보고(QDR) 생성기 (실제 부대·장비·업체·정비 실적 없음)\n",
"# 가상 장비 부위 12종, 결함 유형 8종, 치명도(Critical/Major/Minor), 원인 5종, 가상 외주정비 업체 4곳 + 부대정비.\n",
"# QDR 1건마다 한국어 서술 텍스트를 만들고, 월별 건수는 계절·추세·훈련 강도(usage)·일부 제조 불량 이벤트를 따른다.\n",
"# ---------------------------------------------------------------------------\n",
"PARTS = [ # (이름, 별칭, 월 기준 건수, 계절 진폭, 정점 월, (Critical, Major 비율), 가상 단가지수, 제조사, 주요 결함 가중치)\n",
" (\"엔진 연료계통\", [\"연료계통\", \"연료 라인\", \"연료 펌프\"], 26, 0.15, 7, (0.10, 0.35), 6.0, \"MK1\", \"누유:4 마모:2 체결불량:2 오작동:2\"),\n",
" (\"변속기\", [\"변속기\", \"T/M\", \"미션\"], 22, 0.10, 8, (0.12, 0.38), 9.0, \"MK1\", \"마모:4 누유:2 과열:3 오작동:1\"),\n",
" (\"제동 장치\", [\"제동 장치\", \"브레이크\", \"제동 라인\"], 20, 0.20, 12, (0.26, 0.40), 5.0, \"MK2\", \"마모:4 균열:2 누유:2 체결불량:2\"),\n",
" (\"조향 장치\", [\"조향 장치\", \"핸들 연동부\", \"조향 로드\"], 14, 0.10, 6, (0.30, 0.40), 5.0, \"MK2\", \"체결불량:4 균열:2 마모:3 오작동:1\"),\n",
" (\"냉각 계통\", [\"냉각 계통\", \"라디에이터\", \"냉각수 라인\"], 24, 0.30, 7, (0.05, 0.30), 3.0, \"MK3\", \"누유:4 과열:4 부식:2\"),\n",
" (\"전기·배터리\", [\"배터리\", \"전기 계통\", \"발전기\"], 28, 0.35, 1, (0.04, 0.28), 2.5, \"MK3\", \"접촉불량:4 부식:2 과열:2 오작동:2\"),\n",
" (\"통신 모듈\", [\"통신 모듈\", \"무전기 연동부\", \"안테나 접속부\"], 16, 0.05, 4, (0.03, 0.25), 4.0, \"MK4\", \"접촉불량:5 오작동:4 부식:1\"),\n",
" (\"유압 계통\", [\"유압 계통\", \"유압 호스\", \"유압 실린더\"], 18, 0.10, 9, (0.10, 0.35), 4.5, \"MK2\", \"누유:5 균열:2 마모:2 체결불량:1\"),\n",
" (\"현가 장치\", [\"현가 장치\", \"서스펜션\", \"완충기\"], 15, 0.08, 5, (0.06, 0.30), 3.5, \"MK4\", \"마모:4 균열:3 체결불량:2 부식:1\"),\n",
" (\"차체 용접부\", [\"차체 용접부\", \"프레임 용접부\", \"차체 이음부\"], 10, 0.05, 10, (0.28, 0.40), 7.0, \"MK1\", \"균열:6 부식:3 체결불량:1\"),\n",
" (\"광학·조준 장비\", [\"광학 장비\", \"조준경\", \"관측 장비\"], 8, 0.05, 3, (0.03, 0.22), 8.0, \"MK4\", \"오작동:4 접촉불량:3 부식:2 체결불량:1\"),\n",
" (\"공기 정화 필터\", [\"공기 필터\", \"흡기 필터\", \"정화 장치\"], 12, 0.25, 4, (0.01, 0.12), 1.0, \"MK3\", \"마모:5 부식:2 체결불량:3\")]\n",
"NP = len(PARTS)\n",
"DEFECTS = { # 결함 유형: 증상 표현(동의어)\n",
" \"균열\": [\"균열이 발견됨\", \"크랙이 확인됨\", \"갈라짐이 있음\"], \"누유\": [\"누유가 발생함\", \"오일이 새어 나옴\", \"누설 흔적이 있음\"],\n",
" \"마모\": [\"마모가 심함\", \"과도한 마모가 확인됨\", \"닳은 흔적이 있음\"], \"부식\": [\"부식이 진행됨\", \"녹이 발생함\", \"산화 흔적이 있음\"],\n",
" \"접촉불량\": [\"접촉 불량이 발생함\", \"커넥터 단선이 의심됨\", \"신호가 끊김\"], \"체결불량\": [\"볼트 풀림이 확인됨\", \"체결 상태가 불량함\", \"고정부가 헐거움\"],\n",
" \"과열\": [\"과열 경고가 발생함\", \"온도가 과도하게 상승함\", \"열이 심하게 남\"], \"오작동\": [\"오작동이 반복됨\", \"작동이 불안정함\", \"간헐적으로 멈춤\"]}\n",
"DNAMES = list(DEFECTS)\n",
"SEV = [\"Minor\", \"Major\", \"Critical\"]\n",
"SEV_CUE = {0: [\"경미한 수준임\", \"운용에는 지장이 없음\", \"육안 점검상 사소한 이상임\", \"기능에는 영향이 없음\"],\n",
" 1: [\"성능 저하가 있음\", \"조속한 정비가 필요함\", \"출력이 떨어지는 상태임\", \"기능이 부분적으로 제한됨\"],\n",
" 2: [\"즉시 운용 중지함\", \"안전에 위험이 있어 사용 불가\", \"작동 정지로 임무 수행이 어려움\", \"인명 안전 위협이 우려됨\"]}\n",
"CAUSES = [\"제조 불량\", \"정비 품질\", \"운용 부주의\", \"설계 취약\", \"자연 노후\"]\n",
"CAUSE_CUE = {\n",
" \"제조 불량\": [\"신품 인수 직후 발생함\", \"출고 상태에서 이미 이상이 있었음\", \"인수 후 얼마 지나지 않아 발생함\"],\n",
" \"정비 품질\": [\"외주 정비 후 얼마 되지 않아 재발함\", \"정비 후 재조립 상태가 불량함\", \"정비 직후 같은 부위에서 다시 발생함\"],\n",
" \"운용 부주의\": [\"험지 기동 중 충격 이후 발생함\", \"과부하 운용 후 발생함\", \"점검 없이 연속 운용한 뒤 발생함\"],\n",
" \"설계 취약\": [\"동일 증상이 다른 차량에서도 반복됨\", \"구조적으로 취약한 부위로 보임\", \"여러 부대에서 같은 형태로 보고됨\"],\n",
" \"자연 노후\": [\"사용 연한이 도래함\", \"장기간 사용으로 노후됨\", \"수명 주기 말기에 발생함\"]}\n",
"VENDORS = [\"외주A\", \"외주B\", \"외주C\", \"외주D\", \"부대정비\"]\n",
"ACTIONS = [\"정비소 입고를 요청함\", \"현장 임시 조치 후 운용 중임\", \"부품 교환을 요청함\", \"재검사를 요청함\", \"원인 확인이 필요함\"]\n",
"MAKER_OF = [p[7] for p in PARTS]\n",
"PRICE = np.array([p[6] for p in PARTS])\n",
"\n",
"\n",
"def usage_index(months, seed=0):\n",
" \"\"\"월별 훈련 강도(미리 알 수 있는 계획 값): 연 1회 정점을 갖는 계절 + 작은 잡음\"\"\"\n",
" # [TODO : 학생 실습 5] 결함 치명도(Critical/Major/Minor) 기반 종합 품질 취약성 지수 산출 엔진 개발 관련 핵심 로직을 구현하세요.\n",
" # 힌트: 상단 주석 및 데이터 스키마/지침을 참고하여 작성합니다.\n",
" # <<< 여기에 코드를 작성하세요 >>>\n",
" pass # 작성 후 pass를 제거하세요.\n",
"def make_qdr(months=36, seed=1, scale=1.0, surge=True):\n",
" \"\"\"반환: QDR DataFrame(건당 1행), 월별 usage, 급증 이벤트 목록\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" use = usage_index(months, seed)\n",
" events = []\n",
" if surge:\n",
" for _ in range(max(2, months // 12 * 2)):\n",
" events.append((int(rng.integers(NP)), int(rng.integers(6, months))))\n",
" rows = []\n",
" wdef = [np.array([float(x.split(\":\")[1]) for x in p[8].split()]) for p in PARTS]\n",
" ddef = [[x.split(\":\")[0] for x in p[8].split()] for p in PARTS]\n",
" n = 0\n",
" for m in range(months):\n",
" for pi, p in enumerate(PARTS):\n",
" lam = p[2] * scale * (1 + 0.010 * m) * use[m] * (1 + p[3] * np.cos(2 * np.pi * (m % 12 - p[4]) / 12))\n",
" ev = (pi, m) in events\n",
" k = rng.poisson(lam * (2.6 if ev else 1.0))\n",
" for _ in range(k):\n",
" rows.append((n, m, pi, ev)); n += 1\n",
" N = len(rows)\n",
" A = np.array(rows)\n",
" month, part, ev = A[:, 1], A[:, 2], A[:, 3].astype(bool)\n",
" defect = np.array([rng.choice(ddef[i], p=wdef[i] / wdef[i].sum()) for i in part])\n",
" cr = np.array([PARTS[i][5][0] for i in part]); ma = np.array([PARTS[i][5][1] for i in part])\n",
" u = rng.random(N)\n",
" sev = np.where(u < cr, 2, np.where(u < cr + ma, 1, 0))\n",
" vendor = rng.choice(VENDORS, N, p=[0.25, 0.2, 0.2, 0.15, 0.2])\n",
" # 원인: 기본 비율에 부위(제조사)·업체·이벤트 효과를 곱한다\n",
" base = np.array([0.22, 0.20, 0.20, 0.16, 0.22])\n",
" P = np.tile(base, (N, 1))\n",
" P[vendor == \"외주B\", 1] *= 2.6\n",
" P[ev, 0] *= 5.0\n",
" design = np.isin(part, [2, 3, 7]); P[design, 3] *= 2.2\n",
" P[np.isin(part, [4, 5, 11]), 0] *= 2.0 # 제조사 MK3 납품 부위는 제조 불량 비중이 높다\n",
" P[defect == \"마모\", 4] *= 1.6; P[defect == \"누유\", 4] *= 1.3\n",
" P /= P.sum(axis=1, keepdims=True)\n",
" cause = np.array([CAUSES[rng.choice(5, p=P[i])] for i in range(N)])\n",
" since = np.where(cause == \"정비 품질\", rng.exponential(35, N), rng.exponential(160, N)).clip(1, 720).round().astype(int)\n",
" wait = (rng.lognormal(1.3, 0.5, N) * np.where(vendor == \"외주B\", 1.7, np.where(vendor == \"부대정비\", 0.6, 1.0)) * (0.7 + 0.5 * use[month])).round(1)\n",
" reins = rng.random(N) < np.where(cause == \"정비 품질\", 0.6, 0.08)\n",
" texts = []\n",
" for i in range(N):\n",
" pt = PARTS[part[i]]\n",
" cl = [f\"{rng.choice(pt[1])}에서 {rng.choice(DEFECTS[defect[i]])}\"]\n",
" if rng.random() < 0.7: cl.append(rng.choice(SEV_CUE[sev[i]]))\n",
" if rng.random() < 0.65: # 원인 단서: 8%는 실제 원인과 다른 단서가 적힌다(보고자의 추정)\n",
" cc = cause[i] if rng.random() > 0.08 else rng.choice(CAUSES)\n",
" cl.append(rng.choice(CAUSE_CUE[cc]))\n",
" cl.append(rng.choice(ACTIONS))\n",
" if rng.random() < 0.5: cl.insert(0, f\"VU-{rng.integers(1, 31):02d} 소속\")\n",
" texts.append(\". \".join(cl) + \".\")\n",
" df = pd.DataFrame({\"qdr_id\": [f\"Q{seed}{i:06d}\" for i in range(N)], \"month\": month, \"unit\": [f\"VU-{x:02d}\" for x in rng.integers(1, 31, N)],\n",
" \"part\": part, \"defect\": defect, \"sev\": sev, \"cause\": cause, \"vendor\": vendor, \"maker\": [MAKER_OF[i] for i in part],\n",
" \"since_maint\": since, \"inspect_wait\": wait, \"reinspect\": reins, \"event\": ev, \"text\": texts})\n",
" df[\"cost\"] = PRICE[df[\"part\"]] * np.array([1.0, 2.0, 4.0])[df[\"sev\"]] # [교육용가상자료] 수리·조치 비용지수\n",
" return df, use, events\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# 공통 텍스트 조각: 정규화, 간이 토큰화(조사 제거), 부위·결함·치명도·원인 키워드 라벨링\n",
"# ---------------------------------------------------------------------------\n",
"TYPO = {\"누윳\": \"누유\", \"균렬\": \"균열\", \"접축 불량\": \"접촉 불량\", \"부씩\": \"부식\", \"마묘\": \"마모\", \"과렬\": \"과열\", \"오작똥\": \"오작동\", \"재동\": \"제동\"}\n",
"ABBR = {\"T/M\": \"변속기\", \"BAT\": \"배터리\"}\n",
"PART_KW = sorted([(a, i) for i, p in enumerate(PARTS) for a in p[1]], key=lambda x: -len(x[0]))\n",
"DEF_KW = {\"균열\": [\"균열\", \"크랙\", \"갈라\"], \"누유\": [\"누유\", \"새어\", \"누설\"], \"마모\": [\"마모\", \"닳은\"], \"부식\": [\"부식\", \"녹이\", \"산화\"],\n",
" \"접촉불량\": [\"접촉 불량\", \"단선\", \"끊김\"], \"체결불량\": [\"볼트\", \"체결\", \"헐거\"], \"과열\": [\"과열\", \"온도\", \"열이\"], \"오작동\": [\"오작동\", \"불안정\", \"멈춤\"]}\n",
"SEV_KW = [(2, [\"즉시 운용 중지\", \"사용 불가\", \"위험\"]), (1, [\"성능 저하\", \"조속한\"]), (0, [\"경미\", \"지장이 없\"])]\n",
"CAUSE_KW = {\"제조 불량\": [\"신품\", \"출고\", \"인수\"], \"정비 품질\": [\"정비 후\", \"재조립\", \"정비 직후\"], \"운용 부주의\": [\"충격\", \"과부하\", \"점검 없이\"],\n",
" \"설계 취약\": [\"동일 증상\", \"구조적\", \"여러 부대\"], \"자연 노후\": [\"사용 연한\", \"노후\", \"수명\"]}\n",
"PARTICLES = (\"에서\", \"으로\", \"이\", \"가\", \"은\", \"는\", \"을\", \"를\", \"에\", \"의\", \"로\", \"와\", \"과\", \"도\", \"함\", \"음\", \"됨\")\n",
"\n",
"\n",
"def norm(t):\n",
" \"\"\"정규화: 전각->반각, 특수문자·중복 공백 제거, 약어 확장, 오타 교정\"\"\"\n",
" # [TODO : 학생 실습 5] 결함 치명도(Critical/Major/Minor) 기반 종합 품질 취약성 지수 산출 엔진 개발 관련 핵심 로직을 구현하세요.\n",
" # 힌트: 상단 주석 및 데이터 스키마/지침을 참고하여 작성합니다.\n",
" # <<< 여기에 코드를 작성하세요 >>>\n",
" pass # 작성 후 pass를 제거하세요.\n",
"def tok(t):\n",
" out = []\n",
" for w in re.split(r\"[\\s.,]+\", norm(t)):\n",
" if not w or re.fullmatch(r\"[\\d\\-A-Za-z]+\", w):\n",
" continue\n",
" for p in PARTICLES:\n",
" if len(w) > len(p) + 1 and w.endswith(p):\n",
" w = w[:-len(p)]\n",
" break\n",
" out.append(w)\n",
" return out\n",
"\n",
"\n",
"def label_part(t):\n",
" for a, i in PART_KW:\n",
" if a in t:\n",
" return i\n",
" return -1\n",
"\n",
"\n",
"def label_defect(t):\n",
" for d, ks in DEF_KW.items():\n",
" if any(k in t for k in ks):\n",
" return d\n",
" return \"미분류\"\n",
"\n",
"\n",
"def label_sev(t):\n",
" for s, ks in SEV_KW:\n",
" if any(k in t for k in ks):\n",
" return s\n",
" return -1\n",
"\n",
"\n",
"def label_cause(t):\n",
" for c, ks in CAUSE_KW.items():\n",
" if any(k in t for k in ks):\n",
" return c\n",
" return \"미상\"\n",
"\n",
"df, use, events = make_qdr(48, seed=1, scale=4.0)\n",
"names = [p[0] for p in PARTS]\n",
"W_SEV = np.array([1, 3, 9]) # 취약성 지수 가중치(Minor 1, Major 3, Critical 9)\n",
"tr, va = df[df[\"month\"] < 36], df[df[\"month\"] >= 36]\n",
"print(f\"[교육용가상자료] QDR {len(df)}건 (학습 36개월 {len(tr)}건, 검증 12개월 {len(va)}건)\")\n",
"\n",
"# 1) 개별 QDR 치명도 분류: 키워드 규칙 vs 텍스트+부위·결함 로지스틱 회귀\n",
"rule = va[\"text\"].map(label_sev).replace(-1, 0).values # 치명도 단서가 없으면 Minor로 간주\n",
"vec = TfidfVectorizer(tokenizer=tok, lowercase=False, token_pattern=None, min_df=5)\n",
"Xtr, Xva = vec.fit_transform(tr[\"text\"]), vec.transform(va[\"text\"])\n",
"oh = lambda d: sp.csr_matrix(np.hstack([np.eye(NP)[d[\"part\"]], pd.get_dummies(d[\"defect\"]).reindex(columns=DNAMES, fill_value=0).values.astype(float)]))\n",
"clf = LogisticRegression(C=5, max_iter=500).fit(sp.hstack([Xtr, oh(tr)]), tr[\"sev\"])\n",
"P = clf.predict_proba(sp.hstack([Xva, oh(va)]))\n",
"pred = P.argmax(axis=1)\n",
"acc_r, acc_m = (rule == va[\"sev\"].values).mean(), (pred == va[\"sev\"].values).mean()\n",
"rec = lambda p: ((p == 2) & (va[\"sev\"].values == 2)).sum() / (va[\"sev\"].values == 2).sum()\n",
"print(f\"치명도 분류 정확도: 키워드 규칙 {acc_r:.1%} -> 텍스트 모델 {acc_m:.1%}, Critical 재현율 {rec(rule):.1%} -> {rec(pred):.1%}\")\n",
"conf = P.max(axis=1)\n",
"print(\"[신뢰도 보정] 구간 | 건수 | 평균 신뢰도 | 실제 정확도\")\n",
"for a, b in [(0, 0.6), (0.6, 0.8), (0.8, 0.95), (0.95, 1.01)]:\n",
" m = (conf >= a) & (conf < b)\n",
" print(f\" {a:.2f}~{min(b, 1):.2f} {m.sum():5d} {conf[m].mean():.2f} {(pred[m] == va['sev'].values[m]).mean():.2f}\")\n",
"\n",
"# 2) 월간 부위별 취약성 지수 예측\n",
"g = df.assign(v=W_SEV[df[\"sev\"]]).groupby([\"part\", \"month\"]).agg(n=(\"v\", \"size\"), v=(\"v\", \"sum\")).reset_index()\n",
"rows = []\n",
"for p in range(NP):\n",
" d = g[g[\"part\"] == p].set_index(\"month\").reindex(range(48)).fillna(0)\n",
" f = pd.DataFrame({\"part\": p, \"month\": range(48), \"y\": d[\"v\"].values, \"n\": d[\"n\"].values})\n",
" f[\"lag1\"], f[\"lag2\"], f[\"lag12\"] = f[\"y\"].shift(1), f[\"y\"].shift(2), (f[\"y\"] / use).shift(12) * use # lag12: 작년 같은 달 발생률 x 이번 달 훈련 강도\n",
" f[\"exp_v\"] = (f[\"y\"] / use).shift(1).rolling(6, min_periods=3).mean() * use # 최근 6개월 발생률 x 이번 달 훈련 강도\n",
" f[\"usage\"], f[\"season_c\"], f[\"season_s\"], f[\"trend\"] = use, np.cos(2 * np.pi * f[\"month\"] / 12), np.sin(2 * np.pi * f[\"month\"] / 12), f[\"month\"]\n",
" f[\"price\"], f[\"crit_rate\"] = PRICE[p], PARTS[p][5][0]\n",
" rows.append(f)\n",
"F = pd.concat(rows, ignore_index=True).dropna()\n",
"FE = [\"lag1\", \"lag2\", \"lag12\", \"exp_v\", \"usage\", \"season_c\", \"season_s\", \"trend\", \"price\", \"crit_rate\"]\n",
"Ftr, Fva = F[F[\"month\"] < 42], F[F[\"month\"] >= 42]\n",
"base = Ftr.groupby(\"part\")[\"y\"].mean()\n",
"pb = Fva[\"part\"].map(base).values\n",
"\n",
"\n",
"def members(d_tr, depth, alpha):\n",
" \"\"\"릿지(선형 추세·계절)로 먼저 맞추고, 그래디언트 부스팅·랜덤 포레스트는 릿지 잔차를 보정하도록 학습한다.\"\"\"\n",
" rd = make_pipeline(StandardScaler(), Ridge(alpha=alpha)).fit(d_tr[FE], d_tr[\"y\"])\n",
" res = d_tr[\"y\"] - rd.predict(d_tr[FE])\n",
" gb = GradientBoostingRegressor(n_estimators=100, max_depth=depth, learning_rate=0.05, subsample=0.8, random_state=0).fit(d_tr[FE], res)\n",
" rf = RandomForestRegressor(n_estimators=40, max_depth=6, min_samples_leaf=5, n_jobs=-1, random_state=0).fit(d_tr[FE], res)\n",
" return [rd, gb, rf]\n",
"\n",
"\n",
"def blend(ms, d, w):\n",
" return ms[0].predict(d[FE]) + w[0] * ms[1].predict(d[FE]) + w[1] * ms[2].predict(d[FE])\n",
"\n",
"\n",
"GRID_W = [(a / 2, b / 2) for a in range(3) for b in range(3)] # 잔차 보정 가중치 격자(GBR, RF) 0, 0.5, 1\n",
"best, solo = (1e9,), {}\n",
"folds = [(Ftr[Ftr[\"month\"] < c], Ftr[(Ftr[\"month\"] >= c) & (Ftr[\"month\"] < c + 6)]) for c in (24, 30, 36)] # 확장 윈도 내부 검증 3폴드\n",
"for dp in (2, 3):\n",
" for al in (1, 10):\n",
" fitted = [(members(a_, dp, al), v) for a_, v in folds]\n",
" for w in GRID_W:\n",
" e_ = np.mean([mean_absolute_error(v[\"y\"], blend(ms, v, w)) for ms, v in fitted])\n",
" solo[(dp, al, w)] = e_\n",
" if e_ < best[0]:\n",
" best = (e_, dp, al, w)\n",
"_, dp, al, wt = best\n",
"if solo[(dp, al, (0.0, 0.0))] * 0.98 < best[0]: # 잔차 보정의 내부 검증 개선이 2% 미만이면 단순한 모델(릿지)을 채택(과적합 방지)\n",
" wt = (0.0, 0.0)\n",
"models = members(Ftr, dp, al)\n",
"pe = blend(models, Fva, wt)\n",
"ev = np.array([(int(a), int(b)) in set(events) for a, b in zip(Fva[\"part\"], Fva[\"month\"])]) # 제조 LOT 불량 등 급증 이벤트 월(별도 경보 대상)\n",
"keep = ~ev\n",
"y = Fva[\"y\"].values\n",
"met = lambda p, m=keep: (mean_absolute_error(y[m], p[m]), mean_squared_error(y[m], p[m]) ** 0.5, r2_score(y[m], p[m]))\n",
"mb, me = met(pb), met(pe)\n",
"print(f\"\\n[취약성 지수 예측] 학습 월 12~41, Hold-out 월 42~47 ({len(Fva)}건 중 급증 이벤트 월 {int(ev.sum())}건 제외 평가), 선택 GBR depth={dp}, ridge alpha={al}, 잔차 보정 가중치(GBR, RF)={wt}\")\n",
"print(\"모델 | MAE | RMSE | R2\")\n",
"print(f\" 기준(학습 기간 부위별 평균) {mb[0]:7.1f} {mb[1]:7.1f} {mb[2]:.3f}\")\n",
"print(f\" 전월값(참고) {met(Fva['lag1'].values)[0]:7.1f} {met(Fva['lag1'].values)[1]:7.1f} {met(Fva['lag1'].values)[2]:.3f}\")\n",
"x = met(models[0].predict(Fva[FE]))\n",
"print(f\" 릿지 단독 {x[0]:7.1f} {x[1]:7.1f} {x[2]:.3f}\")\n",
"print(f\" 고도화(선택 모델) {me[0]:7.1f} {me[1]:7.1f} {me[2]:.3f}\")\n",
"imp_mae, imp_rmse = 1 - me[0] / mb[0], 1 - me[1] / mb[1]\n",
"print(f\" (참고) 이벤트 월 포함 전체: 기준 MAE {met(pb, np.ones(len(y), bool))[0]:.1f} -> 앙상블 {met(pe, np.ones(len(y), bool))[0]:.1f}, R2 {met(pe, np.ones(len(y), bool))[2]:.3f}\")\n",
"print(f\"개선율 MAE {imp_mae:.1%}, RMSE {imp_rmse:.1%} (목표 20% 이상: {'달성' if min(imp_mae, imp_rmse) >= 0.2 else '미달'}), R2 {me[2]:.3f} (목표 0.85 이상: {'달성' if me[2] >= 0.85 else '미달'})\")\n",
"\n",
"# 3) 종합 품질 취약성 지수 순위(다음 달 예측 상위 5 부위) 카드\n",
"last = Fva[Fva[\"month\"] == 47].assign(pred=pe[Fva[\"month\"].values == 47]).sort_values(\"pred\", ascending=False)\n",
"print(\"\\n[취약성 지수 카드] 마지막 달 예측 상위 5 부위 | 예측 | 실제\")\n",
"for r in last.head(5).itertuples():\n",
" print(f\" {names[r.part]:10s} 예측 {r.pred:6.0f} 실제 {r.y:6.0f} {'(우선 점검 권고)' if r.pred >= last['pred'].median() * 1.5 else ''}\")\n",
"print(f\"예측 상위 5와 실제 상위 5 일치 {len(set(last.head(5)['part']) & set(last.sort_values('y', ascending=False).head(5)['part']))}/5\")\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"<details>\n",
"<summary><b>💡 [참고] 미션 05 예상 실행 결과 (Output)</b></summary>\n",
"\n",
"```text\n",
"[교육용가상자료] QDR 51700건 (학습 36개월 36834건, 검증 12개월 14866건)\n",
"치명도 분류 정확도: 키워드 규칙 72.0% -> 텍스트 모델 87.2%, Critical 재현율 34.7% -> 69.0%\n",
"[신뢰도 보정] 구간 | 건수 | 평균 신뢰도 | 실제 정확도\n",
" 0.00~0.60 2394 0.48 0.46\n",
" 0.60~0.80 1827 0.68 0.68\n",
" 0.80~0.95 264 0.86 0.89\n",
" 0.95~1.00 10381 1.00 1.00\n",
"\n",
"[취약성 지수 예측] 학습 월 12~41, Hold-out 월 42~47 (72건 중 급증 이벤트 월 1건 제외 평가), 선택 GBR depth=3, ridge alpha=10, 잔차 보정 가중치(GBR, RF)=(0.0, 0.0)\n",
"모델 | MAE | RMSE | R2\n",
" 기준(학습 기간 부위별 평균) 63.5 88.5 0.582\n",
" 전월값(참고) 51.1 74.9 0.700\n",
" 릿지 단독 39.3 51.8 0.857\n",
" 고도화(선택 모델) 39.3 51.8 0.857\n",
" (참고) 이벤트 월 포함 전체: 기준 MAE 68.6 -> 앙상블 43.9, R2 0.777\n",
"개선율 MAE 38.0%, RMSE 41.4% (목표 20% 이상: 달성), R2 0.857 (목표 0.85 이상: 달성)\n",
"\n",
"[취약성 지수 카드] 마지막 달 예측 상위 5 부위 | 예측 | 실제\n",
" 제동 장치 예측 409 실제 412 (우선 점검 권고)\n",
" 차체 용접부 예측 315 실제 171 \n",
" 엔진 연료계통 예측 304 실제 237 \n",
" 변속기 예측 298 실제 303 \n",
" 조향 장치 예측 272 실제 314 \n",
"예측 상위 5와 실제 상위 5 일치 3/5\n",
"```\n",
"</details>\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## 🎯 미션 06. 야전 QDR 발생 현황 및 장비 부위별 결함 집중도 히트맵 대시보드 구현\n",
"**소요시간**: 약 3.0시간\n",
"\n",
"### 📋 미션 배경 및 수행 지침\n",
"#### 미션 배경 및 당위성\n",
"\n",
"야전 QDR 발생 현황 및 장비 부위별 결함 히트맵 대시보드를 구현한다. 실제 전술 현장에서는 단순 탐지에 그치지 않고 신속한 위협 판단과 신뢰도 높은 의사결정이 필수적이므로, **야전 QDR 발생 현황과 부위별 결함 집중도를 히트맵·경보로 구성하고, 예측 건마다 기여도 상위 3개 인자와 자연어 요약을 자동 산출하는 설명가능성(XAI) 모듈을 구현한다.** 과업을 완수하여 오판을 방지하고 작전 대응력을 극대화해야 합니다.\n",
"\n",
"이 문제를 해결하기 위해 본 미션에서는 **치명도 분류 모델의 서술 단어·부위·결함 유형 기여도를 계산해 건마다 상위 3개 근거를 뽑고, 1순위 근거를 제거했을 때 확률이 내려가는지로 유효성을 점검 수행하고 검증하는** 실습을 체계적으로 수행합니다.\n",
"\n",
"학습자는 실습가이드에 제시된 총 4개 세부 활동(기본 기능 구현 ➔ 결과 지표 검증 ➔ 파라미터 튜닝 및 도전과제)을 순차적으로 진행하며 핵심 판정 기준을 달성합니다.\n",
"\n",
"**이번 결과물:** `src/explainability/qlt05_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": [
"\"\"\"야전 QDR 발생 현황을 부위별·월별 히트맵과 집중도 경보로 구성하고, 치명도 분류 모델의 예측마다 기여도 상위 3개 인자(서술 단어·부위·결함 유형)를 자동 산출하며(근거 유효성 검증), 정비·품질 담당자가 읽을 수 있는 자연어 요약 문장을 생성하는 설명가능성(XAI) 모듈을 구현한다.\"\"\"\n",
"import re\n",
"import unicodedata\n",
"import numpy as np\n",
"import pandas as pd\n",
"import scipy.sparse as sp\n",
"import matplotlib.pyplot as plt\n",
"from sklearn.feature_extraction.text import TfidfVectorizer\n",
"from sklearn.linear_model import LogisticRegression\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# [교육용가상자료] 가상 야전 장비 품질결함보고(QDR) 생성기 (실제 부대·장비·업체·정비 실적 없음)\n",
"# 가상 장비 부위 12종, 결함 유형 8종, 치명도(Critical/Major/Minor), 원인 5종, 가상 외주정비 업체 4곳 + 부대정비.\n",
"# QDR 1건마다 한국어 서술 텍스트를 만들고, 월별 건수는 계절·추세·훈련 강도(usage)·일부 제조 불량 이벤트를 따른다.\n",
"# ---------------------------------------------------------------------------\n",
"PARTS = [ # (이름, 별칭, 월 기준 건수, 계절 진폭, 정점 월, (Critical, Major 비율), 가상 단가지수, 제조사, 주요 결함 가중치)\n",
" (\"엔진 연료계통\", [\"연료계통\", \"연료 라인\", \"연료 펌프\"], 26, 0.15, 7, (0.10, 0.35), 6.0, \"MK1\", \"누유:4 마모:2 체결불량:2 오작동:2\"),\n",
" (\"변속기\", [\"변속기\", \"T/M\", \"미션\"], 22, 0.10, 8, (0.12, 0.38), 9.0, \"MK1\", \"마모:4 누유:2 과열:3 오작동:1\"),\n",
" (\"제동 장치\", [\"제동 장치\", \"브레이크\", \"제동 라인\"], 20, 0.20, 12, (0.26, 0.40), 5.0, \"MK2\", \"마모:4 균열:2 누유:2 체결불량:2\"),\n",
" (\"조향 장치\", [\"조향 장치\", \"핸들 연동부\", \"조향 로드\"], 14, 0.10, 6, (0.30, 0.40), 5.0, \"MK2\", \"체결불량:4 균열:2 마모:3 오작동:1\"),\n",
" (\"냉각 계통\", [\"냉각 계통\", \"라디에이터\", \"냉각수 라인\"], 24, 0.30, 7, (0.05, 0.30), 3.0, \"MK3\", \"누유:4 과열:4 부식:2\"),\n",
" (\"전기·배터리\", [\"배터리\", \"전기 계통\", \"발전기\"], 28, 0.35, 1, (0.04, 0.28), 2.5, \"MK3\", \"접촉불량:4 부식:2 과열:2 오작동:2\"),\n",
" (\"통신 모듈\", [\"통신 모듈\", \"무전기 연동부\", \"안테나 접속부\"], 16, 0.05, 4, (0.03, 0.25), 4.0, \"MK4\", \"접촉불량:5 오작동:4 부식:1\"),\n",
" (\"유압 계통\", [\"유압 계통\", \"유압 호스\", \"유압 실린더\"], 18, 0.10, 9, (0.10, 0.35), 4.5, \"MK2\", \"누유:5 균열:2 마모:2 체결불량:1\"),\n",
" (\"현가 장치\", [\"현가 장치\", \"서스펜션\", \"완충기\"], 15, 0.08, 5, (0.06, 0.30), 3.5, \"MK4\", \"마모:4 균열:3 체결불량:2 부식:1\"),\n",
" (\"차체 용접부\", [\"차체 용접부\", \"프레임 용접부\", \"차체 이음부\"], 10, 0.05, 10, (0.28, 0.40), 7.0, \"MK1\", \"균열:6 부식:3 체결불량:1\"),\n",
" (\"광학·조준 장비\", [\"광학 장비\", \"조준경\", \"관측 장비\"], 8, 0.05, 3, (0.03, 0.22), 8.0, \"MK4\", \"오작동:4 접촉불량:3 부식:2 체결불량:1\"),\n",
" (\"공기 정화 필터\", [\"공기 필터\", \"흡기 필터\", \"정화 장치\"], 12, 0.25, 4, (0.01, 0.12), 1.0, \"MK3\", \"마모:5 부식:2 체결불량:3\")]\n",
"NP = len(PARTS)\n",
"DEFECTS = { # 결함 유형: 증상 표현(동의어)\n",
" \"균열\": [\"균열이 발견됨\", \"크랙이 확인됨\", \"갈라짐이 있음\"], \"누유\": [\"누유가 발생함\", \"오일이 새어 나옴\", \"누설 흔적이 있음\"],\n",
" \"마모\": [\"마모가 심함\", \"과도한 마모가 확인됨\", \"닳은 흔적이 있음\"], \"부식\": [\"부식이 진행됨\", \"녹이 발생함\", \"산화 흔적이 있음\"],\n",
" \"접촉불량\": [\"접촉 불량이 발생함\", \"커넥터 단선이 의심됨\", \"신호가 끊김\"], \"체결불량\": [\"볼트 풀림이 확인됨\", \"체결 상태가 불량함\", \"고정부가 헐거움\"],\n",
" \"과열\": [\"과열 경고가 발생함\", \"온도가 과도하게 상승함\", \"열이 심하게 남\"], \"오작동\": [\"오작동이 반복됨\", \"작동이 불안정함\", \"간헐적으로 멈춤\"]}\n",
"DNAMES = list(DEFECTS)\n",
"SEV = [\"Minor\", \"Major\", \"Critical\"]\n",
"SEV_CUE = {0: [\"경미한 수준임\", \"운용에는 지장이 없음\", \"육안 점검상 사소한 이상임\", \"기능에는 영향이 없음\"],\n",
" 1: [\"성능 저하가 있음\", \"조속한 정비가 필요함\", \"출력이 떨어지는 상태임\", \"기능이 부분적으로 제한됨\"],\n",
" 2: [\"즉시 운용 중지함\", \"안전에 위험이 있어 사용 불가\", \"작동 정지로 임무 수행이 어려움\", \"인명 안전 위협이 우려됨\"]}\n",
"CAUSES = [\"제조 불량\", \"정비 품질\", \"운용 부주의\", \"설계 취약\", \"자연 노후\"]\n",
"CAUSE_CUE = {\n",
" \"제조 불량\": [\"신품 인수 직후 발생함\", \"출고 상태에서 이미 이상이 있었음\", \"인수 후 얼마 지나지 않아 발생함\"],\n",
" \"정비 품질\": [\"외주 정비 후 얼마 되지 않아 재발함\", \"정비 후 재조립 상태가 불량함\", \"정비 직후 같은 부위에서 다시 발생함\"],\n",
" \"운용 부주의\": [\"험지 기동 중 충격 이후 발생함\", \"과부하 운용 후 발생함\", \"점검 없이 연속 운용한 뒤 발생함\"],\n",
" \"설계 취약\": [\"동일 증상이 다른 차량에서도 반복됨\", \"구조적으로 취약한 부위로 보임\", \"여러 부대에서 같은 형태로 보고됨\"],\n",
" \"자연 노후\": [\"사용 연한이 도래함\", \"장기간 사용으로 노후됨\", \"수명 주기 말기에 발생함\"]}\n",
"VENDORS = [\"외주A\", \"외주B\", \"외주C\", \"외주D\", \"부대정비\"]\n",
"ACTIONS = [\"정비소 입고를 요청함\", \"현장 임시 조치 후 운용 중임\", \"부품 교환을 요청함\", \"재검사를 요청함\", \"원인 확인이 필요함\"]\n",
"MAKER_OF = [p[7] for p in PARTS]\n",
"PRICE = np.array([p[6] for p in PARTS])\n",
"\n",
"\n",
"def usage_index(months, seed=0):\n",
" \"\"\"월별 훈련 강도(미리 알 수 있는 계획 값): 연 1회 정점을 갖는 계절 + 작은 잡음\"\"\"\n",
" # [TODO : 학생 실습 6] 야전 QDR 발생 현황 및 장비 부위별 결함 집중도 히트맵 대시보드 구현 관련 핵심 로직을 구현하세요.\n",
" # 힌트: 상단 주석 및 데이터 스키마/지침을 참고하여 작성합니다.\n",
" # <<< 여기에 코드를 작성하세요 >>>\n",
" pass # 작성 후 pass를 제거하세요.\n",
"def make_qdr(months=36, seed=1, scale=1.0, surge=True):\n",
" \"\"\"반환: QDR DataFrame(건당 1행), 월별 usage, 급증 이벤트 목록\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" use = usage_index(months, seed)\n",
" events = []\n",
" if surge:\n",
" for _ in range(max(2, months // 12 * 2)):\n",
" events.append((int(rng.integers(NP)), int(rng.integers(6, months))))\n",
" rows = []\n",
" wdef = [np.array([float(x.split(\":\")[1]) for x in p[8].split()]) for p in PARTS]\n",
" ddef = [[x.split(\":\")[0] for x in p[8].split()] for p in PARTS]\n",
" n = 0\n",
" for m in range(months):\n",
" for pi, p in enumerate(PARTS):\n",
" lam = p[2] * scale * (1 + 0.010 * m) * use[m] * (1 + p[3] * np.cos(2 * np.pi * (m % 12 - p[4]) / 12))\n",
" ev = (pi, m) in events\n",
" k = rng.poisson(lam * (2.6 if ev else 1.0))\n",
" for _ in range(k):\n",
" rows.append((n, m, pi, ev)); n += 1\n",
" N = len(rows)\n",
" A = np.array(rows)\n",
" month, part, ev = A[:, 1], A[:, 2], A[:, 3].astype(bool)\n",
" defect = np.array([rng.choice(ddef[i], p=wdef[i] / wdef[i].sum()) for i in part])\n",
" cr = np.array([PARTS[i][5][0] for i in part]); ma = np.array([PARTS[i][5][1] for i in part])\n",
" u = rng.random(N)\n",
" sev = np.where(u < cr, 2, np.where(u < cr + ma, 1, 0))\n",
" vendor = rng.choice(VENDORS, N, p=[0.25, 0.2, 0.2, 0.15, 0.2])\n",
" # 원인: 기본 비율에 부위(제조사)·업체·이벤트 효과를 곱한다\n",
" base = np.array([0.22, 0.20, 0.20, 0.16, 0.22])\n",
" P = np.tile(base, (N, 1))\n",
" P[vendor == \"외주B\", 1] *= 2.6\n",
" P[ev, 0] *= 5.0\n",
" design = np.isin(part, [2, 3, 7]); P[design, 3] *= 2.2\n",
" P[np.isin(part, [4, 5, 11]), 0] *= 2.0 # 제조사 MK3 납품 부위는 제조 불량 비중이 높다\n",
" P[defect == \"마모\", 4] *= 1.6; P[defect == \"누유\", 4] *= 1.3\n",
" P /= P.sum(axis=1, keepdims=True)\n",
" cause = np.array([CAUSES[rng.choice(5, p=P[i])] for i in range(N)])\n",
" since = np.where(cause == \"정비 품질\", rng.exponential(35, N), rng.exponential(160, N)).clip(1, 720).round().astype(int)\n",
" wait = (rng.lognormal(1.3, 0.5, N) * np.where(vendor == \"외주B\", 1.7, np.where(vendor == \"부대정비\", 0.6, 1.0)) * (0.7 + 0.5 * use[month])).round(1)\n",
" reins = rng.random(N) < np.where(cause == \"정비 품질\", 0.6, 0.08)\n",
" texts = []\n",
" for i in range(N):\n",
" pt = PARTS[part[i]]\n",
" cl = [f\"{rng.choice(pt[1])}에서 {rng.choice(DEFECTS[defect[i]])}\"]\n",
" if rng.random() < 0.7: cl.append(rng.choice(SEV_CUE[sev[i]]))\n",
" if rng.random() < 0.65: # 원인 단서: 8%는 실제 원인과 다른 단서가 적힌다(보고자의 추정)\n",
" cc = cause[i] if rng.random() > 0.08 else rng.choice(CAUSES)\n",
" cl.append(rng.choice(CAUSE_CUE[cc]))\n",
" cl.append(rng.choice(ACTIONS))\n",
" if rng.random() < 0.5: cl.insert(0, f\"VU-{rng.integers(1, 31):02d} 소속\")\n",
" texts.append(\". \".join(cl) + \".\")\n",
" df = pd.DataFrame({\"qdr_id\": [f\"Q{seed}{i:06d}\" for i in range(N)], \"month\": month, \"unit\": [f\"VU-{x:02d}\" for x in rng.integers(1, 31, N)],\n",
" \"part\": part, \"defect\": defect, \"sev\": sev, \"cause\": cause, \"vendor\": vendor, \"maker\": [MAKER_OF[i] for i in part],\n",
" \"since_maint\": since, \"inspect_wait\": wait, \"reinspect\": reins, \"event\": ev, \"text\": texts})\n",
" df[\"cost\"] = PRICE[df[\"part\"]] * np.array([1.0, 2.0, 4.0])[df[\"sev\"]] # [교육용가상자료] 수리·조치 비용지수\n",
" return df, use, events\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# 공통 텍스트 조각: 정규화, 간이 토큰화(조사 제거), 부위·결함·치명도·원인 키워드 라벨링\n",
"# ---------------------------------------------------------------------------\n",
"TYPO = {\"누윳\": \"누유\", \"균렬\": \"균열\", \"접축 불량\": \"접촉 불량\", \"부씩\": \"부식\", \"마묘\": \"마모\", \"과렬\": \"과열\", \"오작똥\": \"오작동\", \"재동\": \"제동\"}\n",
"ABBR = {\"T/M\": \"변속기\", \"BAT\": \"배터리\"}\n",
"PART_KW = sorted([(a, i) for i, p in enumerate(PARTS) for a in p[1]], key=lambda x: -len(x[0]))\n",
"DEF_KW = {\"균열\": [\"균열\", \"크랙\", \"갈라\"], \"누유\": [\"누유\", \"새어\", \"누설\"], \"마모\": [\"마모\", \"닳은\"], \"부식\": [\"부식\", \"녹이\", \"산화\"],\n",
" \"접촉불량\": [\"접촉 불량\", \"단선\", \"끊김\"], \"체결불량\": [\"볼트\", \"체결\", \"헐거\"], \"과열\": [\"과열\", \"온도\", \"열이\"], \"오작동\": [\"오작동\", \"불안정\", \"멈춤\"]}\n",
"SEV_KW = [(2, [\"즉시 운용 중지\", \"사용 불가\", \"위험\"]), (1, [\"성능 저하\", \"조속한\"]), (0, [\"경미\", \"지장이 없\"])]\n",
"CAUSE_KW = {\"제조 불량\": [\"신품\", \"출고\", \"인수\"], \"정비 품질\": [\"정비 후\", \"재조립\", \"정비 직후\"], \"운용 부주의\": [\"충격\", \"과부하\", \"점검 없이\"],\n",
" \"설계 취약\": [\"동일 증상\", \"구조적\", \"여러 부대\"], \"자연 노후\": [\"사용 연한\", \"노후\", \"수명\"]}\n",
"PARTICLES = (\"에서\", \"으로\", \"이\", \"가\", \"은\", \"는\", \"을\", \"를\", \"에\", \"의\", \"로\", \"와\", \"과\", \"도\", \"함\", \"음\", \"됨\")\n",
"\n",
"\n",
"def norm(t):\n",
" \"\"\"정규화: 전각->반각, 특수문자·중복 공백 제거, 약어 확장, 오타 교정\"\"\"\n",
" # [TODO : 학생 실습 6] 야전 QDR 발생 현황 및 장비 부위별 결함 집중도 히트맵 대시보드 구현 관련 핵심 로직을 구현하세요.\n",
" # 힌트: 상단 주석 및 데이터 스키마/지침을 참고하여 작성합니다.\n",
" # <<< 여기에 코드를 작성하세요 >>>\n",
" pass # 작성 후 pass를 제거하세요.\n",
"def tok(t):\n",
" out = []\n",
" for w in re.split(r\"[\\s.,]+\", norm(t)):\n",
" if not w or re.fullmatch(r\"[\\d\\-A-Za-z]+\", w):\n",
" continue\n",
" for p in PARTICLES:\n",
" if len(w) > len(p) + 1 and w.endswith(p):\n",
" w = w[:-len(p)]\n",
" break\n",
" out.append(w)\n",
" return out\n",
"\n",
"\n",
"def label_part(t):\n",
" for a, i in PART_KW:\n",
" if a in t:\n",
" return i\n",
" return -1\n",
"\n",
"\n",
"def label_defect(t):\n",
" for d, ks in DEF_KW.items():\n",
" if any(k in t for k in ks):\n",
" return d\n",
" return \"미분류\"\n",
"\n",
"\n",
"def label_sev(t):\n",
" for s, ks in SEV_KW:\n",
" if any(k in t for k in ks):\n",
" return s\n",
" return -1\n",
"\n",
"\n",
"def label_cause(t):\n",
" for c, ks in CAUSE_KW.items():\n",
" if any(k in t for k in ks):\n",
" return c\n",
" return \"미상\"\n",
"\n",
"df, use, events = make_qdr(36, seed=1, scale=2.0)\n",
"names = [p[0] for p in PARTS]\n",
"tr, va = df[df[\"month\"] < 24].reset_index(drop=True), df[df[\"month\"] >= 24].reset_index(drop=True)\n",
"vec = TfidfVectorizer(tokenizer=tok, lowercase=False, token_pattern=None, min_df=5)\n",
"oh = lambda d: sp.csr_matrix(np.hstack([np.eye(NP)[d[\"part\"]], pd.get_dummies(d[\"defect\"]).reindex(columns=DNAMES, fill_value=0).values.astype(float)]))\n",
"Xtr, Xva = sp.hstack([vec.fit_transform(tr[\"text\"]), oh(tr)]).tocsr(), sp.hstack([vec.transform(va[\"text\"]), oh(va)]).tocsr()\n",
"fnames = list(vec.get_feature_names_out()) + [f\"부위:{n}\" for n in names] + [f\"결함:{d}\" for d in DNAMES]\n",
"clf = LogisticRegression(C=5, max_iter=500).fit(Xtr, tr[\"sev\"])\n",
"P = clf.predict_proba(Xva); pred = P.argmax(axis=1)\n",
"print(f\"[교육용가상자료] QDR {len(df)}건, 검증 {len(va)}건 치명도 분류 정확도 {(pred == va['sev'].values).mean():.1%}\")\n",
"\n",
"# 1) 건마다 기여도 상위 3개 인자 (기여도 = 특성값 x 예측 클래스 계수)\n",
"co = clf.coef_\n",
"top3 = []\n",
"for i in range(Xva.shape[0]):\n",
" row = Xva.getrow(i)\n",
" contrib = row.data * co[pred[i], row.indices]\n",
" order = np.argsort(-contrib)[:3]\n",
" top3.append([(row.indices[j], contrib[j]) for j in order])\n",
"ok = sum(len(t) == 3 for t in top3)\n",
"print(f\"예측 {len(top3)}건 중 기여도 상위 3개 인자 산출 {ok}건 (산출률 {ok / len(top3):.0%}, 목표 100%: {'달성' if ok == len(top3) else '미달'})\")\n",
"\n",
"# 2) 근거 유효성: 1순위 인자를 제거하면 예측 클래스 확률이 낮아지는가 (표본 2,000건)\n",
"idx = np.random.default_rng(0).choice(len(top3), 2000, replace=False)\n",
"X2 = Xva[idx].tolil()\n",
"for r, i in enumerate(idx):\n",
" X2[r, top3[i][0][0]] = 0\n",
"P2 = clf.predict_proba(X2.tocsr())\n",
"drop = P2[np.arange(len(idx)), pred[idx]] < P[idx, pred[idx]]\n",
"print(f\"근거 유효성: 1순위 인자를 제거하면 예측 확률이 낮아지는 비율 {drop.mean():.1%} (평균 {P[idx, pred[idx]].mean():.2f} -> {P2[np.arange(len(idx)), pred[idx]].mean():.2f})\")\n",
"\n",
"\n",
"def summarize(i):\n",
" d = va.iloc[i]\n",
" f = [fnames[j] for j, _ in top3[i]]\n",
" return f\"{names[d['part']]}의 {d['defect']} 결함, 치명도 {SEV[pred[i]]} 가능성 {P[i, pred[i]]:.0%}. 주요 근거: \" + \", \".join(f\"'{x}'\" for x in f) + \".\"\n",
"\n",
"\n",
"# 3) 점검 카드 (치명도별 1건)\n",
"print(\"\\n[점검 카드] 치명도 | 실제 | 설명\")\n",
"for s in (2, 1, 0):\n",
" i = int(np.where((pred == s) & (va[\"sev\"].values == s) & (P.max(axis=1) > 0.9))[0][0])\n",
" print(f\" 예측 {SEV[s]:8s} (실제 {SEV[va.iloc[i]['sev']]})\")\n",
" print(\" \" + summarize(i))\n",
" print(\" 기여도: \" + \" / \".join(f\"{fnames[j]}({c:+.1f})\" for j, c in top3[i]))\n",
"\n",
"# 4) 발생 현황 히트맵(최근 6개월 x 부위)과 집중도 경보: 월 건수 / 학습 기간 월평균 > 1.4 이고 Critical 건이 있으면 경보\n",
"H = df[df[\"month\"] >= 30].pivot_table(index=\"part\", columns=\"month\", values=\"qdr_id\", aggfunc=\"size\", fill_value=0)\n",
"base = tr.groupby(\"part\").size() / 24\n",
"ratio = H.div(base, axis=0)\n",
"alerts = [(int(p), int(m), ratio.loc[p, m]) for p in H.index for m in H.columns if ratio.loc[p, m] > 1.4 and ((df[\"part\"] == p) & (df[\"month\"] == m) & (df[\"sev\"] == 2)).any()]\n",
"print(\"\\n[QDR 발생 히트맵] 부위 | 월 30~35 건수 | 학습 월평균 대비 최대 배수\")\n",
"for p in H.sum(axis=1).sort_values(ascending=False).index[:6]:\n",
" print(f\" {names[p]:10s} \" + \" \".join(f\"{v:4d}\" for v in H.loc[p]) + f\" x{ratio.loc[p].max():.2f}\")\n",
"print(f\"집중도 경보 {len(alerts)}건: \" + \", \".join(f\"{names[p]}({m}월, x{r:.1f})\" for p, m, r in sorted(alerts, key=lambda a: -a[2])[:4]))\n",
"\n",
"fig, ax = plt.subplots(1, 2, figsize=(11, 4))\n",
"im = ax[0].imshow(ratio.values, aspect=\"auto\", cmap=\"Reds\"); ax[0].set_yticks(range(NP)); ax[0].set_yticklabels(range(NP)); ax[0].set_title(\"QDR concentration (x baseline)\"); ax[0].set_xlabel(\"month\"); plt.colorbar(im, ax=ax[0])\n",
"ax[1].hist(P.max(axis=1), bins=20); ax[1].set_title(\"Prediction confidence\")\n",
"plt.tight_layout(); plt.show()\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"<details>\n",
"<summary><b>💡 [참고] 미션 06 예상 실행 결과 (Output)</b></summary>\n",
"\n",
"```text\n",
"[교육용가상자료] QDR 18434건, 검증 6823건 치명도 분류 정확도 87.6%\n",
"예측 6823건 중 기여도 상위 3개 인자 산출 6823건 (산출률 100%, 목표 100%: 달성)\n",
"근거 유효성: 1순위 인자를 제거하면 예측 확률이 낮아지는 비율 100.0% (평균 0.87 -> 0.80)\n",
"\n",
"[점검 카드] 치명도 | 실제 | 설명\n",
" 예측 Critical (실제 Critical)\n",
" 엔진 연료계통의 마모 결함, 치명도 Critical 가능성 99%. 주요 근거: '중지', '즉시', '운용'.\n",
" 기여도: 중지(+2.1) / 즉시(+2.1) / 운용(+0.3)\n",
" 예측 Major (실제 Major)\n",
" 엔진 연료계통의 체결불량 결함, 치명도 Major 가능성 99%. 주요 근거: '기능', '부분적', '제한'.\n",
" 기여도: 기능(+1.3) / 부분적(+1.3) / 제한(+1.3)\n",
" 예측 Minor (실제 Minor)\n",
" 엔진 연료계통의 체결불량 결함, 치명도 Minor 가능성 100%. 주요 근거: '없음', '기능에', '영향'.\n",
" 기여도: 없음(+1.7) / 기능에(+1.4) / 영향(+1.4)\n",
"\n",
"[QDR 발생 히트맵] 부위 | 월 30~35 건수 | 학습 월평균 대비 최대 배수\n",
" 냉각 계통 124 87 99 67 56 38 x2.27\n",
" 엔진 연료계통 99 89 87 75 58 49 x1.61\n",
" 전기·배터리 70 58 67 65 71 71 x1.14\n",
" 제동 장치 53 50 71 55 66 62 x1.50\n",
" 변속기 67 69 60 58 44 45 x1.42\n",
" 유압 계통 54 60 59 63 41 42 x1.57\n",
"집중도 경보 19건: 차체 용접부(34월, x3.0), 냉각 계통(30월, x2.3), 냉각 계통(32월, x1.8), 조향 장치(31월, x1.7)\n",
"```\n",
"</details>\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## 🎯 미션 07. 제조사 하자 구상 및 설계 변경 소요 제기를 위한 품질관 HITL 검토 모듈 설계\n",
"**소요시간**: 약 3.5시간\n",
"\n",
"### 📋 미션 배경 및 수행 지침\n",
"#### 미션 배경 및 당위성\n",
"\n",
"설계 변경 소요 제기를 위한 품질관 HITL 검토 모듈을 설계한다. 실제 전술 현장에서는 단순 탐지에 그치지 않고 신속한 위협 판단과 신뢰도 높은 의사결정이 필수적이므로, **제조사 하자 구상과 설계 변경 소요 제기(외주정비 시정 요구 포함)를 위해 품질관이 근거를 검토·승인하는 HITL 모듈을 설계하고 효과를 시뮬레이션으로 검증한다.** 과업을 완수하여 오판을 방지하고 작전 대응력을 극대화해야 합니다.\n",
"\n",
"이 문제를 해결하기 위해 본 미션에서는 **최근 12개월 QDR의 부위별 원인 비중으로 하자 구상, 설계 변경, 외주정비 시정 후보를 제안하고 승인 대기열에 올린다 과정을 진행하는** 실습을 체계적으로 수행합니다.\n",
"\n",
"학습자는 실습가이드에 제시된 총 4개 세부 활동(기본 기능 구현 ➔ 결과 지표 검증 ➔ 파라미터 튜닝 및 도전과제)을 순차적으로 진행하며 핵심 판정 기준을 달성합니다.\n",
"\n",
"**이번 결과물:** `src/optimization/qlt05_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": [
"\"\"\"최근 12개월 QDR 근거로 부위별 제조사 하자 구상(제조 불량 우세)과 설계 변경 소요 제기(설계 취약 우세) 후보를 제안하되 자동 확정하지 않고 품질관 검토 대기열에 올려 승인·반려·무응답(조치 보류)을 사유와 함께 해시 체인 감사 로그에 남기는 HITL 모듈을 시뮬레이션하고, 개별 사후 수리 대비 비용 절감률과 승인 지연·처리 용량·전시 전환 민감도를 분석한다.\"\"\"\n",
"import hashlib\n",
"import json\n",
"import numpy as np\n",
"import pandas as pd\n",
"import matplotlib.pyplot as plt\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# [교육용가상자료] 가상 야전 장비 품질결함보고(QDR) 생성기 (실제 부대·장비·업체·정비 실적 없음)\n",
"# 가상 장비 부위 12종, 결함 유형 8종, 치명도(Critical/Major/Minor), 원인 5종, 가상 외주정비 업체 4곳 + 부대정비.\n",
"# QDR 1건마다 한국어 서술 텍스트를 만들고, 월별 건수는 계절·추세·훈련 강도(usage)·일부 제조 불량 이벤트를 따른다.\n",
"# ---------------------------------------------------------------------------\n",
"PARTS = [ # (이름, 별칭, 월 기준 건수, 계절 진폭, 정점 월, (Critical, Major 비율), 가상 단가지수, 제조사, 주요 결함 가중치)\n",
" (\"엔진 연료계통\", [\"연료계통\", \"연료 라인\", \"연료 펌프\"], 26, 0.15, 7, (0.10, 0.35), 6.0, \"MK1\", \"누유:4 마모:2 체결불량:2 오작동:2\"),\n",
" (\"변속기\", [\"변속기\", \"T/M\", \"미션\"], 22, 0.10, 8, (0.12, 0.38), 9.0, \"MK1\", \"마모:4 누유:2 과열:3 오작동:1\"),\n",
" (\"제동 장치\", [\"제동 장치\", \"브레이크\", \"제동 라인\"], 20, 0.20, 12, (0.26, 0.40), 5.0, \"MK2\", \"마모:4 균열:2 누유:2 체결불량:2\"),\n",
" (\"조향 장치\", [\"조향 장치\", \"핸들 연동부\", \"조향 로드\"], 14, 0.10, 6, (0.30, 0.40), 5.0, \"MK2\", \"체결불량:4 균열:2 마모:3 오작동:1\"),\n",
" (\"냉각 계통\", [\"냉각 계통\", \"라디에이터\", \"냉각수 라인\"], 24, 0.30, 7, (0.05, 0.30), 3.0, \"MK3\", \"누유:4 과열:4 부식:2\"),\n",
" (\"전기·배터리\", [\"배터리\", \"전기 계통\", \"발전기\"], 28, 0.35, 1, (0.04, 0.28), 2.5, \"MK3\", \"접촉불량:4 부식:2 과열:2 오작동:2\"),\n",
" (\"통신 모듈\", [\"통신 모듈\", \"무전기 연동부\", \"안테나 접속부\"], 16, 0.05, 4, (0.03, 0.25), 4.0, \"MK4\", \"접촉불량:5 오작동:4 부식:1\"),\n",
" (\"유압 계통\", [\"유압 계통\", \"유압 호스\", \"유압 실린더\"], 18, 0.10, 9, (0.10, 0.35), 4.5, \"MK2\", \"누유:5 균열:2 마모:2 체결불량:1\"),\n",
" (\"현가 장치\", [\"현가 장치\", \"서스펜션\", \"완충기\"], 15, 0.08, 5, (0.06, 0.30), 3.5, \"MK4\", \"마모:4 균열:3 체결불량:2 부식:1\"),\n",
" (\"차체 용접부\", [\"차체 용접부\", \"프레임 용접부\", \"차체 이음부\"], 10, 0.05, 10, (0.28, 0.40), 7.0, \"MK1\", \"균열:6 부식:3 체결불량:1\"),\n",
" (\"광학·조준 장비\", [\"광학 장비\", \"조준경\", \"관측 장비\"], 8, 0.05, 3, (0.03, 0.22), 8.0, \"MK4\", \"오작동:4 접촉불량:3 부식:2 체결불량:1\"),\n",
" (\"공기 정화 필터\", [\"공기 필터\", \"흡기 필터\", \"정화 장치\"], 12, 0.25, 4, (0.01, 0.12), 1.0, \"MK3\", \"마모:5 부식:2 체결불량:3\")]\n",
"NP = len(PARTS)\n",
"DEFECTS = { # 결함 유형: 증상 표현(동의어)\n",
" \"균열\": [\"균열이 발견됨\", \"크랙이 확인됨\", \"갈라짐이 있음\"], \"누유\": [\"누유가 발생함\", \"오일이 새어 나옴\", \"누설 흔적이 있음\"],\n",
" \"마모\": [\"마모가 심함\", \"과도한 마모가 확인됨\", \"닳은 흔적이 있음\"], \"부식\": [\"부식이 진행됨\", \"녹이 발생함\", \"산화 흔적이 있음\"],\n",
" \"접촉불량\": [\"접촉 불량이 발생함\", \"커넥터 단선이 의심됨\", \"신호가 끊김\"], \"체결불량\": [\"볼트 풀림이 확인됨\", \"체결 상태가 불량함\", \"고정부가 헐거움\"],\n",
" \"과열\": [\"과열 경고가 발생함\", \"온도가 과도하게 상승함\", \"열이 심하게 남\"], \"오작동\": [\"오작동이 반복됨\", \"작동이 불안정함\", \"간헐적으로 멈춤\"]}\n",
"DNAMES = list(DEFECTS)\n",
"SEV = [\"Minor\", \"Major\", \"Critical\"]\n",
"SEV_CUE = {0: [\"경미한 수준임\", \"운용에는 지장이 없음\", \"육안 점검상 사소한 이상임\", \"기능에는 영향이 없음\"],\n",
" 1: [\"성능 저하가 있음\", \"조속한 정비가 필요함\", \"출력이 떨어지는 상태임\", \"기능이 부분적으로 제한됨\"],\n",
" 2: [\"즉시 운용 중지함\", \"안전에 위험이 있어 사용 불가\", \"작동 정지로 임무 수행이 어려움\", \"인명 안전 위협이 우려됨\"]}\n",
"CAUSES = [\"제조 불량\", \"정비 품질\", \"운용 부주의\", \"설계 취약\", \"자연 노후\"]\n",
"CAUSE_CUE = {\n",
" \"제조 불량\": [\"신품 인수 직후 발생함\", \"출고 상태에서 이미 이상이 있었음\", \"인수 후 얼마 지나지 않아 발생함\"],\n",
" \"정비 품질\": [\"외주 정비 후 얼마 되지 않아 재발함\", \"정비 후 재조립 상태가 불량함\", \"정비 직후 같은 부위에서 다시 발생함\"],\n",
" \"운용 부주의\": [\"험지 기동 중 충격 이후 발생함\", \"과부하 운용 후 발생함\", \"점검 없이 연속 운용한 뒤 발생함\"],\n",
" \"설계 취약\": [\"동일 증상이 다른 차량에서도 반복됨\", \"구조적으로 취약한 부위로 보임\", \"여러 부대에서 같은 형태로 보고됨\"],\n",
" \"자연 노후\": [\"사용 연한이 도래함\", \"장기간 사용으로 노후됨\", \"수명 주기 말기에 발생함\"]}\n",
"VENDORS = [\"외주A\", \"외주B\", \"외주C\", \"외주D\", \"부대정비\"]\n",
"ACTIONS = [\"정비소 입고를 요청함\", \"현장 임시 조치 후 운용 중임\", \"부품 교환을 요청함\", \"재검사를 요청함\", \"원인 확인이 필요함\"]\n",
"MAKER_OF = [p[7] for p in PARTS]\n",
"PRICE = np.array([p[6] for p in PARTS])\n",
"\n",
"\n",
"def usage_index(months, seed=0):\n",
" \"\"\"월별 훈련 강도(미리 알 수 있는 계획 값): 연 1회 정점을 갖는 계절 + 작은 잡음\"\"\"\n",
" # [TODO : 학생 실습 7] 제조사 하자 구상 및 설계 변경 소요 제기를 위한 품질관 HITL 검토 모듈 설계 관련 핵심 로직을 구현하세요.\n",
" # 힌트: 상단 주석 및 데이터 스키마/지침을 참고하여 작성합니다.\n",
" # <<< 여기에 코드를 작성하세요 >>>\n",
" pass # 작성 후 pass를 제거하세요.\n",
"def make_qdr(months=36, seed=1, scale=1.0, surge=True):\n",
" \"\"\"반환: QDR DataFrame(건당 1행), 월별 usage, 급증 이벤트 목록\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" use = usage_index(months, seed)\n",
" events = []\n",
" if surge:\n",
" for _ in range(max(2, months // 12 * 2)):\n",
" events.append((int(rng.integers(NP)), int(rng.integers(6, months))))\n",
" rows = []\n",
" wdef = [np.array([float(x.split(\":\")[1]) for x in p[8].split()]) for p in PARTS]\n",
" ddef = [[x.split(\":\")[0] for x in p[8].split()] for p in PARTS]\n",
" n = 0\n",
" for m in range(months):\n",
" for pi, p in enumerate(PARTS):\n",
" lam = p[2] * scale * (1 + 0.010 * m) * use[m] * (1 + p[3] * np.cos(2 * np.pi * (m % 12 - p[4]) / 12))\n",
" ev = (pi, m) in events\n",
" k = rng.poisson(lam * (2.6 if ev else 1.0))\n",
" for _ in range(k):\n",
" rows.append((n, m, pi, ev)); n += 1\n",
" N = len(rows)\n",
" A = np.array(rows)\n",
" month, part, ev = A[:, 1], A[:, 2], A[:, 3].astype(bool)\n",
" defect = np.array([rng.choice(ddef[i], p=wdef[i] / wdef[i].sum()) for i in part])\n",
" cr = np.array([PARTS[i][5][0] for i in part]); ma = np.array([PARTS[i][5][1] for i in part])\n",
" u = rng.random(N)\n",
" sev = np.where(u < cr, 2, np.where(u < cr + ma, 1, 0))\n",
" vendor = rng.choice(VENDORS, N, p=[0.25, 0.2, 0.2, 0.15, 0.2])\n",
" # 원인: 기본 비율에 부위(제조사)·업체·이벤트 효과를 곱한다\n",
" base = np.array([0.22, 0.20, 0.20, 0.16, 0.22])\n",
" P = np.tile(base, (N, 1))\n",
" P[vendor == \"외주B\", 1] *= 2.6\n",
" P[ev, 0] *= 5.0\n",
" design = np.isin(part, [2, 3, 7]); P[design, 3] *= 2.2\n",
" P[np.isin(part, [4, 5, 11]), 0] *= 2.0 # 제조사 MK3 납품 부위는 제조 불량 비중이 높다\n",
" P[defect == \"마모\", 4] *= 1.6; P[defect == \"누유\", 4] *= 1.3\n",
" P /= P.sum(axis=1, keepdims=True)\n",
" cause = np.array([CAUSES[rng.choice(5, p=P[i])] for i in range(N)])\n",
" since = np.where(cause == \"정비 품질\", rng.exponential(35, N), rng.exponential(160, N)).clip(1, 720).round().astype(int)\n",
" wait = (rng.lognormal(1.3, 0.5, N) * np.where(vendor == \"외주B\", 1.7, np.where(vendor == \"부대정비\", 0.6, 1.0)) * (0.7 + 0.5 * use[month])).round(1)\n",
" reins = rng.random(N) < np.where(cause == \"정비 품질\", 0.6, 0.08)\n",
" texts = []\n",
" for i in range(N):\n",
" pt = PARTS[part[i]]\n",
" cl = [f\"{rng.choice(pt[1])}에서 {rng.choice(DEFECTS[defect[i]])}\"]\n",
" if rng.random() < 0.7: cl.append(rng.choice(SEV_CUE[sev[i]]))\n",
" if rng.random() < 0.65: # 원인 단서: 8%는 실제 원인과 다른 단서가 적힌다(보고자의 추정)\n",
" cc = cause[i] if rng.random() > 0.08 else rng.choice(CAUSES)\n",
" cl.append(rng.choice(CAUSE_CUE[cc]))\n",
" cl.append(rng.choice(ACTIONS))\n",
" if rng.random() < 0.5: cl.insert(0, f\"VU-{rng.integers(1, 31):02d} 소속\")\n",
" texts.append(\". \".join(cl) + \".\")\n",
" df = pd.DataFrame({\"qdr_id\": [f\"Q{seed}{i:06d}\" for i in range(N)], \"month\": month, \"unit\": [f\"VU-{x:02d}\" for x in rng.integers(1, 31, N)],\n",
" \"part\": part, \"defect\": defect, \"sev\": sev, \"cause\": cause, \"vendor\": vendor, \"maker\": [MAKER_OF[i] for i in part],\n",
" \"since_maint\": since, \"inspect_wait\": wait, \"reinspect\": reins, \"event\": ev, \"text\": texts})\n",
" df[\"cost\"] = PRICE[df[\"part\"]] * np.array([1.0, 2.0, 4.0])[df[\"sev\"]] # [교육용가상자료] 수리·조치 비용지수\n",
" return df, use, events\n",
"\n",
"df, use, events = make_qdr(48, seed=1, scale=1.5)\n",
"names = [p[0] for p in PARTS]\n",
"D0 = 24 # 검토 기준 월: 직전 12개월(12~23) 근거로 제안, 이후 24개월(24~47) 효과 평가\n",
"CLAIM_FIXED, CHANGE_FIXED, FIX_VENDOR = 15.0, 100.0, 50.0 # [교육용가상자료] 하자 구상 청구 처리비, 설계 변경 소요 제기·검토 비용지수\n",
"RECOVER, REDUCE, LAG, W_WAIT, REINSPECT = 0.8, 0.8, 3, 0.8, 6.0 # 하자 구상 회수율, 설계 변경 후 해당 원인 QDR 감소율, 설계 변경 반영 지연(개월)\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 proposals(d):\n",
" \"\"\"부위별로 근거(건수·치명도·원인 비중)를 집계해 제안 유형을 정한다.\"\"\"\n",
" # [TODO : 학생 실습 7] 제조사 하자 구상 및 설계 변경 소요 제기를 위한 품질관 HITL 검토 모듈 설계 관련 핵심 로직을 구현하세요.\n",
" # 힌트: 상단 주석 및 데이터 스키마/지침을 참고하여 작성합니다.\n",
" # <<< 여기에 코드를 작성하세요 >>>\n",
" pass # 작성 후 pass를 제거하세요.\n",
"def reviewer(pr, rng, second=False):\n",
" \"\"\"[모의 품질관] 근거가 약하거나 운용·노후 원인이 우세하면 반려, 10%는 기한 내 무응답(재상정 시에는 응답).\"\"\"\n",
" if not second and rng.random() < 0.10:\n",
" return \"무응답\", \"기한 내 응답 없음 -> 조치 보류(기본 계획 유지)\"\n",
" if pr[\"noise\"] > 0.46:\n",
" return \"반려\", f\"운용·노후 원인 {pr['noise']:.0%}로 제조사 책임 근거 부족\"\n",
" if pr[\"share\"] < 0.235 and pr[\"kind\"] != \"외주정비 시정\":\n",
" return \"반려\", f\"원인 비중 {pr['share']:.0%}로 평균 수준, 추가 관찰\"\n",
" if pr[\"n\"] < 120 and pr[\"crit\"] < 0.10:\n",
" return \"반려\", f\"근거 {pr['n']}건, Critical {pr['crit']:.0%}로 추가 관찰\"\n",
" return \"승인\", f\"{pr['kind']} 근거 {pr['n']}건, 원인 비중 {pr['share']:.0%}, Critical {pr['crit']:.0%}\"\n",
"\n",
"\n",
"def effect(d, pr, delay, surge):\n",
" \"\"\"승인된 제안의 이후 12개월 효과(비용지수 이득): 하자 구상은 제조 불량 QDR 비용의 일부 회수, 설계 변경은 설계 취약 QDR 감소\"\"\"\n",
" f = d[(d[\"month\"] >= D0) & (d[\"part\"] == pr[\"part\"])] if pr[\"part\"] >= 0 else d[(d[\"month\"] >= D0) & (d[\"vendor\"] == pr[\"vendor\"])]\n",
" start = D0 + delay\n",
" if pr[\"kind\"] == \"외주정비 시정\":\n",
" g = f[(f[\"cause\"] == \"정비 품질\") & (f[\"month\"] >= start + 2)]\n",
" return 0.7 * (g[\"cost\"] + W_WAIT * g[\"inspect_wait\"] + REINSPECT * g[\"reinspect\"]).sum() * surge - FIX_VENDOR\n",
" if pr[\"kind\"] == \"하자 구상\":\n",
" c = f[(f[\"cause\"] == \"제조 불량\") & (f[\"month\"] >= start)][\"cost\"].sum() * surge\n",
" return RECOVER * c - CLAIM_FIXED\n",
" c = f[(f[\"cause\"] == \"설계 취약\") & (f[\"month\"] >= start + LAG)][\"cost\"].sum() * surge\n",
" return REDUCE * c - CHANGE_FIXED\n",
"\n",
"\n",
"def run(mode, delay=1, surge=1.0, cap=99, seed=0, audit=None, resubmit=True):\n",
" rng = np.random.default_rng(seed)\n",
" props = sorted(proposals(df), key=lambda x: -x[\"n\"] * (1 + 3 * x[\"crit\"]))\n",
" extra = 0\n",
" gain, dec, n_ok = 0.0, {\"승인\": 0, \"반려\": 0, \"무응답\": 0}, 0\n",
" for pr in props:\n",
" if mode == \"auto\":\n",
" d_, why = \"승인\", \"비교용 전부 적용\"\n",
" else:\n",
" audit.add(event=\"PROPOSE\", target=(names[pr[\"part\"]] if pr[\"part\"] >= 0 else pr[\"vendor\"]) + \"/\" + pr[\"kind\"], status=\"PENDING\")\n",
" d_, why = reviewer(pr, rng)\n",
" if d_ == \"승인\" and n_ok >= cap:\n",
" d_, why = \"무응답\", \"월 처리 용량 초과 -> 보류\"\n",
" tg = (names[pr[\"part\"]] if pr[\"part\"] >= 0 else pr[\"vendor\"]) + \"/\" + pr[\"kind\"]\n",
" audit.add(event=\"DECIDE\", target=tg, decision=d_, reason=why, by=\"품질관(모의)\")\n",
" extra = 0\n",
" if d_ == \"무응답\" and resubmit: # 무응답은 기본 계획 유지(조치 보류), 다음 분기 검토 회의에 재상정\n",
" audit.add(event=\"RESUBMIT\", target=tg, status=\"PENDING\", note=\"분기 검토 회의 재상정(자동 승인 아님)\")\n",
" d_, why = reviewer(pr, rng, second=True)\n",
" audit.add(event=\"DECIDE\", target=tg, decision=d_, reason=why + \"(재상정)\", by=\"품질관(모의)\")\n",
" extra = 3\n",
" dec[d_] += 1\n",
" if d_ == \"승인\":\n",
" n_ok += 1; gain += effect(df, pr, (delay + (extra if mode != \"auto\" else 0)) if mode != \"auto\" else 0, surge)\n",
" e = df[df[\"month\"] >= D0]\n",
" base = (e[\"cost\"].sum() + W_WAIT * e[\"inspect_wait\"].sum() + REINSPECT * e[\"reinspect\"].sum()) * surge\n",
" return gain, base, dec, len(props)\n",
"\n",
"\n",
"audit = Audit()\n",
"g_h, base, dec, n_prop = run(\"hitl\", seed=3, audit=audit)\n",
"g_a, _, _, _ = run(\"auto\")\n",
"print(f\"[교육용가상자료] QDR {len(df)}건, 검토 기준 월 {D0}, 후보 제안 {n_prop}건(하자 구상·설계 변경·외주정비 시정)\")\n",
"print(\"\\n[비용 비교] 방식 | 이후 24개월 비용지수 | 절감액\")\n",
"for nm, g in [(\"개별 사후 수리(기본 계획)\", 0), (\"제안 + 품질관 승인\", g_h), (\"제안 전부 적용(비교용)\", g_a)]:\n",
" print(f\" {nm:22s} {base - g:9.0f} {g:8.0f}\")\n",
"cut = g_h / base\n",
"print(f\"HITL 적용 비용 절감률 {cut:.1%} (목표 15% 이상: {'달성' if cut >= 0.15 else '미달'}), 비교용 전부 적용 {g_a / base:.1%}\")\n",
"print(f\"최종 결정 {sum(dec.values())}건: \" + \", \".join(f\"{k} {v}건\" for k, v in dec.items()))\n",
"tgt = {e[\"target\"] for e in audit.log if e[\"event\"] == \"PROPOSE\"}; dec_t = {e[\"target\"] for e in audit.log if e[\"event\"] == \"DECIDE\"}\n",
"n_re = sum(e[\"event\"] == \"RESUBMIT\" for e in audit.log)\n",
"print(f\"제안 {len(tgt)}건 중 결정 기록 {len(tgt & dec_t)}건(로깅률 {len(tgt & dec_t) / len(tgt):.0%}), 자동 승인 0건, 무응답 {n_re}건은 조치 보류 후 분기 회의 재상정\")\n",
"rej = [e[\"reason\"] for e in audit.log if e.get(\"decision\") == \"반려\"]\n",
"print(\"반려 사유 예:\", rej[0] if rej else \"-\")\n",
"print(f\"감사 로그 무결성: {'정상' if audit.verify() is None else '이상'}\", end=\", \")\n",
"audit.log[3][\"reason\"] = \"사후 수정\"\n",
"print(f\"사후 변조 시 seq {audit.verify()}에서 탐지\")\n",
"\n",
"print(\"\\n[민감도 1] 승인까지 지연(개월) -> 절감률\")\n",
"for dl in (0, 1, 3, 6, 9):\n",
" g, b, _, _ = run(\"hitl\", delay=dl, seed=3, audit=Audit())\n",
" print(f\" {dl}개월: {g / b:.1%}\")\n",
"print(\"[민감도 2] 품질관 처리 용량(승인 가능 건수) -> 절감률\")\n",
"for cp in (2, 5, 8, 99):\n",
" g, b, _, _ = run(\"hitl\", cap=cp, seed=3, audit=Audit())\n",
" print(f\" 최대 {cp if cp < 99 else '무제한'}건: {g / b:.1%}\")\n",
"print(\"[민감도 3] 전시 전환 가정(QDR 비용 1.5배 급증) -> 절감률\")\n",
"for sg in (1.0, 1.5, 2.0):\n",
" g, b, _, _ = run(\"hitl\", surge=sg, seed=3, audit=Audit())\n",
" print(f\" x{sg}: {g / b:.1%}\")\n",
"\n",
"plt.figure(figsize=(6, 4))\n",
"plt.bar([\"baseline\", \"HITL\", \"auto(ref)\"], [base, base - g_h, base - g_a]); plt.ylabel(\"cost index (24 months)\"); plt.title(\"Quality review HITL simulator\")\n",
"plt.tight_layout(); plt.show()\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"<details>\n",
"<summary><b>💡 [참고] 미션 07 예상 실행 결과 (Output)</b></summary>\n",
"\n",
"```text\n",
"[교육용가상자료] QDR 19316건, 검토 기준 월 24, 후보 제안 13건(하자 구상·설계 변경·외주정비 시정)\n",
"\n",
"[비용 비교] 방식 | 이후 24개월 비용지수 | 절감액\n",
" 개별 사후 수리(기본 계획) 144670 0\n",
" 제안 + 품질관 승인 122929 21741\n",
" 제안 전부 적용(비교용) 119586 25084\n",
"HITL 적용 비용 절감률 15.0% (목표 15% 이상: 달성), 비교용 전부 적용 17.3%\n",
"최종 결정 13건: 승인 12건, 반려 1건, 무응답 0건\n",
"제안 13건 중 결정 기록 13건(로깅률 100%), 자동 승인 0건, 무응답 2건은 조치 보류 후 분기 회의 재상정\n",
"반려 사유 예: 원인 비중 23%로 평균 수준, 추가 관찰\n",
"감사 로그 무결성: 정상, 사후 변조 시 seq 3에서 탐지\n",
"\n",
"[민감도 1] 승인까지 지연(개월) -> 절감률\n",
" 0개월: 15.9%\n",
" 1개월: 15.0%\n",
" 3개월: 13.6%\n",
" 6개월: 11.4%\n",
" 9개월: 9.2%\n",
"[민감도 2] 품질관 처리 용량(승인 가능 건수) -> 절감률\n",
" 최대 2건: 14.2%\n",
" 최대 5건: 14.4%\n",
" 최대 8건: 14.8%\n",
" 최대 무제한건: 15.0%\n",
"[민감도 3] 전시 전환 가정(QDR 비용 1.5배 급증) -> 절감률\n",
" x1.0: 15.0%\n",
" x1.5: 15.1%\n",
" x2.0: 15.2%\n",
"```\n",
"</details>\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## 🎯 미션 08. 10년치 대용량 야전 QDR 텍스트 데이터 일괄 분석 스트레스 테스트\n",
"**소요시간**: 약 3.0시간\n",
"\n",
"### 📋 미션 배경 및 수행 지침\n",
"#### 미션 배경 및 당위성\n",
"\n",
"대용량 야전 QDR 텍스트 데이터 일괄 분석 스트레스 테스트를 수행한다. 실제 전술 현장에서는 단순 탐지에 그치지 않고 신속한 위협 판단과 신뢰도 높은 의사결정이 필수적이므로, **10년(120개월)치 대용량 야전 QDR 텍스트를 일괄 분석하는 스트레스 시험을 수행해 처리 시간, 화면 구성 시간, 경보 정확도를 확인한다.** 과업을 완수하여 오판을 방지하고 작전 대응력을 극대화해야 합니다.\n",
"\n",
"이 문제를 해결하기 위해 본 미션에서는 **가상 QDR 약 6만 건에 빈 서술, 인코딩 깨짐, 과대 길이 레코드를 섞어 1~10년 단위로 주입해 격리·유실 건수와 분석 시간을 센다 과정을 진행하는** 실습을 체계적으로 수행합니다.\n",
"\n",
"학습자는 실습가이드에 제시된 총 4개 세부 활동(기본 기능 구현 ➔ 결과 지표 검증 ➔ 파라미터 튜닝 및 도전과제)을 순차적으로 진행하며 핵심 판정 기준을 달성합니다.\n",
"\n",
"**이번 결과물:** `src/dashboard/qlt05_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": [
"\"\"\"10년(120개월)치 대용량 야전 QDR 텍스트를 한꺼번에 주입해 정규화·부위/결함/치명도 라벨링·월별 집계·집중도 경보를 일괄 분석하고, 규모별 처리 시간, 불량 레코드(빈 서술·인코딩 깨짐·과대 길이) 격리와 유실 0건, 대시보드 화면 구성·필터 응답 시간, 급증 이벤트에 대한 경보 정확도를 확인하는 스트레스 시험을 수행한다.\"\"\"\n",
"import re\n",
"import time\n",
"import unicodedata\n",
"import numpy as np\n",
"import pandas as pd\n",
"import matplotlib.pyplot as plt\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# [교육용가상자료] 가상 야전 장비 품질결함보고(QDR) 생성기 (실제 부대·장비·업체·정비 실적 없음)\n",
"# 가상 장비 부위 12종, 결함 유형 8종, 치명도(Critical/Major/Minor), 원인 5종, 가상 외주정비 업체 4곳 + 부대정비.\n",
"# QDR 1건마다 한국어 서술 텍스트를 만들고, 월별 건수는 계절·추세·훈련 강도(usage)·일부 제조 불량 이벤트를 따른다.\n",
"# ---------------------------------------------------------------------------\n",
"PARTS = [ # (이름, 별칭, 월 기준 건수, 계절 진폭, 정점 월, (Critical, Major 비율), 가상 단가지수, 제조사, 주요 결함 가중치)\n",
" (\"엔진 연료계통\", [\"연료계통\", \"연료 라인\", \"연료 펌프\"], 26, 0.15, 7, (0.10, 0.35), 6.0, \"MK1\", \"누유:4 마모:2 체결불량:2 오작동:2\"),\n",
" (\"변속기\", [\"변속기\", \"T/M\", \"미션\"], 22, 0.10, 8, (0.12, 0.38), 9.0, \"MK1\", \"마모:4 누유:2 과열:3 오작동:1\"),\n",
" (\"제동 장치\", [\"제동 장치\", \"브레이크\", \"제동 라인\"], 20, 0.20, 12, (0.26, 0.40), 5.0, \"MK2\", \"마모:4 균열:2 누유:2 체결불량:2\"),\n",
" (\"조향 장치\", [\"조향 장치\", \"핸들 연동부\", \"조향 로드\"], 14, 0.10, 6, (0.30, 0.40), 5.0, \"MK2\", \"체결불량:4 균열:2 마모:3 오작동:1\"),\n",
" (\"냉각 계통\", [\"냉각 계통\", \"라디에이터\", \"냉각수 라인\"], 24, 0.30, 7, (0.05, 0.30), 3.0, \"MK3\", \"누유:4 과열:4 부식:2\"),\n",
" (\"전기·배터리\", [\"배터리\", \"전기 계통\", \"발전기\"], 28, 0.35, 1, (0.04, 0.28), 2.5, \"MK3\", \"접촉불량:4 부식:2 과열:2 오작동:2\"),\n",
" (\"통신 모듈\", [\"통신 모듈\", \"무전기 연동부\", \"안테나 접속부\"], 16, 0.05, 4, (0.03, 0.25), 4.0, \"MK4\", \"접촉불량:5 오작동:4 부식:1\"),\n",
" (\"유압 계통\", [\"유압 계통\", \"유압 호스\", \"유압 실린더\"], 18, 0.10, 9, (0.10, 0.35), 4.5, \"MK2\", \"누유:5 균열:2 마모:2 체결불량:1\"),\n",
" (\"현가 장치\", [\"현가 장치\", \"서스펜션\", \"완충기\"], 15, 0.08, 5, (0.06, 0.30), 3.5, \"MK4\", \"마모:4 균열:3 체결불량:2 부식:1\"),\n",
" (\"차체 용접부\", [\"차체 용접부\", \"프레임 용접부\", \"차체 이음부\"], 10, 0.05, 10, (0.28, 0.40), 7.0, \"MK1\", \"균열:6 부식:3 체결불량:1\"),\n",
" (\"광학·조준 장비\", [\"광학 장비\", \"조준경\", \"관측 장비\"], 8, 0.05, 3, (0.03, 0.22), 8.0, \"MK4\", \"오작동:4 접촉불량:3 부식:2 체결불량:1\"),\n",
" (\"공기 정화 필터\", [\"공기 필터\", \"흡기 필터\", \"정화 장치\"], 12, 0.25, 4, (0.01, 0.12), 1.0, \"MK3\", \"마모:5 부식:2 체결불량:3\")]\n",
"NP = len(PARTS)\n",
"DEFECTS = { # 결함 유형: 증상 표현(동의어)\n",
" \"균열\": [\"균열이 발견됨\", \"크랙이 확인됨\", \"갈라짐이 있음\"], \"누유\": [\"누유가 발생함\", \"오일이 새어 나옴\", \"누설 흔적이 있음\"],\n",
" \"마모\": [\"마모가 심함\", \"과도한 마모가 확인됨\", \"닳은 흔적이 있음\"], \"부식\": [\"부식이 진행됨\", \"녹이 발생함\", \"산화 흔적이 있음\"],\n",
" \"접촉불량\": [\"접촉 불량이 발생함\", \"커넥터 단선이 의심됨\", \"신호가 끊김\"], \"체결불량\": [\"볼트 풀림이 확인됨\", \"체결 상태가 불량함\", \"고정부가 헐거움\"],\n",
" \"과열\": [\"과열 경고가 발생함\", \"온도가 과도하게 상승함\", \"열이 심하게 남\"], \"오작동\": [\"오작동이 반복됨\", \"작동이 불안정함\", \"간헐적으로 멈춤\"]}\n",
"DNAMES = list(DEFECTS)\n",
"SEV = [\"Minor\", \"Major\", \"Critical\"]\n",
"SEV_CUE = {0: [\"경미한 수준임\", \"운용에는 지장이 없음\", \"육안 점검상 사소한 이상임\", \"기능에는 영향이 없음\"],\n",
" 1: [\"성능 저하가 있음\", \"조속한 정비가 필요함\", \"출력이 떨어지는 상태임\", \"기능이 부분적으로 제한됨\"],\n",
" 2: [\"즉시 운용 중지함\", \"안전에 위험이 있어 사용 불가\", \"작동 정지로 임무 수행이 어려움\", \"인명 안전 위협이 우려됨\"]}\n",
"CAUSES = [\"제조 불량\", \"정비 품질\", \"운용 부주의\", \"설계 취약\", \"자연 노후\"]\n",
"CAUSE_CUE = {\n",
" \"제조 불량\": [\"신품 인수 직후 발생함\", \"출고 상태에서 이미 이상이 있었음\", \"인수 후 얼마 지나지 않아 발생함\"],\n",
" \"정비 품질\": [\"외주 정비 후 얼마 되지 않아 재발함\", \"정비 후 재조립 상태가 불량함\", \"정비 직후 같은 부위에서 다시 발생함\"],\n",
" \"운용 부주의\": [\"험지 기동 중 충격 이후 발생함\", \"과부하 운용 후 발생함\", \"점검 없이 연속 운용한 뒤 발생함\"],\n",
" \"설계 취약\": [\"동일 증상이 다른 차량에서도 반복됨\", \"구조적으로 취약한 부위로 보임\", \"여러 부대에서 같은 형태로 보고됨\"],\n",
" \"자연 노후\": [\"사용 연한이 도래함\", \"장기간 사용으로 노후됨\", \"수명 주기 말기에 발생함\"]}\n",
"VENDORS = [\"외주A\", \"외주B\", \"외주C\", \"외주D\", \"부대정비\"]\n",
"ACTIONS = [\"정비소 입고를 요청함\", \"현장 임시 조치 후 운용 중임\", \"부품 교환을 요청함\", \"재검사를 요청함\", \"원인 확인이 필요함\"]\n",
"MAKER_OF = [p[7] for p in PARTS]\n",
"PRICE = np.array([p[6] for p in PARTS])\n",
"\n",
"\n",
"def usage_index(months, seed=0):\n",
" \"\"\"월별 훈련 강도(미리 알 수 있는 계획 값): 연 1회 정점을 갖는 계절 + 작은 잡음\"\"\"\n",
" # [TODO : 학생 실습 8] 10년치 대용량 야전 QDR 텍스트 데이터 일괄 분석 스트레스 테스트 관련 핵심 로직을 구현하세요.\n",
" # 힌트: 상단 주석 및 데이터 스키마/지침을 참고하여 작성합니다.\n",
" # <<< 여기에 코드를 작성하세요 >>>\n",
" pass # 작성 후 pass를 제거하세요.\n",
"def make_qdr(months=36, seed=1, scale=1.0, surge=True):\n",
" \"\"\"반환: QDR DataFrame(건당 1행), 월별 usage, 급증 이벤트 목록\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" use = usage_index(months, seed)\n",
" events = []\n",
" if surge:\n",
" for _ in range(max(2, months // 12 * 2)):\n",
" events.append((int(rng.integers(NP)), int(rng.integers(6, months))))\n",
" rows = []\n",
" wdef = [np.array([float(x.split(\":\")[1]) for x in p[8].split()]) for p in PARTS]\n",
" ddef = [[x.split(\":\")[0] for x in p[8].split()] for p in PARTS]\n",
" n = 0\n",
" for m in range(months):\n",
" for pi, p in enumerate(PARTS):\n",
" lam = p[2] * scale * (1 + 0.010 * m) * use[m] * (1 + p[3] * np.cos(2 * np.pi * (m % 12 - p[4]) / 12))\n",
" ev = (pi, m) in events\n",
" k = rng.poisson(lam * (2.6 if ev else 1.0))\n",
" for _ in range(k):\n",
" rows.append((n, m, pi, ev)); n += 1\n",
" N = len(rows)\n",
" A = np.array(rows)\n",
" month, part, ev = A[:, 1], A[:, 2], A[:, 3].astype(bool)\n",
" defect = np.array([rng.choice(ddef[i], p=wdef[i] / wdef[i].sum()) for i in part])\n",
" cr = np.array([PARTS[i][5][0] for i in part]); ma = np.array([PARTS[i][5][1] for i in part])\n",
" u = rng.random(N)\n",
" sev = np.where(u < cr, 2, np.where(u < cr + ma, 1, 0))\n",
" vendor = rng.choice(VENDORS, N, p=[0.25, 0.2, 0.2, 0.15, 0.2])\n",
" # 원인: 기본 비율에 부위(제조사)·업체·이벤트 효과를 곱한다\n",
" base = np.array([0.22, 0.20, 0.20, 0.16, 0.22])\n",
" P = np.tile(base, (N, 1))\n",
" P[vendor == \"외주B\", 1] *= 2.6\n",
" P[ev, 0] *= 5.0\n",
" design = np.isin(part, [2, 3, 7]); P[design, 3] *= 2.2\n",
" P[np.isin(part, [4, 5, 11]), 0] *= 2.0 # 제조사 MK3 납품 부위는 제조 불량 비중이 높다\n",
" P[defect == \"마모\", 4] *= 1.6; P[defect == \"누유\", 4] *= 1.3\n",
" P /= P.sum(axis=1, keepdims=True)\n",
" cause = np.array([CAUSES[rng.choice(5, p=P[i])] for i in range(N)])\n",
" since = np.where(cause == \"정비 품질\", rng.exponential(35, N), rng.exponential(160, N)).clip(1, 720).round().astype(int)\n",
" wait = (rng.lognormal(1.3, 0.5, N) * np.where(vendor == \"외주B\", 1.7, np.where(vendor == \"부대정비\", 0.6, 1.0)) * (0.7 + 0.5 * use[month])).round(1)\n",
" reins = rng.random(N) < np.where(cause == \"정비 품질\", 0.6, 0.08)\n",
" texts = []\n",
" for i in range(N):\n",
" pt = PARTS[part[i]]\n",
" cl = [f\"{rng.choice(pt[1])}에서 {rng.choice(DEFECTS[defect[i]])}\"]\n",
" if rng.random() < 0.7: cl.append(rng.choice(SEV_CUE[sev[i]]))\n",
" if rng.random() < 0.65: # 원인 단서: 8%는 실제 원인과 다른 단서가 적힌다(보고자의 추정)\n",
" cc = cause[i] if rng.random() > 0.08 else rng.choice(CAUSES)\n",
" cl.append(rng.choice(CAUSE_CUE[cc]))\n",
" cl.append(rng.choice(ACTIONS))\n",
" if rng.random() < 0.5: cl.insert(0, f\"VU-{rng.integers(1, 31):02d} 소속\")\n",
" texts.append(\". \".join(cl) + \".\")\n",
" df = pd.DataFrame({\"qdr_id\": [f\"Q{seed}{i:06d}\" for i in range(N)], \"month\": month, \"unit\": [f\"VU-{x:02d}\" for x in rng.integers(1, 31, N)],\n",
" \"part\": part, \"defect\": defect, \"sev\": sev, \"cause\": cause, \"vendor\": vendor, \"maker\": [MAKER_OF[i] for i in part],\n",
" \"since_maint\": since, \"inspect_wait\": wait, \"reinspect\": reins, \"event\": ev, \"text\": texts})\n",
" df[\"cost\"] = PRICE[df[\"part\"]] * np.array([1.0, 2.0, 4.0])[df[\"sev\"]] # [교육용가상자료] 수리·조치 비용지수\n",
" return df, use, events\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# 공통 텍스트 조각: 정규화, 간이 토큰화(조사 제거), 부위·결함·치명도·원인 키워드 라벨링\n",
"# ---------------------------------------------------------------------------\n",
"TYPO = {\"누윳\": \"누유\", \"균렬\": \"균열\", \"접축 불량\": \"접촉 불량\", \"부씩\": \"부식\", \"마묘\": \"마모\", \"과렬\": \"과열\", \"오작똥\": \"오작동\", \"재동\": \"제동\"}\n",
"ABBR = {\"T/M\": \"변속기\", \"BAT\": \"배터리\"}\n",
"PART_KW = sorted([(a, i) for i, p in enumerate(PARTS) for a in p[1]], key=lambda x: -len(x[0]))\n",
"DEF_KW = {\"균열\": [\"균열\", \"크랙\", \"갈라\"], \"누유\": [\"누유\", \"새어\", \"누설\"], \"마모\": [\"마모\", \"닳은\"], \"부식\": [\"부식\", \"녹이\", \"산화\"],\n",
" \"접촉불량\": [\"접촉 불량\", \"단선\", \"끊김\"], \"체결불량\": [\"볼트\", \"체결\", \"헐거\"], \"과열\": [\"과열\", \"온도\", \"열이\"], \"오작동\": [\"오작동\", \"불안정\", \"멈춤\"]}\n",
"SEV_KW = [(2, [\"즉시 운용 중지\", \"사용 불가\", \"위험\"]), (1, [\"성능 저하\", \"조속한\"]), (0, [\"경미\", \"지장이 없\"])]\n",
"CAUSE_KW = {\"제조 불량\": [\"신품\", \"출고\", \"인수\"], \"정비 품질\": [\"정비 후\", \"재조립\", \"정비 직후\"], \"운용 부주의\": [\"충격\", \"과부하\", \"점검 없이\"],\n",
" \"설계 취약\": [\"동일 증상\", \"구조적\", \"여러 부대\"], \"자연 노후\": [\"사용 연한\", \"노후\", \"수명\"]}\n",
"PARTICLES = (\"에서\", \"으로\", \"이\", \"가\", \"은\", \"는\", \"을\", \"를\", \"에\", \"의\", \"로\", \"와\", \"과\", \"도\", \"함\", \"음\", \"됨\")\n",
"\n",
"\n",
"def norm(t):\n",
" \"\"\"정규화: 전각->반각, 특수문자·중복 공백 제거, 약어 확장, 오타 교정\"\"\"\n",
" # [TODO : 학생 실습 8] 10년치 대용량 야전 QDR 텍스트 데이터 일괄 분석 스트레스 테스트 관련 핵심 로직을 구현하세요.\n",
" # 힌트: 상단 주석 및 데이터 스키마/지침을 참고하여 작성합니다.\n",
" # <<< 여기에 코드를 작성하세요 >>>\n",
" pass # 작성 후 pass를 제거하세요.\n",
"def tok(t):\n",
" out = []\n",
" for w in re.split(r\"[\\s.,]+\", norm(t)):\n",
" if not w or re.fullmatch(r\"[\\d\\-A-Za-z]+\", w):\n",
" continue\n",
" for p in PARTICLES:\n",
" if len(w) > len(p) + 1 and w.endswith(p):\n",
" w = w[:-len(p)]\n",
" break\n",
" out.append(w)\n",
" return out\n",
"\n",
"\n",
"def label_part(t):\n",
" for a, i in PART_KW:\n",
" if a in t:\n",
" return i\n",
" return -1\n",
"\n",
"\n",
"def label_defect(t):\n",
" for d, ks in DEF_KW.items():\n",
" if any(k in t for k in ks):\n",
" return d\n",
" return \"미분류\"\n",
"\n",
"\n",
"def label_sev(t):\n",
" for s, ks in SEV_KW:\n",
" if any(k in t for k in ks):\n",
" return s\n",
" return -1\n",
"\n",
"\n",
"def label_cause(t):\n",
" for c, ks in CAUSE_KW.items():\n",
" if any(k in t for k in ks):\n",
" return c\n",
" return \"미상\"\n",
"\n",
"names = [p[0] for p in PARTS]\n",
"t0 = time.perf_counter()\n",
"full, use, events = make_qdr(120, seed=3, scale=1.5)\n",
"print(f\"[교육용가상자료] 10년(120개월) QDR {len(full)}건 생성({time.perf_counter() - t0:.1f}초, 분석 시간에는 포함하지 않음), 부위 {NP}종, 급증 이벤트 {len(events)}건\")\n",
"rng = np.random.default_rng(0)\n",
"RE_SPEC, RE_SPACE = re.compile(r\"[!#~*@^]+\"), re.compile(r\"\\s+\")\n",
"PART_RE = [(re.compile(\"|\".join(map(re.escape, p[1]))), i) for i, p in enumerate(PARTS)]\n",
"DEF_RE = [(d, re.compile(\"|\".join(ks))) for d, ks in DEF_KW.items()]\n",
"\n",
"\n",
"def analyze(d):\n",
" \"\"\"일괄 분석: 검증(격리) -> 정규화 -> 부위·결함·치명도 라벨링. 반환: (정상 결과, 격리 건수)\"\"\"\n",
" ok = d[\"text\"].str.len().between(5, 400) & ~d[\"text\"].str.contains(\"�\", regex=False)\n",
" g = d[ok].copy()\n",
" g[\"t\"] = g[\"text\"].str.replace(RE_SPEC, \" \", regex=True).str.replace(RE_SPACE, \" \", regex=True).str.strip()\n",
" g[\"p_lab\"] = -1\n",
" for rx, i in PART_RE:\n",
" g.loc[(g[\"p_lab\"] == -1) & g[\"t\"].str.contains(rx), \"p_lab\"] = i\n",
" g[\"d_lab\"] = \"미분류\"\n",
" for dn, rx in DEF_RE:\n",
" g.loc[(g[\"d_lab\"] == \"미분류\") & g[\"t\"].str.contains(rx), \"d_lab\"] = dn\n",
" g[\"s_lab\"] = np.where(g[\"t\"].str.contains(\"즉시 운용 중지|사용 불가|위험|임무 수행이 어려움|위협\"), 2, np.where(g[\"t\"].str.contains(\"성능 저하|조속한|출력이|부분적으로\"), 1, 0))\n",
" return g, int((~ok).sum())\n",
"\n",
"\n",
"def alarms(g, months):\n",
" \"\"\"월·부위 집계 후 직전 12개월 발생률(훈련 강도 보정) 대비 급증 경보\"\"\"\n",
" M = g.pivot_table(index=\"p_lab\", columns=\"month\", values=\"qdr_id\", aggfunc=\"size\", fill_value=0).reindex(index=range(NP), columns=range(months), fill_value=0)\n",
" rate = (M / use[:months]).T.rolling(12, min_periods=6).mean().shift(1).T\n",
" exp = rate * use[:months]\n",
" z = (M - exp) / np.sqrt(exp.clip(lower=1))\n",
" return M, (M / exp.clip(lower=1) > 1.9) & (z > 4)\n",
"\n",
"\n",
"def make_input(n_months, bad=0.01):\n",
" d = full[full[\"month\"] < n_months].copy()\n",
" k = int(len(d) * bad)\n",
" idx = rng.choice(len(d), k, replace=False)\n",
" d.iloc[idx[: k // 3], d.columns.get_loc(\"text\")] = \"\"\n",
" d.iloc[idx[k // 3: 2 * k // 3], d.columns.get_loc(\"text\")] = \"��� 오류\"\n",
" d.iloc[idx[2 * k // 3:], d.columns.get_loc(\"text\")] = \"가\" * 900\n",
" return d, k\n",
"\n",
"\n",
"print(\"\\n[기간별 일괄 분석] 기간 | 입력 | 정상 | 격리 | 유실 | 분석 시간 | 처리량\")\n",
"res = {}\n",
"for months in (12, 36, 72, 120):\n",
" d, k = make_input(months)\n",
" t = time.perf_counter()\n",
" g, q = analyze(d)\n",
" M, A = alarms(g, months)\n",
" sec = time.perf_counter() - t\n",
" res[months] = (g, M, A, sec)\n",
" print(f\" {months // 12:2d}년 {len(d):7d} {len(g):7d} {q:5d} {len(d) - len(g) - q:3d} {sec:6.2f}초 {len(d) / sec:8.0f}건/초\")\n",
"g, M, A, sec = res[120]\n",
"\n",
"# 라벨링 품질과 경보 정확도\n",
"tr_ = full.set_index(\"qdr_id\").loc[g[\"qdr_id\"]]\n",
"print(f\"\\n라벨링 정확도(10년치): 부위 {(g['p_lab'].values == tr_['part'].values).mean():.1%}, 결함 유형 {(g['d_lab'].values == tr_['defect'].values).mean():.1%}, 치명도 {(g['s_lab'].values == tr_['sev'].values).mean():.1%} (단서 없는 건은 Minor 처리)\")\n",
"truth = np.zeros((NP, 120), bool)\n",
"for p, m in events:\n",
" truth[p, m] = True\n",
"valid = ~np.isnan(A.values.astype(float)) & (np.arange(120) >= 12)[None, :]\n",
"al = A.values & valid\n",
"tp, fp, fn = (al & truth).sum(), (al & ~truth).sum(), (~al & truth & valid).sum()\n",
"acc = ((al == truth) & valid).sum() / valid.sum()\n",
"print(f\"급증 경보(12개월 이후 {int(valid.sum())}개 부위-월 셀): 경보 정확도 {acc:.1%} (목표 95% 이상: {'달성' if acc >= 0.95 else '미달'}), 재현율 {tp / max(tp + fn, 1):.0%}, 정밀도 {tp / max(tp + fp, 1):.0%}, 실제 이벤트 {int((truth & valid).sum())}건\")\n",
"\n",
"# 대시보드 화면 구성: 10년치 집계표에서 히트맵·상위 5 부위·경보 목록을 만드는 시간, 필터(부위·기간) 응답 시간\n",
"t = time.perf_counter()\n",
"heat = M.values.reshape(NP, 10, 12).sum(axis=2)\n",
"top5 = M.sum(axis=1).sort_values(ascending=False).head(5)\n",
"alert_list = [(names[p], m) for p, m in zip(*np.where(al))]\n",
"load = time.perf_counter() - t\n",
"t = time.perf_counter()\n",
"for p in range(NP):\n",
" sub = g[(g[\"p_lab\"] == p) & (g[\"month\"] >= 84)]\n",
" _ = sub.groupby(\"d_lab\").size()\n",
"flt = (time.perf_counter() - t) / NP\n",
"print(f\"화면 구성(연도별 히트맵·상위 5·경보 {len(alert_list)}건) {load * 1000:.0f}ms, 필터 1회 응답 {flt * 1000:.0f}ms (목표 2초 이내: {'달성' if max(load, flt) <= 2 else '미달'})\")\n",
"print(\"상위 5 부위: \" + \", \".join(f\"{names[p]} {v}\" for p, v in top5.items()))\n",
"print(\"운용 기준: 일괄 분석은 야간 배치로 집계표를 갱신하고 화면은 집계표만 읽는다. 처리가 느려지면 최근 36개월만 재계산하고 격리율이 5%를 넘으면 서술 입력 체계를 점검한다.\")\n",
"\n",
"plt.figure(figsize=(6, 4))\n",
"ms = sorted(res)\n",
"plt.plot([m // 12 for m in ms], [res[m][3] for m in ms], marker=\"o\"); plt.xlabel(\"years of QDR\"); plt.ylabel(\"analysis seconds\"); plt.title(\"Bulk analysis time\")\n",
"plt.tight_layout(); plt.show()\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"<details>\n",
"<summary><b>💡 [참고] 미션 08 예상 실행 결과 (Output)</b></summary>\n",
"\n",
"```text\n",
"[교육용가상자료] 10년(120개월) QDR 62293건 생성(6.5초, 분석 시간에는 포함하지 않음), 부위 12종, 급증 이벤트 20건\n",
"\n",
"[기간별 일괄 분석] 기간 | 입력 | 정상 | 격리 | 유실 | 분석 시간 | 처리량\n",
" 1년 4177 4136 41 0 0.14초 30805건/초\n",
" 3년 14068 13928 140 0 0.36초 39039건/초\n",
" 6년 32040 31720 320 0 0.77초 41671건/초\n",
" 10년 62293 61671 622 0 1.78초 34950건/초\n",
"\n",
"라벨링 정확도(10년치): 부위 100.0%, 결함 유형 100.0%, 치명도 87.0% (단서 없는 건은 Minor 처리)\n",
"급증 경보(12개월 이후 1296개 부위-월 셀): 경보 정확도 99.8% (목표 95% 이상: 달성), 재현율 90%, 정밀도 100%, 실제 이벤트 20건\n",
"화면 구성(연도별 히트맵·상위 5·경보 18건) 1ms, 필터 1회 응답 4ms (목표 2초 이내: 달성)\n",
"상위 5 부위: 엔진 연료계통 7848, 전기·배터리 7816, 냉각 계통 7095, 변속기 6301, 제동 장치 5667\n",
"운용 기준: 일괄 분석은 야간 배치로 집계표를 갱신하고 화면은 집계표만 읽는다. 처리가 느려지면 최근 36개월만 재계산하고 격리율이 5%를 넘으면 서술 입력 체계를 점검한다.\n",
"```\n",
"</details>\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## 🎯 미션 09. AAR 기반 결함 원인 오분류 분석 및 텍스트 마이닝 룰셋 재튜닝\n",
"**소요시간**: 약 3.0시간\n",
"\n",
"### 📋 미션 배경 및 수행 지침\n",
"#### 미션 배경 및 당위성\n",
"\n",
"AAR 기반 결함 원인 오분류를 분석하고 마이닝 룰셋을 재튜닝한다. 실제 전술 현장에서는 단순 탐지에 그치지 않고 신속한 위협 판단과 신뢰도 높은 의사결정이 필수적이므로, **AAR 기반으로 결함 원인 오분류 사례를 분석하고 텍스트 마이닝 룰셋을 보조 규칙으로 재튜닝해 효과를 지표로 산출한다.** 과업을 완수하여 오판을 방지하고 작전 대응력을 극대화해야 합니다.\n",
"\n",
"이 문제를 해결하기 위해 본 미션에서는 **AAR 기간 원인 분류 오류를 원인 단서 없음과 단서-실제 원인 불일치로 나누고 대표 사례 정리하고 규격화하는** 실습을 체계적으로 수행합니다.\n",
"\n",
"학습자는 실습가이드에 제시된 총 4개 세부 활동(기본 기능 구현 ➔ 결과 지표 검증 ➔ 파라미터 튜닝 및 도전과제)을 순차적으로 진행하며 핵심 판정 기준을 달성합니다.\n",
"\n",
"**이번 결과물:** `reports/qlt05_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 기간(12~23개월) QDR의 결함 원인 분류 오류를 원인별로 분석하고, 키워드 기반 텍스트 마이닝 룰셋에 외주 정비 후 경과일·업체·결함 유형 등 보조 규칙(결정 트리로 도출)을 더해 재튜닝한 뒤, 검증 기간(24~35개월)에서 정확도와 오분류 비용 변화를 확인하고 3대 정량 효과 지표(비용 절감, 확인 시간 단축, 제조·정비 책임 건 식별 증가)와 도입 로드맵을 산출한다.\"\"\"\n",
"import re\n",
"import unicodedata\n",
"import numpy as np\n",
"import pandas as pd\n",
"from sklearn.tree import DecisionTreeClassifier, export_text\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# [교육용가상자료] 가상 야전 장비 품질결함보고(QDR) 생성기 (실제 부대·장비·업체·정비 실적 없음)\n",
"# 가상 장비 부위 12종, 결함 유형 8종, 치명도(Critical/Major/Minor), 원인 5종, 가상 외주정비 업체 4곳 + 부대정비.\n",
"# QDR 1건마다 한국어 서술 텍스트를 만들고, 월별 건수는 계절·추세·훈련 강도(usage)·일부 제조 불량 이벤트를 따른다.\n",
"# ---------------------------------------------------------------------------\n",
"PARTS = [ # (이름, 별칭, 월 기준 건수, 계절 진폭, 정점 월, (Critical, Major 비율), 가상 단가지수, 제조사, 주요 결함 가중치)\n",
" (\"엔진 연료계통\", [\"연료계통\", \"연료 라인\", \"연료 펌프\"], 26, 0.15, 7, (0.10, 0.35), 6.0, \"MK1\", \"누유:4 마모:2 체결불량:2 오작동:2\"),\n",
" (\"변속기\", [\"변속기\", \"T/M\", \"미션\"], 22, 0.10, 8, (0.12, 0.38), 9.0, \"MK1\", \"마모:4 누유:2 과열:3 오작동:1\"),\n",
" (\"제동 장치\", [\"제동 장치\", \"브레이크\", \"제동 라인\"], 20, 0.20, 12, (0.26, 0.40), 5.0, \"MK2\", \"마모:4 균열:2 누유:2 체결불량:2\"),\n",
" (\"조향 장치\", [\"조향 장치\", \"핸들 연동부\", \"조향 로드\"], 14, 0.10, 6, (0.30, 0.40), 5.0, \"MK2\", \"체결불량:4 균열:2 마모:3 오작동:1\"),\n",
" (\"냉각 계통\", [\"냉각 계통\", \"라디에이터\", \"냉각수 라인\"], 24, 0.30, 7, (0.05, 0.30), 3.0, \"MK3\", \"누유:4 과열:4 부식:2\"),\n",
" (\"전기·배터리\", [\"배터리\", \"전기 계통\", \"발전기\"], 28, 0.35, 1, (0.04, 0.28), 2.5, \"MK3\", \"접촉불량:4 부식:2 과열:2 오작동:2\"),\n",
" (\"통신 모듈\", [\"통신 모듈\", \"무전기 연동부\", \"안테나 접속부\"], 16, 0.05, 4, (0.03, 0.25), 4.0, \"MK4\", \"접촉불량:5 오작동:4 부식:1\"),\n",
" (\"유압 계통\", [\"유압 계통\", \"유압 호스\", \"유압 실린더\"], 18, 0.10, 9, (0.10, 0.35), 4.5, \"MK2\", \"누유:5 균열:2 마모:2 체결불량:1\"),\n",
" (\"현가 장치\", [\"현가 장치\", \"서스펜션\", \"완충기\"], 15, 0.08, 5, (0.06, 0.30), 3.5, \"MK4\", \"마모:4 균열:3 체결불량:2 부식:1\"),\n",
" (\"차체 용접부\", [\"차체 용접부\", \"프레임 용접부\", \"차체 이음부\"], 10, 0.05, 10, (0.28, 0.40), 7.0, \"MK1\", \"균열:6 부식:3 체결불량:1\"),\n",
" (\"광학·조준 장비\", [\"광학 장비\", \"조준경\", \"관측 장비\"], 8, 0.05, 3, (0.03, 0.22), 8.0, \"MK4\", \"오작동:4 접촉불량:3 부식:2 체결불량:1\"),\n",
" (\"공기 정화 필터\", [\"공기 필터\", \"흡기 필터\", \"정화 장치\"], 12, 0.25, 4, (0.01, 0.12), 1.0, \"MK3\", \"마모:5 부식:2 체결불량:3\")]\n",
"NP = len(PARTS)\n",
"DEFECTS = { # 결함 유형: 증상 표현(동의어)\n",
" \"균열\": [\"균열이 발견됨\", \"크랙이 확인됨\", \"갈라짐이 있음\"], \"누유\": [\"누유가 발생함\", \"오일이 새어 나옴\", \"누설 흔적이 있음\"],\n",
" \"마모\": [\"마모가 심함\", \"과도한 마모가 확인됨\", \"닳은 흔적이 있음\"], \"부식\": [\"부식이 진행됨\", \"녹이 발생함\", \"산화 흔적이 있음\"],\n",
" \"접촉불량\": [\"접촉 불량이 발생함\", \"커넥터 단선이 의심됨\", \"신호가 끊김\"], \"체결불량\": [\"볼트 풀림이 확인됨\", \"체결 상태가 불량함\", \"고정부가 헐거움\"],\n",
" \"과열\": [\"과열 경고가 발생함\", \"온도가 과도하게 상승함\", \"열이 심하게 남\"], \"오작동\": [\"오작동이 반복됨\", \"작동이 불안정함\", \"간헐적으로 멈춤\"]}\n",
"DNAMES = list(DEFECTS)\n",
"SEV = [\"Minor\", \"Major\", \"Critical\"]\n",
"SEV_CUE = {0: [\"경미한 수준임\", \"운용에는 지장이 없음\", \"육안 점검상 사소한 이상임\", \"기능에는 영향이 없음\"],\n",
" 1: [\"성능 저하가 있음\", \"조속한 정비가 필요함\", \"출력이 떨어지는 상태임\", \"기능이 부분적으로 제한됨\"],\n",
" 2: [\"즉시 운용 중지함\", \"안전에 위험이 있어 사용 불가\", \"작동 정지로 임무 수행이 어려움\", \"인명 안전 위협이 우려됨\"]}\n",
"CAUSES = [\"제조 불량\", \"정비 품질\", \"운용 부주의\", \"설계 취약\", \"자연 노후\"]\n",
"CAUSE_CUE = {\n",
" \"제조 불량\": [\"신품 인수 직후 발생함\", \"출고 상태에서 이미 이상이 있었음\", \"인수 후 얼마 지나지 않아 발생함\"],\n",
" \"정비 품질\": [\"외주 정비 후 얼마 되지 않아 재발함\", \"정비 후 재조립 상태가 불량함\", \"정비 직후 같은 부위에서 다시 발생함\"],\n",
" \"운용 부주의\": [\"험지 기동 중 충격 이후 발생함\", \"과부하 운용 후 발생함\", \"점검 없이 연속 운용한 뒤 발생함\"],\n",
" \"설계 취약\": [\"동일 증상이 다른 차량에서도 반복됨\", \"구조적으로 취약한 부위로 보임\", \"여러 부대에서 같은 형태로 보고됨\"],\n",
" \"자연 노후\": [\"사용 연한이 도래함\", \"장기간 사용으로 노후됨\", \"수명 주기 말기에 발생함\"]}\n",
"VENDORS = [\"외주A\", \"외주B\", \"외주C\", \"외주D\", \"부대정비\"]\n",
"ACTIONS = [\"정비소 입고를 요청함\", \"현장 임시 조치 후 운용 중임\", \"부품 교환을 요청함\", \"재검사를 요청함\", \"원인 확인이 필요함\"]\n",
"MAKER_OF = [p[7] for p in PARTS]\n",
"PRICE = np.array([p[6] for p in PARTS])\n",
"\n",
"\n",
"def usage_index(months, seed=0):\n",
" \"\"\"월별 훈련 강도(미리 알 수 있는 계획 값): 연 1회 정점을 갖는 계절 + 작은 잡음\"\"\"\n",
" # [TODO : 학생 실습 9] AAR 기반 결함 원인 오분류 분석 및 텍스트 마이닝 룰셋 재튜닝 관련 핵심 로직을 구현하세요.\n",
" # 힌트: 상단 주석 및 데이터 스키마/지침을 참고하여 작성합니다.\n",
" # <<< 여기에 코드를 작성하세요 >>>\n",
" pass # 작성 후 pass를 제거하세요.\n",
"def make_qdr(months=36, seed=1, scale=1.0, surge=True):\n",
" \"\"\"반환: QDR DataFrame(건당 1행), 월별 usage, 급증 이벤트 목록\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" use = usage_index(months, seed)\n",
" events = []\n",
" if surge:\n",
" for _ in range(max(2, months // 12 * 2)):\n",
" events.append((int(rng.integers(NP)), int(rng.integers(6, months))))\n",
" rows = []\n",
" wdef = [np.array([float(x.split(\":\")[1]) for x in p[8].split()]) for p in PARTS]\n",
" ddef = [[x.split(\":\")[0] for x in p[8].split()] for p in PARTS]\n",
" n = 0\n",
" for m in range(months):\n",
" for pi, p in enumerate(PARTS):\n",
" lam = p[2] * scale * (1 + 0.010 * m) * use[m] * (1 + p[3] * np.cos(2 * np.pi * (m % 12 - p[4]) / 12))\n",
" ev = (pi, m) in events\n",
" k = rng.poisson(lam * (2.6 if ev else 1.0))\n",
" for _ in range(k):\n",
" rows.append((n, m, pi, ev)); n += 1\n",
" N = len(rows)\n",
" A = np.array(rows)\n",
" month, part, ev = A[:, 1], A[:, 2], A[:, 3].astype(bool)\n",
" defect = np.array([rng.choice(ddef[i], p=wdef[i] / wdef[i].sum()) for i in part])\n",
" cr = np.array([PARTS[i][5][0] for i in part]); ma = np.array([PARTS[i][5][1] for i in part])\n",
" u = rng.random(N)\n",
" sev = np.where(u < cr, 2, np.where(u < cr + ma, 1, 0))\n",
" vendor = rng.choice(VENDORS, N, p=[0.25, 0.2, 0.2, 0.15, 0.2])\n",
" # 원인: 기본 비율에 부위(제조사)·업체·이벤트 효과를 곱한다\n",
" base = np.array([0.22, 0.20, 0.20, 0.16, 0.22])\n",
" P = np.tile(base, (N, 1))\n",
" P[vendor == \"외주B\", 1] *= 2.6\n",
" P[ev, 0] *= 5.0\n",
" design = np.isin(part, [2, 3, 7]); P[design, 3] *= 2.2\n",
" P[np.isin(part, [4, 5, 11]), 0] *= 2.0 # 제조사 MK3 납품 부위는 제조 불량 비중이 높다\n",
" P[defect == \"마모\", 4] *= 1.6; P[defect == \"누유\", 4] *= 1.3\n",
" P /= P.sum(axis=1, keepdims=True)\n",
" cause = np.array([CAUSES[rng.choice(5, p=P[i])] for i in range(N)])\n",
" since = np.where(cause == \"정비 품질\", rng.exponential(35, N), rng.exponential(160, N)).clip(1, 720).round().astype(int)\n",
" wait = (rng.lognormal(1.3, 0.5, N) * np.where(vendor == \"외주B\", 1.7, np.where(vendor == \"부대정비\", 0.6, 1.0)) * (0.7 + 0.5 * use[month])).round(1)\n",
" reins = rng.random(N) < np.where(cause == \"정비 품질\", 0.6, 0.08)\n",
" texts = []\n",
" for i in range(N):\n",
" pt = PARTS[part[i]]\n",
" cl = [f\"{rng.choice(pt[1])}에서 {rng.choice(DEFECTS[defect[i]])}\"]\n",
" if rng.random() < 0.7: cl.append(rng.choice(SEV_CUE[sev[i]]))\n",
" if rng.random() < 0.65: # 원인 단서: 8%는 실제 원인과 다른 단서가 적힌다(보고자의 추정)\n",
" cc = cause[i] if rng.random() > 0.08 else rng.choice(CAUSES)\n",
" cl.append(rng.choice(CAUSE_CUE[cc]))\n",
" cl.append(rng.choice(ACTIONS))\n",
" if rng.random() < 0.5: cl.insert(0, f\"VU-{rng.integers(1, 31):02d} 소속\")\n",
" texts.append(\". \".join(cl) + \".\")\n",
" df = pd.DataFrame({\"qdr_id\": [f\"Q{seed}{i:06d}\" for i in range(N)], \"month\": month, \"unit\": [f\"VU-{x:02d}\" for x in rng.integers(1, 31, N)],\n",
" \"part\": part, \"defect\": defect, \"sev\": sev, \"cause\": cause, \"vendor\": vendor, \"maker\": [MAKER_OF[i] for i in part],\n",
" \"since_maint\": since, \"inspect_wait\": wait, \"reinspect\": reins, \"event\": ev, \"text\": texts})\n",
" df[\"cost\"] = PRICE[df[\"part\"]] * np.array([1.0, 2.0, 4.0])[df[\"sev\"]] # [교육용가상자료] 수리·조치 비용지수\n",
" return df, use, events\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# 공통 텍스트 조각: 정규화, 간이 토큰화(조사 제거), 부위·결함·치명도·원인 키워드 라벨링\n",
"# ---------------------------------------------------------------------------\n",
"TYPO = {\"누윳\": \"누유\", \"균렬\": \"균열\", \"접축 불량\": \"접촉 불량\", \"부씩\": \"부식\", \"마묘\": \"마모\", \"과렬\": \"과열\", \"오작똥\": \"오작동\", \"재동\": \"제동\"}\n",
"ABBR = {\"T/M\": \"변속기\", \"BAT\": \"배터리\"}\n",
"PART_KW = sorted([(a, i) for i, p in enumerate(PARTS) for a in p[1]], key=lambda x: -len(x[0]))\n",
"DEF_KW = {\"균열\": [\"균열\", \"크랙\", \"갈라\"], \"누유\": [\"누유\", \"새어\", \"누설\"], \"마모\": [\"마모\", \"닳은\"], \"부식\": [\"부식\", \"녹이\", \"산화\"],\n",
" \"접촉불량\": [\"접촉 불량\", \"단선\", \"끊김\"], \"체결불량\": [\"볼트\", \"체결\", \"헐거\"], \"과열\": [\"과열\", \"온도\", \"열이\"], \"오작동\": [\"오작동\", \"불안정\", \"멈춤\"]}\n",
"SEV_KW = [(2, [\"즉시 운용 중지\", \"사용 불가\", \"위험\"]), (1, [\"성능 저하\", \"조속한\"]), (0, [\"경미\", \"지장이 없\"])]\n",
"CAUSE_KW = {\"제조 불량\": [\"신품\", \"출고\", \"인수\"], \"정비 품질\": [\"정비 후\", \"재조립\", \"정비 직후\"], \"운용 부주의\": [\"충격\", \"과부하\", \"점검 없이\"],\n",
" \"설계 취약\": [\"동일 증상\", \"구조적\", \"여러 부대\"], \"자연 노후\": [\"사용 연한\", \"노후\", \"수명\"]}\n",
"PARTICLES = (\"에서\", \"으로\", \"이\", \"가\", \"은\", \"는\", \"을\", \"를\", \"에\", \"의\", \"로\", \"와\", \"과\", \"도\", \"함\", \"음\", \"됨\")\n",
"\n",
"\n",
"def norm(t):\n",
" \"\"\"정규화: 전각->반각, 특수문자·중복 공백 제거, 약어 확장, 오타 교정\"\"\"\n",
" # [TODO : 학생 실습 9] AAR 기반 결함 원인 오분류 분석 및 텍스트 마이닝 룰셋 재튜닝 관련 핵심 로직을 구현하세요.\n",
" # 힌트: 상단 주석 및 데이터 스키마/지침을 참고하여 작성합니다.\n",
" # <<< 여기에 코드를 작성하세요 >>>\n",
" pass # 작성 후 pass를 제거하세요.\n",
"def tok(t):\n",
" out = []\n",
" for w in re.split(r\"[\\s.,]+\", norm(t)):\n",
" if not w or re.fullmatch(r\"[\\d\\-A-Za-z]+\", w):\n",
" continue\n",
" for p in PARTICLES:\n",
" if len(w) > len(p) + 1 and w.endswith(p):\n",
" w = w[:-len(p)]\n",
" break\n",
" out.append(w)\n",
" return out\n",
"\n",
"\n",
"def label_part(t):\n",
" for a, i in PART_KW:\n",
" if a in t:\n",
" return i\n",
" return -1\n",
"\n",
"\n",
"def label_defect(t):\n",
" for d, ks in DEF_KW.items():\n",
" if any(k in t for k in ks):\n",
" return d\n",
" return \"미분류\"\n",
"\n",
"\n",
"def label_sev(t):\n",
" for s, ks in SEV_KW:\n",
" if any(k in t for k in ks):\n",
" return s\n",
" return -1\n",
"\n",
"\n",
"def label_cause(t):\n",
" for c, ks in CAUSE_KW.items():\n",
" if any(k in t for k in ks):\n",
" return c\n",
" return \"미상\"\n",
"\n",
"df, use, events = make_qdr(36, seed=1, scale=1.5)\n",
"df[\"t\"] = df[\"text\"].map(norm)\n",
"aar, val = df[(df[\"month\"] >= 12) & (df[\"month\"] < 24)].copy(), df[df[\"month\"] >= 24].copy()\n",
"print(f\"[교육용가상자료] AAR 기간 {len(aar)}건, 검증 기간 {len(val)}건\")\n",
"\n",
"# 1) 기존 룰셋: 서술 속 원인 단서 키워드(첫 일치)\n",
"for d in (aar, val):\n",
" d[\"rule0\"] = d[\"t\"].map(label_cause)\n",
"wrong = aar[aar[\"rule0\"] != aar[\"cause\"]]\n",
"no_cue = wrong[wrong[\"rule0\"] == \"미상\"]\n",
"conflict = wrong[wrong[\"rule0\"] != \"미상\"]\n",
"print(f\"기존 룰셋 정확도: AAR {1 - len(wrong) / len(aar):.1%}, 검증 {(val['rule0'] == val['cause']).mean():.1%}\")\n",
"print(f\"AAR 오분류 {len(wrong)}건 ({len(wrong) / len(aar):.1%}): 원인 단서 없음(미상) {len(no_cue)}건 ({len(no_cue) / len(wrong):.0%}), 단서와 실제 원인 불일치 {len(conflict)}건 ({len(conflict) / len(wrong):.0%})\")\n",
"print(\" 미상 건의 실제 원인: \" + \", \".join(f\"{k} {v}\" for k, v in no_cue[\"cause\"].value_counts().items()))\n",
"c0 = conflict.groupby([\"rule0\", \"cause\"]).size().sort_values(ascending=False).head(2)\n",
"print(\" 불일치 상위: \" + \", \".join(f\"룰 '{a}' -> 실제 '{b}' {v}건\" for (a, b), v in c0.items()))\n",
"ex = no_cue[no_cue[\"cause\"] == \"정비 품질\"].iloc[0]\n",
"print(f\" 사례: '{ex['text']}' (업체 {ex['vendor']}, 정비 후 {ex['since_maint']}일) -> 룰 미상, 실제 정비 품질\")\n",
"\n",
"# 2) 재튜닝: 미상 건에 보조 규칙 적용(AAR 기간 미상 건만으로 결정 트리 학습, 깊이 3)\n",
"def meta(d):\n",
" X = pd.DataFrame({\"since_maint\": d[\"since_maint\"], \"ext_vendor\": (d[\"vendor\"] != \"부대정비\").astype(int), \"vendor_B\": (d[\"vendor\"] == \"외주B\").astype(int),\n",
" \"wait\": d[\"inspect_wait\"], \"reinspect\": d[\"reinspect\"].astype(int), \"sev\": d[\"sev\"], \"wear\": d[\"defect\"].isin([\"마모\", \"부식\"]).astype(int),\n",
" \"leak\": (d[\"defect\"] == \"누유\").astype(int)})\n",
" for p_ in (2, 3, 7):\n",
" X[f\"part{p_}\"] = (d[\"part\"] == p_).astype(int)\n",
" for p_ in (4, 5, 11):\n",
" X[f\"part{p_}\"] = (d[\"part\"] == p_).astype(int)\n",
" return X\n",
"\n",
"\n",
"tree = DecisionTreeClassifier(max_depth=3, min_samples_leaf=40, random_state=0).fit(meta(no_cue.assign(cause=no_cue[\"cause\"])), no_cue[\"cause\"])\n",
"print(\"\\n[재튜닝 보조 규칙(결정 트리, 미상 건 대상)]\")\n",
"for line in export_text(tree, feature_names=list(meta(no_cue).columns), max_depth=3).splitlines()[:9]:\n",
" print(\" \" + line)\n",
"\n",
"\n",
"CONF = 0.45\n",
"\n",
"\n",
"def rule1(d):\n",
" out = d[\"rule0\"].copy()\n",
" unk = out == \"미상\"\n",
" if unk.any():\n",
" pr = tree.predict_proba(meta(d[unk]))\n",
" out[unk] = np.where(pr.max(axis=1) >= CONF, tree.classes_[pr.argmax(axis=1)], \"미상\") # 확신이 낮으면 미상으로 남겨 사람이 확인\n",
" return out\n",
"\n",
"\n",
"for d in (aar, val):\n",
" d[\"rule1\"] = rule1(d)\n",
"acc = lambda d, c: (d[c] == d[\"cause\"]).mean()\n",
"print(f\"\\n정확도 변화: AAR(튜닝) {acc(aar, 'rule0'):.1%} -> {acc(aar, 'rule1'):.1%}, 검증(미사용) {acc(val, 'rule0'):.1%} -> {acc(val, 'rule1'):.1%}\")\n",
"for c in (\"제조 불량\", \"정비 품질\"):\n",
" m = val[\"cause\"] == c\n",
" print(f\" {c} 재현율(검증): {(val.loc[m, 'rule0'] == c).mean():.0%} -> {(val.loc[m, 'rule1'] == c).mean():.0%}\")\n",
"\n",
"# 3) 3대 정량 효과 지표 (검증 12개월, 가정 단가)\n",
"REWORK, HOURS = 0.5, 0.8 # [교육용가상자료] 오분류 건당 재작업 비용 = 조치 비용 x 0.5, 미상 건 수작업 원인 확인 0.8시간\n",
"cost0, cost1 = (val.loc[val[\"rule0\"] != val[\"cause\"], \"cost\"].sum() * REWORK), (val.loc[val[\"rule1\"] != val[\"cause\"], \"cost\"].sum() * REWORK)\n",
"h0, h1 = (val[\"rule0\"] == \"미상\").sum() * HOURS, (val[\"rule1\"] == \"미상\").sum() * HOURS\n",
"ext0, ext1 = ((val[\"rule0\"].isin([\"제조 불량\", \"정비 품질\"])) & (val[\"cause\"].isin([\"제조 불량\", \"정비 품질\"])) & (val[\"rule0\"] == val[\"cause\"])).sum(), \\\n",
" ((val[\"rule1\"].isin([\"제조 불량\", \"정비 품질\"])) & (val[\"cause\"].isin([\"제조 불량\", \"정비 품질\"])) & (val[\"rule1\"] == val[\"cause\"])).sum()\n",
"print(\"\\n[3대 정량 효과 지표] (검증 12개월, 가정 단가)\")\n",
"print(f\" 1) 비용 절감: 오분류 재작업 비용지수 {cost0:.0f} -> {cost1:.0f} ({1 - cost1 / cost0:.1%} 절감), 연 환산 약 {cost0 - cost1:.0f}\")\n",
"print(f\" 2) 시간 단축: 원인 수작업 확인 {h0:.0f}h -> {h1:.0f}h ({1 - h1 / h0:.1%} 단축, 확신 {CONF:.0%} 미만은 미상으로 남겨 사람이 확인)\")\n",
"print(f\" 3) 제조·정비 책임 건 정확 식별: {ext0}건 -> {ext1}건 (+{ext1 / ext0 - 1:.0%}), 하자 구상·시정 요구 근거 확보 증가\")\n",
"print(\"[도입 로드맵] 단기(1~3월): 보조 규칙 적용 + 품질관 확인 병행 / 중기(3~12월): 분기 AAR로 규칙 재학습, 단서 용어 사전 확장 / 장기(1년~): 군수 정보체계 정비 이력과 원인 라벨 환류\")\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"<details>\n",
"<summary><b>💡 [참고] 미션 09 예상 실행 결과 (Output)</b></summary>\n",
"\n",
"```text\n",
"[교육용가상자료] AAR 기간 4568건, 검증 기간 5103건\n",
"기존 룰셋 정확도: AAR 60.3%, 검증 61.7%\n",
"AAR 오분류 1815건 (39.7%): 원인 단서 없음(미상) 1617건 (89%), 단서와 실제 원인 불일치 198건 (11%)\n",
" 미상 건의 실제 원인: 제조 불량 407, 자연 노후 354, 정비 품질 331, 설계 취약 266, 운용 부주의 259\n",
" 불일치 상위: 룰 '설계 취약' -> 실제 '정비 품질' 15건, 룰 '제조 불량' -> 실제 '정비 품질' 15건\n",
" 사례: '연료계통에서 누설 흔적이 있음. 재검사를 요청함.' (업체 외주B, 정비 후 6일) -> 룰 미상, 실제 정비 품질\n",
"\n",
"[재튜닝 보조 규칙(결정 트리, 미상 건 대상)]\n",
" |--- reinspect <= 0.50\n",
" | |--- since_maint <= 62.50\n",
" | | |--- vendor_B <= 0.50\n",
" | | | |--- class: 제조 불량\n",
" | | |--- vendor_B > 0.50\n",
" | | | |--- class: 정비 품질\n",
" | |--- since_maint > 62.50\n",
" | | |--- part5 <= 0.50\n",
" | | | |--- class: 자연 노후\n",
"\n",
"정확도 변화: AAR(튜닝) 60.3% -> 65.4%, 검증(미사용) 61.7% -> 66.2%\n",
" 제조 불량 재현율(검증): 63% -> 67%\n",
" 정비 품질 재현율(검증): 62% -> 79%\n",
"\n",
"[3대 정량 효과 지표] (검증 12개월, 가정 단가)\n",
" 1) 비용 절감: 오분류 재작업 비용지수 8127 -> 7253 (10.8% 절감), 연 환산 약 874\n",
" 2) 시간 단축: 원인 수작업 확인 1409h -> 1126h (20.1% 단축, 확신 45% 미만은 미상으로 남겨 사람이 확인)\n",
" 3) 제조·정비 책임 건 정확 식별: 1454건 -> 1685건 (+16%), 하자 구상·시정 요구 근거 확보 증가\n",
"[도입 로드맵] 단기(1~3월): 보조 규칙 적용 + 품질관 확인 병행 / 중기(3~12월): 분기 AAR로 규칙 재학습, 단서 용어 사전 확장 / 장기(1년~): 군수 정보체계 정비 이력과 원인 라벨 환류\n",
"```\n",
"</details>\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## 🎯 미션 10. 야전 장비 결함 보고(QDR) 데이터 분석 및 품질 취약점 식별 도구 통합 완성\n",
"**소요시간**: 약 3.5시간\n",
"\n",
"### 📋 미션 배경 및 수행 지침\n",
"#### 미션 배경 및 당위성\n",
"\n",
"야전 장비 결함 보고(QDR) 데이터 분석 및 품질 취약점 식별 도구를 완성한다. 실제 전술 현장에서는 단순 탐지에 그치지 않고 신속한 위협 판단과 신뢰도 높은 의사결정이 필수적이므로, **QDR 검증·정제부터 치명도 분류, 취약성 지수·경보, 원인 분류, 승인 대기 후보 제안, 재학습까지 한 파일로 통합하고 자체 점검과 브리핑 요약을 출력하는 도구를 완성한다.** 과업을 완수하여 오판을 방지하고 작전 대응력을 극대화해야 합니다.\n",
"\n",
"이 문제를 해결하기 위해 본 미션에서는 **지난달·이번 달 실적으로 챔피언 모델과 도전자 모델을 학습해 검증 정확도를 비교하고 비슷하거나 나을 때만 교체 수행하고 검증하는** 실습을 체계적으로 수행합니다.\n",
"\n",
"학습자는 실습가이드에 제시된 총 4개 세부 활동(기본 기능 구현 ➔ 결과 지표 검증 ➔ 파라미터 튜닝 및 도전과제)을 순차적으로 진행하며 핵심 판정 기준을 달성합니다.\n",
"\n",
"**이번 결과물:** `docs/qlt05_operations_guide.md 및 pipeline/qlt05_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": [
"\"\"\"QDR 서술 검증·정제·라벨링, 치명도 분류 모델의 자동 재학습(챔피언·도전자 비교 후 교체), 부위별 취약성 지수와 급증 경보, 재튜닝 규칙의 원인 분류, 하자 구상·설계 변경·외주정비 시정 후보의 품질관 승인 대기 목록까지 야전 QDR 품질 취약점 식별 도구를 한 파일로 통합 실행하고, 자체 점검·운영 문서 완비 점검·품질 담당 장교 브리핑 요약을 출력한다.\"\"\"\n",
"import re\n",
"import time\n",
"import unicodedata\n",
"import numpy as np\n",
"import pandas as pd\n",
"import scipy.sparse as sp\n",
"import matplotlib.pyplot as plt\n",
"from sklearn.feature_extraction.text import TfidfVectorizer\n",
"from sklearn.linear_model import LogisticRegression\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# [교육용가상자료] 가상 야전 장비 품질결함보고(QDR) 생성기 (실제 부대·장비·업체·정비 실적 없음)\n",
"# 가상 장비 부위 12종, 결함 유형 8종, 치명도(Critical/Major/Minor), 원인 5종, 가상 외주정비 업체 4곳 + 부대정비.\n",
"# QDR 1건마다 한국어 서술 텍스트를 만들고, 월별 건수는 계절·추세·훈련 강도(usage)·일부 제조 불량 이벤트를 따른다.\n",
"# ---------------------------------------------------------------------------\n",
"PARTS = [ # (이름, 별칭, 월 기준 건수, 계절 진폭, 정점 월, (Critical, Major 비율), 가상 단가지수, 제조사, 주요 결함 가중치)\n",
" (\"엔진 연료계통\", [\"연료계통\", \"연료 라인\", \"연료 펌프\"], 26, 0.15, 7, (0.10, 0.35), 6.0, \"MK1\", \"누유:4 마모:2 체결불량:2 오작동:2\"),\n",
" (\"변속기\", [\"변속기\", \"T/M\", \"미션\"], 22, 0.10, 8, (0.12, 0.38), 9.0, \"MK1\", \"마모:4 누유:2 과열:3 오작동:1\"),\n",
" (\"제동 장치\", [\"제동 장치\", \"브레이크\", \"제동 라인\"], 20, 0.20, 12, (0.26, 0.40), 5.0, \"MK2\", \"마모:4 균열:2 누유:2 체결불량:2\"),\n",
" (\"조향 장치\", [\"조향 장치\", \"핸들 연동부\", \"조향 로드\"], 14, 0.10, 6, (0.30, 0.40), 5.0, \"MK2\", \"체결불량:4 균열:2 마모:3 오작동:1\"),\n",
" (\"냉각 계통\", [\"냉각 계통\", \"라디에이터\", \"냉각수 라인\"], 24, 0.30, 7, (0.05, 0.30), 3.0, \"MK3\", \"누유:4 과열:4 부식:2\"),\n",
" (\"전기·배터리\", [\"배터리\", \"전기 계통\", \"발전기\"], 28, 0.35, 1, (0.04, 0.28), 2.5, \"MK3\", \"접촉불량:4 부식:2 과열:2 오작동:2\"),\n",
" (\"통신 모듈\", [\"통신 모듈\", \"무전기 연동부\", \"안테나 접속부\"], 16, 0.05, 4, (0.03, 0.25), 4.0, \"MK4\", \"접촉불량:5 오작동:4 부식:1\"),\n",
" (\"유압 계통\", [\"유압 계통\", \"유압 호스\", \"유압 실린더\"], 18, 0.10, 9, (0.10, 0.35), 4.5, \"MK2\", \"누유:5 균열:2 마모:2 체결불량:1\"),\n",
" (\"현가 장치\", [\"현가 장치\", \"서스펜션\", \"완충기\"], 15, 0.08, 5, (0.06, 0.30), 3.5, \"MK4\", \"마모:4 균열:3 체결불량:2 부식:1\"),\n",
" (\"차체 용접부\", [\"차체 용접부\", \"프레임 용접부\", \"차체 이음부\"], 10, 0.05, 10, (0.28, 0.40), 7.0, \"MK1\", \"균열:6 부식:3 체결불량:1\"),\n",
" (\"광학·조준 장비\", [\"광학 장비\", \"조준경\", \"관측 장비\"], 8, 0.05, 3, (0.03, 0.22), 8.0, \"MK4\", \"오작동:4 접촉불량:3 부식:2 체결불량:1\"),\n",
" (\"공기 정화 필터\", [\"공기 필터\", \"흡기 필터\", \"정화 장치\"], 12, 0.25, 4, (0.01, 0.12), 1.0, \"MK3\", \"마모:5 부식:2 체결불량:3\")]\n",
"NP = len(PARTS)\n",
"DEFECTS = { # 결함 유형: 증상 표현(동의어)\n",
" \"균열\": [\"균열이 발견됨\", \"크랙이 확인됨\", \"갈라짐이 있음\"], \"누유\": [\"누유가 발생함\", \"오일이 새어 나옴\", \"누설 흔적이 있음\"],\n",
" \"마모\": [\"마모가 심함\", \"과도한 마모가 확인됨\", \"닳은 흔적이 있음\"], \"부식\": [\"부식이 진행됨\", \"녹이 발생함\", \"산화 흔적이 있음\"],\n",
" \"접촉불량\": [\"접촉 불량이 발생함\", \"커넥터 단선이 의심됨\", \"신호가 끊김\"], \"체결불량\": [\"볼트 풀림이 확인됨\", \"체결 상태가 불량함\", \"고정부가 헐거움\"],\n",
" \"과열\": [\"과열 경고가 발생함\", \"온도가 과도하게 상승함\", \"열이 심하게 남\"], \"오작동\": [\"오작동이 반복됨\", \"작동이 불안정함\", \"간헐적으로 멈춤\"]}\n",
"DNAMES = list(DEFECTS)\n",
"SEV = [\"Minor\", \"Major\", \"Critical\"]\n",
"SEV_CUE = {0: [\"경미한 수준임\", \"운용에는 지장이 없음\", \"육안 점검상 사소한 이상임\", \"기능에는 영향이 없음\"],\n",
" 1: [\"성능 저하가 있음\", \"조속한 정비가 필요함\", \"출력이 떨어지는 상태임\", \"기능이 부분적으로 제한됨\"],\n",
" 2: [\"즉시 운용 중지함\", \"안전에 위험이 있어 사용 불가\", \"작동 정지로 임무 수행이 어려움\", \"인명 안전 위협이 우려됨\"]}\n",
"CAUSES = [\"제조 불량\", \"정비 품질\", \"운용 부주의\", \"설계 취약\", \"자연 노후\"]\n",
"CAUSE_CUE = {\n",
" \"제조 불량\": [\"신품 인수 직후 발생함\", \"출고 상태에서 이미 이상이 있었음\", \"인수 후 얼마 지나지 않아 발생함\"],\n",
" \"정비 품질\": [\"외주 정비 후 얼마 되지 않아 재발함\", \"정비 후 재조립 상태가 불량함\", \"정비 직후 같은 부위에서 다시 발생함\"],\n",
" \"운용 부주의\": [\"험지 기동 중 충격 이후 발생함\", \"과부하 운용 후 발생함\", \"점검 없이 연속 운용한 뒤 발생함\"],\n",
" \"설계 취약\": [\"동일 증상이 다른 차량에서도 반복됨\", \"구조적으로 취약한 부위로 보임\", \"여러 부대에서 같은 형태로 보고됨\"],\n",
" \"자연 노후\": [\"사용 연한이 도래함\", \"장기간 사용으로 노후됨\", \"수명 주기 말기에 발생함\"]}\n",
"VENDORS = [\"외주A\", \"외주B\", \"외주C\", \"외주D\", \"부대정비\"]\n",
"ACTIONS = [\"정비소 입고를 요청함\", \"현장 임시 조치 후 운용 중임\", \"부품 교환을 요청함\", \"재검사를 요청함\", \"원인 확인이 필요함\"]\n",
"MAKER_OF = [p[7] for p in PARTS]\n",
"PRICE = np.array([p[6] for p in PARTS])\n",
"\n",
"\n",
"def usage_index(months, seed=0):\n",
" \"\"\"월별 훈련 강도(미리 알 수 있는 계획 값): 연 1회 정점을 갖는 계절 + 작은 잡음\"\"\"\n",
" # [TODO : 학생 실습 10] 야전 장비 결함 보고(QDR) 데이터 분석 및 품질 취약점 식별 도구 통합 완성 관련 핵심 로직을 구현하세요.\n",
" # 힌트: 상단 주석 및 데이터 스키마/지침을 참고하여 작성합니다.\n",
" # <<< 여기에 코드를 작성하세요 >>>\n",
" pass # 작성 후 pass를 제거하세요.\n",
"def make_qdr(months=36, seed=1, scale=1.0, surge=True):\n",
" \"\"\"반환: QDR DataFrame(건당 1행), 월별 usage, 급증 이벤트 목록\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" use = usage_index(months, seed)\n",
" events = []\n",
" if surge:\n",
" for _ in range(max(2, months // 12 * 2)):\n",
" events.append((int(rng.integers(NP)), int(rng.integers(6, months))))\n",
" rows = []\n",
" wdef = [np.array([float(x.split(\":\")[1]) for x in p[8].split()]) for p in PARTS]\n",
" ddef = [[x.split(\":\")[0] for x in p[8].split()] for p in PARTS]\n",
" n = 0\n",
" for m in range(months):\n",
" for pi, p in enumerate(PARTS):\n",
" lam = p[2] * scale * (1 + 0.010 * m) * use[m] * (1 + p[3] * np.cos(2 * np.pi * (m % 12 - p[4]) / 12))\n",
" ev = (pi, m) in events\n",
" k = rng.poisson(lam * (2.6 if ev else 1.0))\n",
" for _ in range(k):\n",
" rows.append((n, m, pi, ev)); n += 1\n",
" N = len(rows)\n",
" A = np.array(rows)\n",
" month, part, ev = A[:, 1], A[:, 2], A[:, 3].astype(bool)\n",
" defect = np.array([rng.choice(ddef[i], p=wdef[i] / wdef[i].sum()) for i in part])\n",
" cr = np.array([PARTS[i][5][0] for i in part]); ma = np.array([PARTS[i][5][1] for i in part])\n",
" u = rng.random(N)\n",
" sev = np.where(u < cr, 2, np.where(u < cr + ma, 1, 0))\n",
" vendor = rng.choice(VENDORS, N, p=[0.25, 0.2, 0.2, 0.15, 0.2])\n",
" # 원인: 기본 비율에 부위(제조사)·업체·이벤트 효과를 곱한다\n",
" base = np.array([0.22, 0.20, 0.20, 0.16, 0.22])\n",
" P = np.tile(base, (N, 1))\n",
" P[vendor == \"외주B\", 1] *= 2.6\n",
" P[ev, 0] *= 5.0\n",
" design = np.isin(part, [2, 3, 7]); P[design, 3] *= 2.2\n",
" P[np.isin(part, [4, 5, 11]), 0] *= 2.0 # 제조사 MK3 납품 부위는 제조 불량 비중이 높다\n",
" P[defect == \"마모\", 4] *= 1.6; P[defect == \"누유\", 4] *= 1.3\n",
" P /= P.sum(axis=1, keepdims=True)\n",
" cause = np.array([CAUSES[rng.choice(5, p=P[i])] for i in range(N)])\n",
" since = np.where(cause == \"정비 품질\", rng.exponential(35, N), rng.exponential(160, N)).clip(1, 720).round().astype(int)\n",
" wait = (rng.lognormal(1.3, 0.5, N) * np.where(vendor == \"외주B\", 1.7, np.where(vendor == \"부대정비\", 0.6, 1.0)) * (0.7 + 0.5 * use[month])).round(1)\n",
" reins = rng.random(N) < np.where(cause == \"정비 품질\", 0.6, 0.08)\n",
" texts = []\n",
" for i in range(N):\n",
" pt = PARTS[part[i]]\n",
" cl = [f\"{rng.choice(pt[1])}에서 {rng.choice(DEFECTS[defect[i]])}\"]\n",
" if rng.random() < 0.7: cl.append(rng.choice(SEV_CUE[sev[i]]))\n",
" if rng.random() < 0.65: # 원인 단서: 8%는 실제 원인과 다른 단서가 적힌다(보고자의 추정)\n",
" cc = cause[i] if rng.random() > 0.08 else rng.choice(CAUSES)\n",
" cl.append(rng.choice(CAUSE_CUE[cc]))\n",
" cl.append(rng.choice(ACTIONS))\n",
" if rng.random() < 0.5: cl.insert(0, f\"VU-{rng.integers(1, 31):02d} 소속\")\n",
" texts.append(\". \".join(cl) + \".\")\n",
" df = pd.DataFrame({\"qdr_id\": [f\"Q{seed}{i:06d}\" for i in range(N)], \"month\": month, \"unit\": [f\"VU-{x:02d}\" for x in rng.integers(1, 31, N)],\n",
" \"part\": part, \"defect\": defect, \"sev\": sev, \"cause\": cause, \"vendor\": vendor, \"maker\": [MAKER_OF[i] for i in part],\n",
" \"since_maint\": since, \"inspect_wait\": wait, \"reinspect\": reins, \"event\": ev, \"text\": texts})\n",
" df[\"cost\"] = PRICE[df[\"part\"]] * np.array([1.0, 2.0, 4.0])[df[\"sev\"]] # [교육용가상자료] 수리·조치 비용지수\n",
" return df, use, events\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# 공통 텍스트 조각: 정규화, 간이 토큰화(조사 제거), 부위·결함·치명도·원인 키워드 라벨링\n",
"# ---------------------------------------------------------------------------\n",
"TYPO = {\"누윳\": \"누유\", \"균렬\": \"균열\", \"접축 불량\": \"접촉 불량\", \"부씩\": \"부식\", \"마묘\": \"마모\", \"과렬\": \"과열\", \"오작똥\": \"오작동\", \"재동\": \"제동\"}\n",
"ABBR = {\"T/M\": \"변속기\", \"BAT\": \"배터리\"}\n",
"PART_KW = sorted([(a, i) for i, p in enumerate(PARTS) for a in p[1]], key=lambda x: -len(x[0]))\n",
"DEF_KW = {\"균열\": [\"균열\", \"크랙\", \"갈라\"], \"누유\": [\"누유\", \"새어\", \"누설\"], \"마모\": [\"마모\", \"닳은\"], \"부식\": [\"부식\", \"녹이\", \"산화\"],\n",
" \"접촉불량\": [\"접촉 불량\", \"단선\", \"끊김\"], \"체결불량\": [\"볼트\", \"체결\", \"헐거\"], \"과열\": [\"과열\", \"온도\", \"열이\"], \"오작동\": [\"오작동\", \"불안정\", \"멈춤\"]}\n",
"SEV_KW = [(2, [\"즉시 운용 중지\", \"사용 불가\", \"위험\"]), (1, [\"성능 저하\", \"조속한\"]), (0, [\"경미\", \"지장이 없\"])]\n",
"CAUSE_KW = {\"제조 불량\": [\"신품\", \"출고\", \"인수\"], \"정비 품질\": [\"정비 후\", \"재조립\", \"정비 직후\"], \"운용 부주의\": [\"충격\", \"과부하\", \"점검 없이\"],\n",
" \"설계 취약\": [\"동일 증상\", \"구조적\", \"여러 부대\"], \"자연 노후\": [\"사용 연한\", \"노후\", \"수명\"]}\n",
"PARTICLES = (\"에서\", \"으로\", \"이\", \"가\", \"은\", \"는\", \"을\", \"를\", \"에\", \"의\", \"로\", \"와\", \"과\", \"도\", \"함\", \"음\", \"됨\")\n",
"\n",
"\n",
"def norm(t):\n",
" \"\"\"정규화: 전각->반각, 특수문자·중복 공백 제거, 약어 확장, 오타 교정\"\"\"\n",
" # [TODO : 학생 실습 10] 야전 장비 결함 보고(QDR) 데이터 분석 및 품질 취약점 식별 도구 통합 완성 관련 핵심 로직을 구현하세요.\n",
" # 힌트: 상단 주석 및 데이터 스키마/지침을 참고하여 작성합니다.\n",
" # <<< 여기에 코드를 작성하세요 >>>\n",
" pass # 작성 후 pass를 제거하세요.\n",
"def tok(t):\n",
" out = []\n",
" for w in re.split(r\"[\\s.,]+\", norm(t)):\n",
" if not w or re.fullmatch(r\"[\\d\\-A-Za-z]+\", w):\n",
" continue\n",
" for p in PARTICLES:\n",
" if len(w) > len(p) + 1 and w.endswith(p):\n",
" w = w[:-len(p)]\n",
" break\n",
" out.append(w)\n",
" return out\n",
"\n",
"\n",
"def label_part(t):\n",
" for a, i in PART_KW:\n",
" if a in t:\n",
" return i\n",
" return -1\n",
"\n",
"\n",
"def label_defect(t):\n",
" for d, ks in DEF_KW.items():\n",
" if any(k in t for k in ks):\n",
" return d\n",
" return \"미분류\"\n",
"\n",
"\n",
"def label_sev(t):\n",
" for s, ks in SEV_KW:\n",
" if any(k in t for k in ks):\n",
" return s\n",
" return -1\n",
"\n",
"\n",
"def label_cause(t):\n",
" for c, ks in CAUSE_KW.items():\n",
" if any(k in t for k in ks):\n",
" return c\n",
" return \"미상\"\n",
"\n",
"CONFIG = {\"retrain_tolerance\": 0.005, \"alarm_ratio\": 1.9, \"alarm_z\": 4.0, \"since_short\": 62, \"min_cases\": 60, \"mfg_share\": 0.22, \"design_share\": 0.20, \"vendor_share\": 0.28}\n",
"OPS_GUIDE = { # docs/qlt05_operations_guide.md 요약\n",
" \"1. 목적과 범위\": \"야전 QDR 서술·실적으로 다발 결함 부위와 품질 취약점을 식별하는 보조 도구. 하자 구상·설계 변경 소요 제기의 결정은 품질관.\",\n",
" \"2. 입력 데이터\": \"QDR(부대·장비·부위·서술·치명도), 정비 수행처와 경과일, 검사 대기·재검사. 가상 식별코드 사용, 실제 부대 자료는 부대 내에서만 처리.\",\n",
" \"3. 일일 운용 절차\": \"접수 -> 검증·격리 -> 정제·라벨링 -> 치명도 분류 -> 월별 집계·경보 -> 후보 제안(대기) -> 품질관 결정 -> 결과 기록.\",\n",
" \"4. 승인(HITL) 기준\": \"제조사 하자 구상·설계 변경·외주정비 시정은 품질관 승인 후 확정, 무응답은 조치 보류 후 분기 회의 재상정, 사유 기록.\",\n",
" \"5. 재학습 절차\": \"분기 1회 신규 QDR로 도전자 학습, 검증 정확도가 나빠지지 않을 때만 교체.\",\n",
" \"6. 장애 대응\": \"모델 오류 시 키워드 규칙으로 대체, 손상·비정상 서술은 수동 확인 목록으로 분리.\",\n",
" \"7. 연동 인터페이스\": \"국방 군수 정보체계와 QDR·정비 이력 표준 필드(장비코드·분류군·일시)를 CSV/API로 교환하는 안을 차기 과제로 둔다.\",\n",
"}\n",
"names = [p[0] for p in PARTS]\n",
"oh = lambda d: sp.csr_matrix(np.hstack([np.eye(NP)[d[\"part\"]], pd.get_dummies(d[\"defect\"]).reindex(columns=DNAMES, fill_value=0).values.astype(float)]))\n",
"\n",
"\n",
"def validate(d):\n",
" ok = d[\"text\"].str.len().between(5, 400) & ~d[\"text\"].str.contains(\"�\", regex=False)\n",
" return d[ok].copy(), d[~ok].copy()\n",
"\n",
"\n",
"def fit_sev(d):\n",
" vec = TfidfVectorizer(tokenizer=tok, lowercase=False, token_pattern=None, min_df=5)\n",
" X = sp.hstack([vec.fit_transform(d[\"text\"]), oh(d)]).tocsr()\n",
" return vec, LogisticRegression(C=5, max_iter=500).fit(X, d[\"sev\"])\n",
"\n",
"\n",
"def predict_sev(model, d):\n",
" vec, clf = model\n",
" return clf.predict(sp.hstack([vec.transform(d[\"text\"]), oh(d)]).tocsr())\n",
"\n",
"\n",
"def retrain(champion, old, new):\n",
" \"\"\"도전자(지난달+이번 달 앞 2/3) vs 챔피언(지난달) -> 이번 달 뒤 1/3 검증 정확도 비교, 비슷하거나 나을 때만 교체\"\"\"\n",
" cut = new[\"month\"].quantile(2 / 3)\n",
" hold, new_tr = new[new[\"month\"] > cut], new[new[\"month\"] <= cut]\n",
" challenger = fit_sev(pd.concat([old, new_tr], ignore_index=True))\n",
" a_old, a_new = (predict_sev(champion, hold) == hold[\"sev\"].values).mean(), (predict_sev(challenger, hold) == hold[\"sev\"].values).mean()\n",
" swap = a_new >= a_old - CONFIG[\"retrain_tolerance\"]\n",
" return (challenger if swap else champion), {\"챔피언\": round(float(a_old), 3), \"도전자\": round(float(a_new), 3), \"교체\": bool(swap)}\n",
"\n",
"\n",
"def sev_safe(model, d):\n",
" try:\n",
" return predict_sev(model, d), \"모델\"\n",
" except Exception:\n",
" return d[\"text\"].map(label_sev).replace(-1, 0).values, \"키워드 규칙 대체\"\n",
"\n",
"\n",
"def cause_rule(d):\n",
" c = d[\"text\"].map(norm).map(label_cause)\n",
" short = (c == \"미상\") & (d[\"vendor\"] == \"외주B\") & (d[\"since_maint\"] <= CONFIG[\"since_short\"])\n",
" return c.where(~short, \"정비 품질\")\n",
"\n",
"\n",
"# ---------------- 1. 통합 실행 ----------------\n",
"t_start = time.perf_counter()\n",
"old, _, _ = make_qdr(36, seed=11, scale=1.5) # 지난달 실적\n",
"new, _, _ = make_qdr(36, seed=1, scale=1.5) # 이번 달 실적\n",
"ops, use, events = make_qdr(24, seed=21, scale=1.5) # 다음 달 운용(신규 QDR)\n",
"ops.iloc[[10, 300], ops.columns.get_loc(\"text\")] = [\"\", \"�� 오류\"] # 손상 서술 2건 주입\n",
"champion = fit_sev(old)\n",
"model, rt = retrain(champion, old, new)\n",
"print(f\"[교육용가상자료] 재학습: {rt} -> {'도전자 교체' if rt['교체'] else '챔피언 유지'}\")\n",
"good, bad = validate(ops)\n",
"good = good.copy()\n",
"good[\"sev_pred\"], src = sev_safe(model, good)\n",
"good[\"cause_pred\"] = cause_rule(good)\n",
"sec = time.perf_counter() - t_start\n",
"acc = (good[\"sev_pred\"] == good[\"sev\"]).mean()\n",
"print(f\"운용 QDR {len(ops)}건: 분석 {len(good)}건, 수동 확인 {len(bad)}건, 치명도 출처 '{src}', 정확도 {acc:.1%}, 전체 실행 {sec:.1f}초\")\n",
"\n",
"# 월별 집계·경보·취약성 지수\n",
"M = good.pivot_table(index=\"part\", columns=\"month\", values=\"qdr_id\", aggfunc=\"size\", fill_value=0).reindex(index=range(NP), columns=range(24), fill_value=0)\n",
"rate = (M / use[:24]).T.rolling(12, min_periods=12).mean().shift(1).T\n",
"exp = rate * use[:24]\n",
"Z = (M - exp) / np.sqrt(exp.clip(lower=1))\n",
"A = (M / exp.clip(lower=1) > CONFIG[\"alarm_ratio\"]) & (Z > CONFIG[\"alarm_z\"])\n",
"W_SEV = np.array([1, 3, 9])\n",
"V = good.assign(v=W_SEV[good[\"sev_pred\"]]).groupby(\"part\")[\"v\"].sum()\n",
"recent = good[good[\"month\"] >= 21].assign(v=lambda d: W_SEV[d[\"sev_pred\"]]).groupby(\"part\")[\"v\"].sum().sort_values(ascending=False)\n",
"\n",
"# 후보 제안(승인 대기): 최근 12개월 근거\n",
"ev = good[good[\"month\"] >= 12]\n",
"cands = []\n",
"for p, g in ev.groupby(\"part\"):\n",
" mf, ds = (g[\"cause_pred\"] == \"제조 불량\").mean(), (g[\"cause_pred\"] == \"설계 취약\").mean()\n",
" if len(g) >= CONFIG[\"min_cases\"] and mf >= CONFIG[\"mfg_share\"]: cands.append((names[p], \"하자 구상\", len(g), mf))\n",
" if len(g) >= CONFIG[\"min_cases\"] and ds >= CONFIG[\"design_share\"]: cands.append((names[p], \"설계 변경\", len(g), ds))\n",
"for v, g in ev.groupby(\"vendor\"):\n",
" sh = (g[\"cause_pred\"] == \"정비 품질\").mean()\n",
" if v != \"부대정비\" and len(g) >= 100 and sh >= CONFIG[\"vendor_share\"]: cands.append((v, \"외주정비 시정\", len(g), sh))\n",
"C = pd.DataFrame(cands, columns=[\"대상\", \"유형\", \"근거 건수\", \"원인 비중\"])\n",
"C[\"status\"] = \"PENDING\" # 품질관 결정 전까지 확정하지 않는다\n",
"\n",
"# ---------------- 2. 자체 점검 ----------------\n",
"_, bad_src = sev_safe(None, good.head(3))\n",
"checks = {\n",
" \"QDR 유실 0건\": len(good) + len(bad) == len(ops),\n",
" \"손상 서술 2건 수동 확인 분리\": len(bad) == 2,\n",
" \"재학습 파이프라인 실행\": \"교체\" in rt,\n",
" \"치명도 정확도 80% 이상\": bool(acc >= 0.80),\n",
" \"모델 장애 시 키워드 규칙 대체\": bad_src == \"키워드 규칙 대체\",\n",
" \"경보 셀 모두 12개월 이후\": not bool(A.iloc[:, :12].values.any()),\n",
" \"후보 제안 1건 이상 생성\": len(C) >= 1,\n",
" \"자동 승인 0건(후보는 대기)\": bool((C[\"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",
"fig, ax = plt.subplots(1, 2, figsize=(11, 4))\n",
"ax[0].imshow((M / exp.clip(lower=1)).clip(upper=3).values, aspect=\"auto\", cmap=\"Reds\"); ax[0].set_title(\"QDR concentration (x expected)\"); ax[0].set_xlabel(\"month\"); ax[0].set_ylabel(\"part\")\n",
"ax[1].bar(range(NP), V.reindex(range(NP)).fillna(0).values); ax[1].set_title(\"Vulnerability index by part\"); ax[1].set_xlabel(\"part id\")\n",
"plt.tight_layout(); plt.show()\n",
"\n",
"fa = [(names[p], int(m)) for p, m in zip(*np.where(A.values))]\n",
"print(\"\\n[품질 담당 장교 브리핑 요약] [교육용가상자료]\")\n",
"print(f\"1. 운용 24개월 QDR {len(ops)}건 중 손상 {len(bad)}건 수동 확인, 치명도 Critical {int((good['sev_pred'] == 2).sum())}건({(good['sev_pred'] == 2).mean():.0%})\")\n",
"print(\"2. 최근 3개월 취약성 지수 상위 부위: \" + \", \".join(f\"{names[p]} {v:.0f}\" for p, v in recent.head(3).items()))\n",
"print(f\"3. 월별 급증 경보 {len(fa)}건\" + (\": \" + \", \".join(f\"{n}({m}월)\" for n, m in fa[:3]) if fa else \"\"))\n",
"print(f\"4. 후보 제안 {len(C)}건은 품질관 승인 대기(자동 승인 0건): \" + \", \".join(f\"{r['대상']} {r['유형']}\" for _, r in C.head(4).iterrows()))\n",
"vb = good[good[\"vendor\"] == \"외주B\"]\n",
"print(f\"5. 외주B 검사 대기 평균 {vb['inspect_wait'].mean():.1f}일(타 수행처 {good.loc[good['vendor'] != '외주B', 'inspect_wait'].mean():.1f}일), 재검사율 {vb['reinspect'].mean():.0%}\")\n",
"print(f\"6. 치명도 모델: {'신규 실적으로 교체' if rt['교체'] else '기존 유지'}(검증 정확도 {max(rt['챔피언'], rt['도전자']):.1%})\")\n",
"print(\"7. 건의: 외주정비 업체 시정 요구와 제조사 하자 구상 근거 정리, 국방 군수 정보체계와 QDR 표준 필드 연동 인터페이스 정의\")\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"<details>\n",
"<summary><b>💡 [참고] 미션 10 예상 실행 결과 (Output)</b></summary>\n",
"\n",
"```text\n",
"[교육용가상자료] 재학습: {'챔피언': 0.867, '도전자': 0.865, '교체': True} -> 도전자 교체\n",
"운용 QDR 8923건: 분석 8921건, 수동 확인 2건, 치명도 출처 '모델', 정확도 87.8%, 전체 실행 7.3초\n",
"\n",
"자체 점검 8/8 통과, 자동 재학습 파이프라인 정상 실행\n",
"운영 문서 7개 장 작성률 100% (목표 100%: 달성) - 목적과 범위, 입력 데이터, 일일 운용 절차, 승인(HITL) 기준, 재학습 절차, 장애 대응, 연동 인터페이스\n",
"\n",
"[품질 담당 장교 브리핑 요약] [교육용가상자료]\n",
"1. 운용 24개월 QDR 8923건 중 손상 2건 수동 확인, 치명도 Critical 717건(8%)\n",
"2. 최근 3개월 취약성 지수 상위 부위: 제동 장치 378, 조향 장치 268, 엔진 연료계통 244\n",
"3. 월별 급증 경보 3건: 조향 장치(20월), 냉각 계통(16월), 전기·배터리(18월)\n",
"4. 후보 제안 3건은 품질관 승인 대기(자동 승인 0건): 냉각 계통 하자 구상, 전기·배터리 하자 구상, 외주B 외주정비 시정\n",
"5. 외주B 검사 대기 평균 8.7일(타 수행처 4.5일), 재검사율 26%\n",
"6. 치명도 모델: 신규 실적으로 교체(검증 정확도 86.7%)\n",
"7. 건의: 외주정비 업체 시정 요구와 제조사 하자 구상 근거 정리, 국방 군수 정보체계와 QDR 표준 필드 연동 인터페이스 정의\n",
"```\n",
"</details>\n"
]
}
],
"metadata": {
"colab": {
"provenance": []
},
"kernelspec": {
"display_name": "Python 3",
"name": "python3"
},
"language_info": {
"name": "python"
}
},
"nbformat": 4,
"nbformat_minor": 0
}