의료 데이터는 정답(Label)이 없는 경우가 많습니다. "이 환자는 당뇨입니다"라는 진단명이 붙어있지 않아도, 수많은 환자들의 검진 데이터를 분석해 "비슷한 건강 상태를 가진 환자 그룹"을 찾아내야 할 때가 있죠. 이를 환자 세분화(Patient Segmentation)라고 합니다.
가상의 환자 데이터를 직접 만들고, 이를 분석하여 숨겨진 환자 군을 찾아내는 4가지 핵심 알고리즘을 실습해 봅니다.
1. 데이터 준비: 가상의 환자 데이터 만들기 (Data Generation)
먼저 실습에 사용할 데이터를 만들어 봅시다. BMI(체질량지수)와 혈당(Glucose) 수치를 가진 환자 300명의 데이터를 생성합니다. 현실성을 위해 3개의 그룹(정상, 주의, 위험)과 일부 이상치(Outlier)를 섞어서 만듭니다.
import pandas as pd
import numpy as np
import plotly.graph_objects as go
from sklearn.datasets import make_blobs
# 1. 가상 데이터 생성 (3개의 클러스터: 정상, 주의, 위험군)
# random_state를 고정하여 항상 같은 데이터가 나오게 함
X, y_true = make_blobs(n_samples=300, centers=3, cluster_std=4.0, random_state=42)
# 의료 데이터처럼 보이게 스케일링 (BMI: 18~35, 혈당: 80~200 범위로 조정)
X[:, 0] = X[:, 0] * 2 + 25 # BMI 분포로 변환
X[:, 1] = X[:, 1] * 10 + 120 # 혈당 분포로 변환
# 데이터프레임 생성
df_patient = pd.DataFrame(X, columns=['BMI', 'Glucose'])
# 2. 이상치(Outlier) 인위적 추가 (DBSCAN 테스트용)
# 아주 특이한 케이스 10명 추가 (BMI 40이상, 혈당 250이상 등)
outliers = np.array([
[45, 280], [15, 60], [42, 300], [16, 50], [50, 290]
])
# 이상치 데이터프레임 생성 후 합치기
df_outliers = pd.DataFrame(outliers, columns=['BMI', 'Glucose'])
df_patient = pd.concat([df_patient, df_outliers], ignore_index=True)
print(f"환자 데이터 생성 완료: {df_patient.shape}")
print(df_patient.head())
# 3. 데이터 분포 시각화 (Scatter Plot)
fig = go.Figure()
fig.add_trace(go.Scatter(
x=df_patient['BMI'], y=df_patient['Glucose'],
mode='markers', marker=dict(size=8, opacity=0.6),
name='Patient Data'
))
fig.update_layout(title='환자 BMI vs 혈당 분포 (Raw Data)', xaxis_title='BMI', yaxis_title='Glucose (mg/dL)')
fig.show()

2. K-Means Clustering: 가장 대표적인 그룹핑
가장 대중적인 알고리즘입니다. 데이터 공간에 K개의 중심점을 찍고 환자들을 배정합니다.
① 최적의 그룹 수(K) 찾기: Elbow Method
무턱대고 나누는 것이 아니라, Inertia(중심점과 거리의 합)가 급격히 줄어드는 '팔꿈치' 지점을 찾습니다.
from sklearn.cluster import KMeans
ks = range(1, 10)
inertias = list()
for k in ks:
# init='k-means++': 초기 중심점을 신중하게 잡아 성능을 높임
model = KMeans(n_clusters=k, init='k-means++', n_init=10, random_state=42)
model.fit(df_patient)
inertias.append(model.inertia_)
# Elbow 그래프
fig = go.Figure()
fig.add_trace(go.Scatter(x=list(ks), y=inertias, mode='lines+markers'))
fig.update_layout(title='Elbow Method: 최적의 K 찾기', xaxis_title='K (그룹 수)', yaxis_title='Inertia')
fig.show()

