본문으로 건너뛰기

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