본문으로 건너뛰기

040 분류 실전 - 질병 진단 예측

키워드: 의료, 진단

개요

질병 진단 예측은 의료 분야의 핵심 분류 문제입니다. 이 글에서는 당뇨병 진단을 예측하는 모델을 구축하며, 의료 AI의 특수성을 다룹니다.

실습 환경

  • Python 버전: 3.11 권장
  • 필요 패키지: pycaret[full]>=3.0

의료 AI의 특수성

  1. Recall 중시: 환자를 놓치면 안 됨 (FN 최소화)
  2. 해석 가능성: 의료진에게 설명 가능해야 함
  3. 신뢰구간: 불확실성 전달 필요
  4. 규제 준수: 의료기기 인증 요구

프로젝트 개요

목표: 피마 인디언 당뇨병 진단 예측

데이터셋: 피마 인디언 여성의 의료 기록

  • 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