060 DBSCAN 클러스터링 상세
키워드: DBSCAN, 밀도 기반
개요
DBSCAN(Density-Based Spatial Clustering of Applications with Noise)은 밀도 기반 클러스터링 알고리즘입니다. K를 미리 지정하지 않아도 되고, 이상치를 자동으로 탐지합니다.
실습 환경
- Python 버전: 3.11 권장
- 필요 패키지:
pycaret[full]>=3.0
DBSCAN 원리
핵심 개념
eps (epsilon): 이웃 반경
min_samples: 핵심점이 되기 위한 최소 이웃 수
● 핵심점 (Core Point)
- eps 반경 내에 min_samples 이상의 점이 있음
- 클러스터의 중심 역할
○ 경계점 (Border Point)
- 핵심점의 이웃이지만 자체는 핵심점이 아님
- 클러스터의 가장자리
✗ 노이즈 (Noise/Outlier)
- 어떤 핵심점의 이웃도 아님
- 클러스터 -1로 표시
알고리즘 과정
1. 임의의 점 선택
2. eps 반경 내 이웃 검색
3. min_samples 이상이면 → 핵심점 → 새 클러스터 시작
4. 핵심점의 이웃들 재귀적으로 클러스터에 추가
5. 모든 점이 방문될 때까지 반복
PyCaret에서 DBSCAN
from pycaret.clustering import *
from pycaret.datasets import get_data
data = get_data('jewellery')
clust = setup(data, normalize=True, session_id=42, verbose=False)
# 060 DBSCAN (eps, min_samples 자동 설정)
dbscan = create_model('dbscan')
# 060 클러스터 확인
clustered = assign_model(dbscan)
print("클러스터별 샘플 수:")
print(clustered['Cluster'].value_counts().sort_index())
eps와 min_samples 설정
K-거리 그래프로 eps 결정
from sklearn.neighbors import NearestNeighbors
import numpy as np
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
# 060 k번째 최근접 이웃까지의 거리 계산
k = 5 # min_samples와 같게 설정
neigh = NearestNeighbors(n_neighbors=k)
neigh.fit(X)
distances, indices = neigh.kneighbors(X)
# 060 k번째 이웃까지의 거리 정렬
k_distances = np.sort(distances[:, k-1])
plt.figure(figsize=(10, 6))
plt.plot(range(len(k_distances)), k_distances, 'b-')
plt.xlabel('Points sorted by distance')
plt.ylabel(f'{k}-th Nearest Neighbor Distance')
plt.title('K-Distance Graph for eps Selection')
plt.grid(True, alpha=0.3)
# 060 엘보우 포인트 표시
elbow_idx = 200 # 시각적으로 확인
plt.axhline(y=k_distances[elbow_idx], color='r', linestyle='--',
label=f'Suggested eps = {k_distances[elbow_idx]:.2f}')
plt.legend()
plt.savefig('dbscan_eps.png', dpi=150)
파라미터 그리드 서치
from sklearn.cluster import DBSCAN
from sklearn.metrics import silhouette_score
import pandas as pd
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
# 060 다양한 파라미터 조합 테스트
results = []
for eps in [0.3, 0.5, 0.7, 1.0, 1.5]:
for min_samples in [3, 5, 7, 10]:
dbscan = DBSCAN(eps=eps, min_samples=min_samples)
labels = dbscan.fit_predict(X)
n_clusters = len(set(labels)) - (1 if -1 in labels else 0)
n_noise = list(labels).count(-1)
if n_clusters > 1 and n_noise < len(X) * 0.5:
# 노이즈 제외하고 실루엣 점수 계산
mask = labels != -1
if mask.sum() > n_clusters:
score = silhouette_score(X[mask], labels[mask])
else:
score = -1
else:
score = -1
results.append({
'eps': eps,
'min_samples': min_samples,
'n_clusters': n_clusters,
'n_noise': n_noise,
'noise_ratio': n_noise / len(X),
'silhouette': score
})
df = pd.DataFrame(results)
df_valid = df[df['silhouette'] > 0].sort_values('silhouette', ascending=False)
print("DBSCAN 파라미터 검색 결과:")
print(df_valid.head(10))
비구형 클러스터 탐지
import numpy as np
import matplotlib.pyplot as plt
from sklearn.cluster import KMeans, DBSCAN
from sklearn.datasets import make_moons, make_circles
# 060 비구형 데이터 생성
X_moons, _ = make_moons(n_samples=300, noise=0.05, random_state=42)
X_circles, _ = make_circles(n_samples=300, noise=0.05, factor=0.5, random_state=42)
fig, axes = plt.subplots(2, 3, figsize=(15, 10))
for idx, (X, name) in enumerate([(X_moons, 'Moons'), (X_circles, 'Circles')]):
# 원본
axes[idx, 0].scatter(X[:, 0], X[:, 1], c='gray', alpha=0.7)
axes[idx, 0].set_title(f'{name} - Original')
# K-Means
kmeans = KMeans(n_clusters=2, random_state=42, n_init=10)
labels_km = kmeans.fit_predict(X)
axes[idx, 1].scatter(X[:, 0], X[:, 1], c=labels_km, cmap='viridis', alpha=0.7)
axes[idx, 1].set_title(f'{name} - K-Means (Poor)')
# DBSCAN
dbscan = DBSCAN(eps=0.2, min_samples=5)
labels_db = dbscan.fit_predict(X)
axes[idx, 2].scatter(X[:, 0], X[:, 1], c=labels_db, cmap='viridis', alpha=0.7)
axes[idx, 2].set_title(f'{name} - DBSCAN (Good)')
plt.tight_layout()
plt.savefig('dbscan_nonspherical.png', dpi=150)
이상치 탐지
from sklearn.cluster import DBSCAN
import numpy as np
import matplotlib.pyplot as plt
# 060 이상치가 있는 데이터
np.random.seed(42)
X_normal = np.random.randn(100, 2)
X_outliers = np.random.uniform(low=-5, high=5, size=(10, 2))
X = np.vstack([X_normal, X_outliers])
# 060 DBSCAN
dbscan = DBSCAN(eps=0.5, min_samples=5)
labels = dbscan.fit_predict(X)
# 060 시각화
plt.figure(figsize=(10, 6))
colors = ['blue' if l >= 0 else 'red' for l in labels]
plt.scatter(X[:, 0], X[:, 1], c=colors, alpha=0.7)
plt.scatter(X[labels == -1, 0], X[labels == -1, 1],
c='red', marker='x', s=100, label='Outliers')
plt.title(f'DBSCAN: {sum(labels == -1)} outliers detected')
plt.legend()
plt.grid(True, alpha=0.3)
plt.savefig('dbscan_outliers.png', dpi=150)
print(f"탐지된 이상치: {sum(labels == -1)}개")
클러스터 분석
from pycaret.clustering import *
from pycaret.datasets import get_data
from sklearn.cluster import DBSCAN
data = get_data('jewellery')
clust = setup(data, normalize=True, session_id=42, verbose=False)
X = get_config('X')
# 060 최적 파라미터로 DBSCAN
dbscan = DBSCAN(eps=1.0, min_samples=5)
labels = dbscan.fit_predict(X)
# 060 결과 분석
clustered = data.copy()
clustered['Cluster'] = labels
print("=== DBSCAN 클러스터 분석 ===\n")
# 060 클러스터별 통계
for cluster in sorted(clustered['Cluster'].unique()):
cluster_data = clustered[clustered['Cluster'] == cluster]
if cluster == -1:
print(f"노이즈 (n={len(cluster_data)}):")
else:
print(f"클러스터 {cluster} (n={len(cluster_data)}):")
print(cluster_data.describe().loc['mean'].round(2))
print()
HDBSCAN (계층적 DBSCAN)
eps 튜닝이 어려울 때:
# 060 hdbscan 패키지 필요: pip install hdbscan
try:
import hdbscan
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
# HDBSCAN (eps 자동)
clusterer = hdbscan.HDBSCAN(min_cluster_size=10, min_samples=5)
labels = clusterer.fit_predict(X)
n_clusters = len(set(labels)) - (1 if -1 in labels else 0)
n_noise = sum(labels == -1)
print(f"HDBSCAN 결과:")
print(f" 클러스터 수: {n_clusters}")
print(f" 노이즈: {n_noise}개 ({n_noise/len(X)*100:.1f}%)")
except ImportError:
print("hdbscan 패키지 설치 필요: pip install hdbscan")
DBSCAN vs K-Means vs 계층적
| 항목 | K-Means | 계층적 | DBSCAN |
|---|---|---|---|
| 클러스터 수 | 미리 지정 | 미리 지정 또는 후결정 | 자동 결정 |
| 클러스터 형태 | 구형 | 다양 | 밀도 기반 |
| 이상치 처리 | 못함 | 못함 | 자동 탐지 |
| 시간 복잡도 | O(nkt) | O(n²) | O(n log n) |
| 파라미터 | K | 연결 방법 | eps, min_samples |
장단점
장점:
- K를 미리 지정하지 않아도 됨
- 비구형 클러스터 탐지
- 이상치 자동 탐지
- 노이즈에 강건
단점:
- eps, min_samples 튜닝 필요
- 밀도가 다른 클러스터 처리 어려움
- 고차원에서 성능 저하
- 연결된 클러스터 분리 어려움
정리
- DBSCAN은 밀도 기반 클러스터링
- eps와 min_samples가 핵심 파라미터
- K-거리 그래프로 eps 결정
- 비구형 클러스터와 이상치 탐지에 효과적
- 밀도가 균일한 데이터에 적합
다음 글 예고
다음 글에서는 최적 클러스터 수 결정하기를 다룹니다.
PyCaret 머신러닝 마스터 시리즈 #060