040 분류 실전 - 질병 진단 예측
키워드: 의료, 진단
개요
질병 진단 예측은 의료 분야의 핵심 분류 문제입니다. 이 글에서는 당뇨병 진단을 예측하는 모델을 구축하며, 의료 AI의 특수성을 다룹니다.
실습 환경
- Python 버전: 3.11 권장
- 필요 패키지:
pycaret[full]>=3.0
의료 AI의 특수성
- Recall 중시: 환자를 놓치면 안 됨 (FN 최소화)
- 해석 가능성: 의료진에게 설명 가능해야 함
- 신뢰구간: 불확실성 전달 필요
- 규제 준수: 의료기기 인증 요구
프로젝트 개요
목표: 피마 인디언 당뇨병 진단 예측
데이터셋: 피마 인디언 여성의 의료 기록
- Pregnancies: 임신 횟수
- Glucose: 포도당 농도
- BloodPressure: 혈압
- SkinThickness: 피부 두께
- Insulin: 인슐린 수치
- BMI: 체질량 지수
- DiabetesPedigreeFunction: 당뇨병 가족력 함수
- Age: 나이
데이터 로드 및 탐색
from pycaret.classification import *
from pycaret.datasets import get_data
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
# 040 데이터 로드
data = get_data('diabetes')
print(f"데이터 크기: {data.shape}")
print(f"\n컬럼 정보:")
print(data.info())
print(f"\n기초 통계:")
print(data.describe())
print(f"\n타겟 분포:")
print(data['Class variable'].value_counts(normalize=True))
데이터 품질 분석
# 040 의료 데이터에서 0은 결측일 수 있음
zero_check = ['Glucose', 'BloodPressure', 'SkinThickness', 'Insulin', 'BMI']
print("0 값 비율 (결측 의심):")
for col in zero_check:
zero_pct = (data[col] == 0).sum() / len(data) * 100
print(f" {col}: {zero_pct:.1f}%")
# 0을 결측으로 처리
data_clean = data.copy()
for col in zero_check:
data_clean[col] = data_clean[col].replace(0, pd.NA)
print(f"\n결측치 현황:")
print(data_clean.isnull().sum())
탐색적 데이터 분석
# 040 타겟별 분포 비교
fig, axes = plt.subplots(2, 4, figsize=(16, 8))
axes = axes.ravel()
for idx, col in enumerate(data.columns[:-1]):
data.boxplot(column=col, by='Class variable', ax=axes[idx])
axes[idx].set_title(col)
axes[idx].set_xlabel('')
plt.suptitle('')
plt.tight_layout()
plt.savefig('diabetes_eda.png', dpi=150)
# 040 상관관계
plt.figure(figsize=(10, 8))
sns.heatmap(data.corr(), annot=True, cmap='coolwarm', center=0)
plt.title('Feature Correlation Matrix')
plt.tight_layout()
plt.savefig('diabetes_correlation.png', dpi=150)
PyCaret 설정
# 040 환경 설정
clf = setup(
data=data_clean,
target='Class variable',
# 결측치 중앙값 대체
numeric_imputation='median',
# 스케일링
normalize=True,
normalize_method='zscore',
# 불균형 처리 (환자 클래스가 적음)
fix_imbalance=True,
# 교차 검증
fold=10,
# 재현성
session_id=42,
verbose=False
)
모델 비교 (Recall 중시)
# 040 의료에서는 Recall이 중요 (환자를 놓치면 안 됨)
print("=== 모델 비교 (Recall 기준) ===")
best_models = compare_models(sort='Recall', n_select=5)
print("\n=== AUC 기준으로도 확인 ===")
best_auc = compare_models(sort='AUC', n_select=3)
해석 가능한 모델 우선
# 040 의료에서는 해석 가능성이 중요
print("\n=== 해석 가능한 모델 ===")
# 040 로지스틱 회귀
print("1. 로지스틱 회귀:")
lr = create_model('lr')
# 040 결정 트리
print("\n2. 결정 트리:")
dt = create_model('dt', max_depth=5)
로지스틱 회귀 해석
import numpy as np
# 040 계수 추출
feature_names = get_config('X_train').columns
coefficients = lr.coef_[0]
# 040 오즈비 계산
odds_ratio = np.exp(coefficients)
# 040 DataFrame으로 정리
interpretation = pd.DataFrame({
'Feature': feature_names,
'Coefficient': coefficients,
'Odds Ratio': odds_ratio
}).sort_values('Odds Ratio', ascending=False)
print("특성별 영향도 (오즈비):")
print(interpretation)
# 040 해석 예시:
# 040 Glucose의 OR=2.0이면, 포도당이 1 표준편차 증가할 때
# 040 당뇨병 확률이 2배 증가
결정 트리 규칙 추출
from sklearn.tree import export_text
# 040 결정 규칙 추출
tree_rules = export_text(dt, feature_names=list(feature_names))
print("\n결정 트리 규칙:")
print(tree_rules)
# 040 의료진이 이해할 수 있는 형태로 변환 가능
성능과 해석의 균형
# 040 성능 좋은 모델
print("\n=== XGBoost + SHAP 해석 ===")
xgb = create_model('xgboost')
# 040 SHAP으로 해석
interpret_model(xgb)
# 040 Feature Importance
plot_model(xgb, plot='feature', save=True)
임계값 최적화 (Recall 중시)
from sklearn.metrics import precision_recall_curve, confusion_matrix
# 040 예측 결과
predictions = predict_model(xgb)
y_true = predictions['Class variable']
y_score = predictions['prediction_score']
# 040 Precision-Recall 곡선
precision, recall, thresholds = precision_recall_curve(y_true, y_score)
# 040 Recall 90% 이상인 임계값 찾기
target_recall = 0.90
idx = np.argmin(np.abs(recall - target_recall))
optimal_threshold = thresholds[min(idx, len(thresholds)-1)]
print(f"Recall {target_recall*100}% 달성 임계값: {optimal_threshold:.4f}")
print(f"해당 임계값의 Precision: {precision[idx]:.4f}")
# 040 새 임계값으로 예측
custom_pred = (y_score >= optimal_threshold).astype(int)
print(f"\n혼동 행렬 (임계값={optimal_threshold:.4f}):")
print(confusion_matrix(y_true, custom_pred))
신뢰 구간 제공
# 040 확률 기반 리스크 레벨
def risk_level(probability):
if probability < 0.3:
return "Low Risk"
elif probability < 0.6:
return "Medium Risk"
elif probability < 0.8:
return "High Risk"
else:
return "Very High Risk"
predictions['risk_level'] = predictions['prediction_score'].apply(risk_level)
print("\n리스크 레벨별 분포:")
print(predictions['risk_level'].value_counts())
모델 검증
# 040 교차 검증으로 안정성 확인
print("\n=== 교차 검증 결과 ===")
results = pull()
print(f"Recall 평균: {results['Recall'].mean():.4f}")
print(f"Recall 표준편차: {results['Recall'].std():.4f}")
# 040 표준편차가 크면 불안정 → 더 많은 데이터 또는 단순한 모델 필요
최종 모델 저장
# 040 앙상블 (해석과 성능의 균형)
print("\n=== 최종 앙상블 ===")
final_ensemble = blend_models([lr, xgb])
# 040 최종화
final_model = finalize_model(final_ensemble)
# 040 저장
save_model(final_model, 'diabetes_diagnosis_model')
print("모델 저장 완료")
새 환자 예측
# 040 새 환자 데이터
new_patient = pd.DataFrame({
'Pregnancies': [2],
'Glucose': [150],
'BloodPressure': [85],
'SkinThickness': [29],
'Insulin': [100],
'BMI': [32.0],
'DiabetesPedigreeFunction': [0.5],
'Age': [45]
})
# 040 예측
model = load_model('diabetes_diagnosis_model')
prediction = predict_model(model, data=new_patient)
prob = prediction['prediction_score'].values[0]
risk = risk_level(prob)
print(f"\n=== 진단 결과 ===")
print(f"당뇨병 확률: {prob:.2%}")
print(f"리스크 레벨: {risk}")
print(f"\n권장 조치: {'정밀 검사 필요' if prob > 0.5 else '정기 검진 유지'}")
의료 AI 체크리스트
□ Recall 우선 (환자 놓치지 않기)
□ 해석 가능한 모델 사용
□ 불확실성/신뢰구간 제공
□ 편향 검토 (인종, 성별, 나이별)
□ 외부 검증 데이터로 테스트
□ 임상 전문가 검토
□ 규제 요구사항 확인
정리
- 의료 AI는 Recall (민감도) 중시
- 해석 가능성이 필수 (로지스틱, 결정 트리, SHAP)
- 임계값 조정으로 민감도-특이도 균형
- 확률과 리스크 레벨 함께 제공
- 편향과 공정성 검토 필요
다음 글 예고
다음 글에서는 회귀 문제의 이해를 다룹니다.
PyCaret 머신러닝 마스터 시리즈 #040