** Elbow Method: 최적의 그룹 수 결정
- 목적: "환자를 몇 개의 그룹으로 나누는 것이 가장 효율적인가?"를 결정하기 위함입니다.
- 해석: 그래프를 보면 K=3 지점에서 급격하던 기울기가 완만하게 꺾이는 '팔꿈치(Elbow)' 현상이 나타납니다.
- K=1 $\rightarrow$ 2: 오차가 급격히 줄어듦 (분류 효과 큼)
- K=2 $\rightarrow$ 3: 여전히 오차가 많이 줄어듦 (분류 효과 유의미)
- K=3 이후: 오차 감소폭이 미미함 (그룹을 더 늘려봐야 의미 없음)
- 결론: 수학적으로나 임상적으로나 환자를 3개 그룹(예: 정상/주의/위험)으로 나누는 것이 최적임을 시사합니다.
② 환자 분류 실행 (K=3 )
# K=3 (정상, 주의, 위험)으로 설정
model = KMeans(n_clusters=3, init='k-means++', n_init=10, random_state=42)
y_kmeans = model.fit_predict(df_patient)
# 결과 시각화
fig = go.Figure()
fig.add_trace(go.Scatter(
x=df_patient['BMI'], y=df_patient['Glucose'],
mode='markers', marker=dict(color=y_kmeans, showscale=True, size=8),
text=y_kmeans, name='Cluster'
))
fig.update_layout(title='K-Means 결과: 환자 3그룹 분류', xaxis_title='BMI', yaxis_title='Glucose')
fig.show()

** K-Means 결과: 환자 3그룹 분류
- 목적: 가장 일반적인 방법으로 환자 군을 분류합니다.
- 해석: 시각화 결과, 환자들이 혈당(Glucose, Y축) 수치에 따라 뚜렷하게 3개의 층(Layer)으로 나뉜 것을 볼 수 있습니다.
- 하단 그룹 (보라색): 혈당이 낮고 안정적인 '정상군'
- 중간 그룹 (주황색/분홍색): 혈당이 다소 높은 '전당뇨/주의군'
- 상단 그룹 (노란색): 혈당이 매우 높은 '당뇨/고위험군'
- 결론: BMI(X축)보다는 혈당(Y축)이 환자의 위험도를 가르는 주된 요인으로 작용하고 있음을 알 수 있습니다.
3. 계층적 군집화 (Hierarchical Clustering)
환자 한 명 한 명을 합쳐나가며 계층 구조를 만듭니다. 질병의 세부 아형(Subtype)을 파악할 때 유용합니다.
from sklearn.cluster import AgglomerativeClustering
# 병합 군집 (Agglomerative)
model = AgglomerativeClustering(n_clusters=3)
y_agg = model.fit_predict(df_patient)
# 시각화 (K-Means와 비교해보세요)
fig = go.Figure()
fig.add_trace(go.Scatter(
x=df_patient['BMI'], y=df_patient['Glucose'],
mode='markers', marker=dict(color=y_agg, colorscale='Viridis', size=8)
))
fig.update_layout(title='Hierarchical Clustering 결과', xaxis_title='BMI', yaxis_title='Glucose')
fig.show()

** Hierarchical Clustering 결과
- 목적: 계층적 구조를 고려했을 때도 K-Means와 같은 결과가 나오는지 검증합니다.
- 해석: K-Means 결과와 거의 동일한 패턴(3개의 층)을 보입니다.
- 결론: 이는 우리가 찾은 3개의 환자 그룹 구조가 특정 알고리즘에만 의존하는 우연한 결과가 아니라, 데이터 자체에 내재된 뚜렷한 특징임을 증명합니다. (분석 결과의 신뢰도 상승)
4. DBSCAN: 이상치(Outlier) 탐지 전문가
현실 데이터에는 우리가 인위적으로 넣은 이상치(Outlier)들이 섞여 있습니다. K-Means는 이들을 억지로 어딘가에 끼워 넣지만, DBSCAN은 "이건 이상하다"며 걸러냅니다.
- 의료 적용: 기기 오류 데이터나 희귀 질환 환자를 찾아낼 때 사용합니다.
from sklearn.cluster import DBSCAN
# eps: 반경 (데이터 간 거리), min_samples: 최소 환자 수
# 거리가 8 이내에 이웃이 5명은 있어야 '정상 그룹'으로 인정
model = DBSCAN(eps=8, min_samples=5)
y_dbscan = model.fit_predict(df_patient)
# y_dbscan 값이 -1인 데이터가 바로 '이상치(Noise)'입니다.
outlier_mask = y_dbscan == -1
fig = go.Figure()
# 정상 그룹 시각화
fig.add_trace(go.Scatter(
x=df_patient[~outlier_mask]['BMI'], y=df_patient[~outlier_mask]['Glucose'],
mode='markers', marker=dict(color=y_dbscan[~outlier_mask], size=8),
name='Normal Cluster'
))
# 이상치(Outlier) 강조 시각화 (빨간색 X 표시)
fig.add_trace(go.Scatter(
x=df_patient[outlier_mask]['BMI'], y=df_patient[outlier_mask]['Glucose'],
mode='markers', marker=dict(color='red', symbol='x', size=12),
name='Outlier (Noise)'
))
fig.update_layout(title='DBSCAN 결과: 이상치(빨간 X) 탐지', xaxis_title='BMI', yaxis_title='Glucose')
fig.show()

