061 최적 클러스터 수 결정하기
키워드: 엘보우, 실루엣
개요
클러스터링에서 가장 어려운 문제 중 하나는 최적의 클러스터 수(K)를 결정하는 것입니다. 이 글에서는 다양한 방법을 사용하여 최적 K를 찾는 방법을 알아봅니다.
실습 환경
- Python 버전: 3.11 권장
- 필요 패키지:
pycaret[full]>=3.0
왜 최적 K가 중요한가?
K가 너무 작으면:
- 서로 다른 그룹이 하나로 합쳐짐
- 중요한 패턴을 놓칠 수 있음
K가 너무 크면:
- 같은 그룹이 불필요하게 분리됨
- 과적합과 유사한 문제
- 해석이 어려워짐
1. 엘보우 방법 (Elbow Method)
관성(Inertia) = 각 점에서 클러스터 중심까지 거리의 제곱합
from pycaret.clustering import *
from pycaret.datasets import get_data
from sklearn.cluster import KMeans
import matplotlib.pyplot as plt
data = get_data('jewellery')
clust = setup(data, normalize=True, session_id=42, verbose=False)
X = get_config('X').values
# 061 K별 관성 계산
inertias = []
K_range = range(1, 11)
for k in K_range:
kmeans = KMeans(n_clusters=k, random_state=42, n_init=10)
kmeans.fit(X)
inertias.append(kmeans.inertia_)
# 061 시각화
plt.figure(figsize=(10, 6))
plt.plot(K_range, inertias, 'bo-', linewidth=2, markersize=10)
plt.xlabel('Number of Clusters (K)', fontsize=12)
plt.ylabel('Inertia (Within-cluster sum of squares)', fontsize=12)
plt.title('Elbow Method', fontsize=14)
plt.grid(True, alpha=0.3)
plt.xticks(K_range)
# 061 엘보우 포인트 강조
plt.annotate('Elbow Point', xy=(4, inertias[3]),
xytext=(6, inertias[3] + 50),
arrowprops=dict(arrowstyle='->', color='red'),
fontsize=12, color='red')
plt.savefig('elbow_method.png', dpi=150)
엘보우 자동 탐지
from kneed import KneeLocator # pip install kneed
# 061 엘보우 포인트 자동 탐지
kneedle = KneeLocator(
list(K_range), inertias,
curve='convex', direction='decreasing'
)
print(f"자동 탐지된 엘보우: K = {kneedle.elbow}")
2. 실루엣 분석 (Silhouette Analysis)
실루엣 점수 = (b - a) / max(a, b)
- a: 같은 클러스터 내 평균 거리
- b: 가장 가까운 다른 클러스터와의 평균 거리
- 범위: -1 ~ 1 (1에 가까울수록 좋음)
from pycaret.clustering import *
from pycaret.datasets import get_data
from sklearn.cluster import KMeans
from sklearn.metrics import silhouette_score, silhouette_samples
import matplotlib.pyplot as plt
import numpy as np
data = get_data('jewellery')
clust = setup(data, normalize=True, session_id=42, verbose=False)
X = get_config('X').values
# 061 K별 실루엣 점수
scores = []
K_range = range(2, 11)
for k in K_range:
kmeans = KMeans(n_clusters=k, random_state=42, n_init=10)
labels = kmeans.fit_predict(X)
score = silhouette_score(X, labels)
scores.append(score)
# 061 시각화
plt.figure(figsize=(10, 6))
plt.bar(K_range, scores, color='steelblue', edgecolor='black')
plt.xlabel('Number of Clusters (K)', fontsize=12)
plt.ylabel('Silhouette Score', fontsize=12)
plt.title('Silhouette Analysis', fontsize=14)
plt.xticks(K_range)
plt.grid(True, alpha=0.3, axis='y')
# 061 최적 K 표시
best_k = K_range[np.argmax(scores)]
plt.axvline(x=best_k, color='red', linestyle='--', label=f'Best K = {best_k}')
plt.legend()
plt.savefig('silhouette_analysis.png', dpi=150)
print(f"최적 K: {best_k} (실루엣 점수: {max(scores):.4f})")
실루엣 다이어그램
from sklearn.cluster import KMeans
from sklearn.metrics import silhouette_samples
import matplotlib.pyplot as plt
import matplotlib.cm as cm
import numpy as np
from pycaret.clustering import *
from pycaret.datasets import get_data
data = get_data('jewellery')
clust = setup(data, normalize=True, session_id=42, verbose=False)
X = get_config('X').values
fig, axes = plt.subplots(2, 2, figsize=(12, 10))
K_values = [2, 3, 4, 5]
for idx, k in enumerate(K_values):
ax = axes[idx // 2, idx % 2]
kmeans = KMeans(n_clusters=k, random_state=42, n_init=10)
labels = kmeans.fit_predict(X)
silhouette_avg = silhouette_score(X, labels)
sample_silhouette_values = silhouette_samples(X, labels)
y_lower = 10
for i in range(k):
cluster_silhouette_values = sample_silhouette_values[labels == i]
cluster_silhouette_values.sort()
size_cluster_i = cluster_silhouette_values.shape[0]
y_upper = y_lower + size_cluster_i
color = cm.nipy_spectral(float(i) / k)
ax.fill_betweenx(np.arange(y_lower, y_upper),
0, cluster_silhouette_values,
facecolor=color, edgecolor=color, alpha=0.7)
ax.text(-0.05, y_lower + 0.5 * size_cluster_i, str(i))
y_lower = y_upper + 10
ax.axvline(x=silhouette_avg, color='red', linestyle='--')
ax.set_title(f'K={k}, Avg Score={silhouette_avg:.3f}')
ax.set_xlabel('Silhouette Coefficient')
ax.set_ylabel('Cluster Label')
plt.tight_layout()
plt.savefig('silhouette_diagram.png', dpi=150)
3. 갭 통계량 (Gap Statistic)
실제 데이터와 균일 분포 랜덤 데이터의 클러스터링 품질 차이 비교
from sklearn.cluster import KMeans
import numpy as np
from pycaret.clustering import *
from pycaret.datasets import get_data
data = get_data('jewellery')
clust = setup(data, normalize=True, session_id=42, verbose=False)
X = get_config('X').values
def compute_gap_statistic(X, k_range, n_references=10):
"""갭 통계량 계산"""
gaps = []
s_k = []
for k in k_range:
# 실제 데이터 클러스터링
kmeans = KMeans(n_clusters=k, random_state=42, n_init=10)
kmeans.fit(X)
W_k = kmeans.inertia_
# 랜덤 데이터 클러스터링
W_kb = []
for _ in range(n_references):
# 균일 분포 랜덤 데이터
random_data = np.random.uniform(
low=X.min(axis=0),
high=X.max(axis=0),
size=X.shape
)
kmeans_random = KMeans(n_clusters=k, random_state=42, n_init=10)
kmeans_random.fit(random_data)
W_kb.append(kmeans_random.inertia_)
# 갭 계산
gap = np.mean(np.log(W_kb)) - np.log(W_k)
gaps.append(gap)
s_k.append(np.std(np.log(W_kb)) * np.sqrt(1 + 1/n_references))
return gaps, s_k
K_range = range(1, 11)
gaps, s_k = compute_gap_statistic(X, K_range)
# 061 최적 K: Gap(k) >= Gap(k+1) - s(k+1)인 가장 작은 k
optimal_k = 1
for k in range(len(gaps) - 1):
if gaps[k] >= gaps[k + 1] - s_k[k + 1]:
optimal_k = k + 1
break
print(f"갭 통계량 기반 최적 K: {optimal_k}")
# 061 시각화
import matplotlib.pyplot as plt
plt.figure(figsize=(10, 6))
plt.errorbar(K_range, gaps, yerr=s_k, fmt='o-', capsize=5)
plt.xlabel('Number of Clusters (K)')
plt.ylabel('Gap Statistic')
plt.title('Gap Statistic Method')
plt.axvline(x=optimal_k, color='red', linestyle='--', label=f'Optimal K = {optimal_k}')
plt.legend()
plt.grid(True, alpha=0.3)
plt.savefig('gap_statistic.png', dpi=150)
4. 칼린스키-하라바스 지수 (Calinski-Harabasz Index)
클러스터 간 분산 / 클러스터 내 분산 비율 (높을수록 좋음)
from sklearn.cluster import KMeans
from sklearn.metrics import calinski_harabasz_score
import matplotlib.pyplot as plt
from pycaret.clustering import *
from pycaret.datasets import get_data
data = get_data('jewellery')
clust = setup(data, normalize=True, session_id=42, verbose=False)
X = get_config('X').values
scores = []
K_range = range(2, 11)
for k in K_range:
kmeans = KMeans(n_clusters=k, random_state=42, n_init=10)
labels = kmeans.fit_predict(X)
score = calinski_harabasz_score(X, labels)
scores.append(score)
plt.figure(figsize=(10, 6))
plt.plot(K_range, scores, 'go-', linewidth=2, markersize=10)
plt.xlabel('Number of Clusters (K)')
plt.ylabel('Calinski-Harabasz Index')
plt.title('Calinski-Harabasz Index')
plt.xticks(K_range)
plt.grid(True, alpha=0.3)
best_k = K_range[np.argmax(scores)]
plt.axvline(x=best_k, color='red', linestyle='--', label=f'Best K = {best_k}')
plt.legend()
plt.savefig('calinski_harabasz.png', dpi=150)
5. 데이비스-볼딘 지수 (Davies-Bouldin Index)
클러스터 내 분산과 클러스터 간 거리 비율 (낮을수록 좋음)
from sklearn.cluster import KMeans
from sklearn.metrics import davies_bouldin_score
import matplotlib.pyplot as plt
import numpy as np
from pycaret.clustering import *
from pycaret.datasets import get_data
data = get_data('jewellery')
clust = setup(data, normalize=True, session_id=42, verbose=False)
X = get_config('X').values
scores = []
K_range = range(2, 11)
for k in K_range:
kmeans = KMeans(n_clusters=k, random_state=42, n_init=10)
labels = kmeans.fit_predict(X)
score = davies_bouldin_score(X, labels)
scores.append(score)
plt.figure(figsize=(10, 6))
plt.plot(K_range, scores, 'mo-', linewidth=2, markersize=10)
plt.xlabel('Number of Clusters (K)')
plt.ylabel('Davies-Bouldin Index (lower is better)')
plt.title('Davies-Bouldin Index')
plt.xticks(K_range)
plt.grid(True, alpha=0.3)
best_k = K_range[np.argmin(scores)]
plt.axvline(x=best_k, color='red', linestyle='--', label=f'Best K = {best_k}')
plt.legend()
plt.savefig('davies_bouldin.png', dpi=150)
종합 분석
from sklearn.cluster import KMeans
from sklearn.metrics import silhouette_score, calinski_harabasz_score, davies_bouldin_score
import pandas as pd
import numpy as np
from pycaret.clustering import *
from pycaret.datasets import get_data
data = get_data('jewellery')
clust = setup(data, normalize=True, session_id=42, verbose=False)
X = get_config('X').values
# 061 모든 지표 계산
results = []
K_range = range(2, 11)
for k in K_range:
kmeans = KMeans(n_clusters=k, random_state=42, n_init=10)
labels = kmeans.fit_predict(X)
results.append({
'K': k,
'Inertia': kmeans.inertia_,
'Silhouette': silhouette_score(X, labels),
'Calinski-Harabasz': calinski_harabasz_score(X, labels),
'Davies-Bouldin': davies_bouldin_score(X, labels)
})
df = pd.DataFrame(results)
print("=== 최적 K 종합 분석 ===\n")
print(df.to_string(index=False))
# 061 각 지표별 최적 K
print("\n지표별 최적 K:")
print(f" 실루엣 (최대): K = {df.loc[df['Silhouette'].idxmax(), 'K']}")
print(f" 칼린스키-하라바스 (최대): K = {df.loc[df['Calinski-Harabasz'].idxmax(), 'K']}")
print(f" 데이비스-볼딘 (최소): K = {df.loc[df['Davies-Bouldin'].idxmin(), 'K']}")
PyCaret 자동 분석
from pycaret.clustering import *
from pycaret.datasets import get_data
data = get_data('jewellery')
clust = setup(data, normalize=True, session_id=42, verbose=False)
# 061 K-Means 모델 생성 후 엘보우 플롯
kmeans = create_model('kmeans', num_clusters=4)
# 061 엘보우 플롯 (자동)
plot_model(kmeans, plot='elbow')
# 061 실루엣 플롯 (자동)
plot_model(kmeans, plot='silhouette')
방법별 특징 비교
| 방법 | 장점 | 단점 | 적합한 상황 |
|---|---|---|---|
| 엘보우 | 직관적, 계산 빠름 | 주관적 해석 | 빠른 탐색 |
| 실루엣 | 해석 명확, 클러스터별 분석 | K=2일 때 편향 | 클러스터 품질 평가 |
| 갭 통계량 | 통계적 근거 | 계산 비용 높음 | 엄밀한 분석 |
| 칼린스키-하라바스 | 빠른 계산 | 볼록 클러스터 가정 | 빠른 비교 |
| 데이비스-볼딘 | 클러스터 형태 고려 | 볼록 클러스터 가정 | 보조 지표 |
정리
- 단일 방법보다 여러 방법 종합 권장
- 엘보우와 실루엣이 가장 많이 사용됨
- 도메인 지식도 K 결정에 반영
- 비즈니스 해석 가능성도 고려
다음 글 예고
다음 글에서는 클러스터링 평가와 시각화를 다룹니다.
PyCaret 머신러닝 마스터 시리즈 #061