** DBSCAN 결과: 이상치(Outlier) 탐지
- 목적: 일반적인 패턴에서 벗어난 특이 케이스(Noise)를 찾아냅니다.
- 해석:
- 파란색 점 (Normal Cluster): 밀집된 정상 패턴을 보이는 대다수의 환자들입니다.
- 빨간색 X (Outlier): 그룹에서 동떨어진 특이 환자들입니다.
- 예: BMI는 낮은데 혈당이 비정상적으로 높은 환자, BMI가 극도로 높은 초고도비만 환자 등
- 결론: K-Means는 이런 특이한 점들도 억지로 그룹에 포함시키려 하지만, DBSCAN은 이를 '이상치'로 명확히 분리해냅니다. 이들은 데이터 오기입(Error)일 수도 있고, 집중 관리가 필요한 특수 환자일 수도 있으므로 별도 리스트로 추출해 검토해야 합니다.
5. Spectral Clustering: 복잡한 구조 해석
데이터가 둥글게 뭉쳐있지 않고, 길게 늘어지거나 복잡하게 얽혀 있을 때 사용합니다.
from sklearn.cluster import SpectralClustering
# 복잡한 기하학적 구조를 가정하고 군집화
model = SpectralClustering(n_clusters=3, affinity='nearest_neighbors', assign_labels='kmeans', random_state=42)
y_spec = model.fit_predict(df_patient)
# 시각화
fig = go.Figure()
fig.add_trace(go.Scatter(
x=df_patient['BMI'], y=df_patient['Glucose'],
mode='markers', marker=dict(color=y_spec, size=8)
))
fig.update_layout(title='Spectral Clustering 결과', xaxis_title='BMI', yaxis_title='Glucose')
fig.show()

** Spectral Clustering 결과
- 목적: 데이터 구조가 복잡할 때를 대비한 고급 기법입니다.
- 해석: 현재 데이터는 비교적 단순한 분포를 가지고 있어, K-Means나 Hierarchical Clustering과 유사한 3그룹 형태를 보입니다.
- 결론: 만약 환자 데이터가 도넛 모양이나 소용돌이처럼 복잡하게 얽혀 있었다면 K-Means는 실패했겠지만, Spectral Clustering은 그런 경우에도 잘 작동합니다. 이번 케이스에서는 기본 모델(K-Means)로도 충분함을 역설적으로 보여줍니다.
각 알고리즘의 성격
- K-Means(++): 가장 무난하게 환자를 3그룹(정상/주의/위험)으로 나눴습니다. 일반적인 건강 검진 분석에 적합합니다.
- DBSCAN: 우리가 억지로 심어놓은 이상치(Outlier)들을 정확하게 빨간색 X로 잡아냈습니다. 데이터 전처리 단계에서 노이즈를 제거할 때 필수입니다.
- Hierarchical: 환자 그룹이 어떻게 뭉치는지 과정을 보여주므로, 연구 목적으로 좋습니다.
- Spectral: 데이터 모양이 특이할 때 사용하는 고급 옵션입니다.
'Study Note > Machine Learning' 카테고리의 다른 글
| 정답 없는 환자 데이터에서 그룹 찾기: 군집화(Clustering) 알고리즘 완전 정복 (0) | 2025.12.29 |
|---|---|
| 의료 AI의 협진 시스템: Random Forest와 XGBoost 섞어 쓰기 (Blending) (0) | 2025.12.29 |
| 응급실 예측 2편: 로그의 마법과 5대 회귀 모델 총출동 (Linear부터 XGBoost까지) (0) | 2025.12.24 |
| 응급실 예측, 디테일의 차이: Metric 정의부터 타겟 분리(Target Splitting)까지 (0) | 2025.12.23 |
| 응급실은 언제 붐빌까? : 시계열 회귀, 로그(Log)의 마법, 그리고 4대 평가지표 (0) | 2025.12.23 |