Skip to main content

고객 세분화 프로젝트

비지도학습(Unsupervised Learning)을 활용하여 고객 데이터를 의미 있는 그룹으로 나누는 프로젝트입니다. K-Means와 DBSCAN을 비교하고, PCA로 시각화하여 각 군집의 특성을 해석합니다.

프로젝트 개요

항목내용
문제 유형비지도학습 (클러스터링)
데이터셋Mall Customer Segmentation (UCI 기반 가상 데이터)
핵심 기법스케일링, 차원 축소, 군집 수 결정
사용 알고리즘K-Means, DBSCAN, PCA
난이도중급

프로젝트 실습

1
데이터 준비 및 EDA
2
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns

# 고객 데이터 생성 (실무에서는 실제 데이터를 사용)
np.random.seed(42)
n = 500

df = pd.DataFrame({
    "age": np.concatenate([
        np.random.normal(25, 5, 150),
        np.random.normal(35, 8, 200),
        np.random.normal(55, 7, 150),
    ]).clip(18, 80).astype(int),
    "annual_income": np.concatenate([
        np.random.normal(30, 10, 150),
        np.random.normal(60, 15, 200),
        np.random.normal(90, 20, 150),
    ]).clip(10, 200).round(1),
    "spending_score": np.concatenate([
        np.random.normal(70, 15, 150),
        np.random.normal(50, 20, 200),
        np.random.normal(30, 12, 150),
    ]).clip(1, 100).astype(int),
    "visit_frequency": np.concatenate([
        np.random.poisson(8, 150),
        np.random.poisson(4, 200),
        np.random.poisson(2, 150),
    ]).clip(0, 20),
    "avg_purchase": np.concatenate([
        np.random.normal(15, 5, 150),
        np.random.normal(45, 15, 200),
        np.random.normal(80, 25, 150),
    ]).clip(5, 200).round(1),
})

print(f"데이터 크기: {df.shape}")
print(f"\n기술 통계:\n{df.describe()}")

# 변수 분포 확인
fig, axes = plt.subplots(1, 5, figsize=(20, 4))
for ax, col in zip(axes, df.columns):
    df[col].hist(bins=30, ax=ax, edgecolor="black")
    ax.set_title(col)
plt.tight_layout()
plt.show()

# 산점도 매트릭스
sns.pairplot(df, diag_kind="kde")
plt.suptitle("변수 간 산점도", y=1.02)
plt.show()
3
스케일링 및 차원 축소
4
from sklearn.preprocessing import StandardScaler
from sklearn.decomposition import PCA

# 스케일링 (클러스터링 전 필수)
scaler = StandardScaler()
X_scaled = scaler.fit_transform(df)

# PCA로 차원 축소 (시각화 + 노이즈 제거)
pca = PCA()
X_pca = pca.fit_transform(X_scaled)

# 설명 분산 비율 확인
plt.figure(figsize=(8, 4))
plt.bar(range(1, len(pca.explained_variance_ratio_) + 1),
        pca.explained_variance_ratio_, label="개별 설명 분산")
plt.step(range(1, len(pca.explained_variance_ratio_) + 1),
         np.cumsum(pca.explained_variance_ratio_), label="누적 설명 분산",
         where="mid", color="red")
plt.xlabel("주성분 번호")
plt.ylabel("설명 분산 비율")
plt.title("PCA 설명 분산 비율")
plt.legend()
plt.show()

# 2차원으로 투영 (시각화용)
X_pca_2d = X_pca[:, :2]
print(f"2차원 설명 분산: {pca.explained_variance_ratio_[:2].sum():.2%}")
5
최적 군집 수 결정
6
from sklearn.cluster import KMeans
from sklearn.metrics import silhouette_score

# 엘보우 방법 + 실루엣 점수
k_range = range(2, 11)
inertias = []
silhouette_scores = []

for k in k_range:
    kmeans = KMeans(n_clusters=k, random_state=42, n_init=10)
    kmeans.fit(X_scaled)
    inertias.append(kmeans.inertia_)
    silhouette_scores.append(silhouette_score(X_scaled, kmeans.labels_))

fig, axes = plt.subplots(1, 2, figsize=(14, 5))

# 엘보우 방법
axes[0].plot(k_range, inertias, "bo-")
axes[0].set_xlabel("군집 수 (k)")
axes[0].set_ylabel("Inertia")
axes[0].set_title("엘보우 방법")

# 실루엣 점수
axes[1].plot(k_range, silhouette_scores, "ro-")
axes[1].set_xlabel("군집 수 (k)")
axes[1].set_ylabel("실루엣 점수")
axes[1].set_title("실루엣 점수")

plt.tight_layout()
plt.show()

# 최적 k 확인
best_k = k_range[np.argmax(silhouette_scores)]
print(f"최적 군집 수: {best_k} (실루엣 점수: {max(silhouette_scores):.4f})")
7
K-Means 클러스터링
8
from sklearn.metrics import silhouette_samples

# 최적 k로 K-Means 학습
kmeans = KMeans(n_clusters=best_k, random_state=42, n_init=10)
labels_kmeans = kmeans.fit_predict(X_scaled)

# PCA 2D 시각화
plt.figure(figsize=(10, 7))
scatter = plt.scatter(X_pca_2d[:, 0], X_pca_2d[:, 1],
                      c=labels_kmeans, cmap="viridis", alpha=0.6, s=30)
plt.colorbar(scatter, label="군집")
plt.xlabel("PC1")
plt.ylabel("PC2")
plt.title(f"K-Means 클러스터링 (k={best_k})")
plt.show()

# 군집별 특성 분석
df["cluster_kmeans"] = labels_kmeans
cluster_summary = df.groupby("cluster_kmeans").agg(["mean", "std"]).round(2)
print("군집별 특성 요약:")
print(cluster_summary)

# 군집별 프로필 시각화 (레이더 차트 대신 히트맵)
cluster_means = df.groupby("cluster_kmeans").mean()
cluster_normalized = (cluster_means - cluster_means.min()) / (cluster_means.max() - cluster_means.min())

plt.figure(figsize=(10, 5))
sns.heatmap(cluster_normalized, annot=cluster_means.round(1).values,
            fmt="", cmap="YlOrRd", yticklabels=[f"군집 {i}" for i in range(best_k)])
plt.title("군집별 특성 프로필 (정규화 색상, 원본 값 표기)")
plt.tight_layout()
plt.show()
9
DBSCAN과 비교
10
from sklearn.cluster import DBSCAN

# DBSCAN 클러스터링 (밀도 기반)
dbscan = DBSCAN(eps=1.2, min_samples=10)
labels_dbscan = dbscan.fit_predict(X_scaled)

n_clusters = len(set(labels_dbscan)) - (1 if -1 in labels_dbscan else 0)
n_noise = (labels_dbscan == -1).sum()
print(f"DBSCAN 군집 수: {n_clusters}, 노이즈 포인트: {n_noise}")

# K-Means vs DBSCAN 비교
fig, axes = plt.subplots(1, 2, figsize=(16, 6))

axes[0].scatter(X_pca_2d[:, 0], X_pca_2d[:, 1],
                c=labels_kmeans, cmap="viridis", alpha=0.6, s=30)
axes[0].set_title(f"K-Means (k={best_k})")

axes[1].scatter(X_pca_2d[:, 0], X_pca_2d[:, 1],
                c=labels_dbscan, cmap="viridis", alpha=0.6, s=30)
axes[1].set_title(f"DBSCAN (군집: {n_clusters}, 노이즈: {n_noise})")

for ax in axes:
    ax.set_xlabel("PC1")
    ax.set_ylabel("PC2")

plt.tight_layout()
plt.show()
11
군집 해석 및 비즈니스 인사이트
12
# 각 군집에 의미 있는 이름 부여
cluster_names = {}
for c in range(best_k):
    group = df[df["cluster_kmeans"] == c]
    age_avg = group["age"].mean()
    income_avg = group["annual_income"].mean()
    spending_avg = group["spending_score"].mean()

    # 특성 기반 이름 부여 (예시)
    if spending_avg > 60 and income_avg < 50:
        name = "젊은 소비형"
    elif income_avg > 70 and spending_avg < 40:
        name = "고소득 절약형"
    elif income_avg > 50 and spending_avg > 50:
        name = "고소득 소비형"
    else:
        name = f"일반 고객 {c}"

    cluster_names[c] = name
    print(f"군집 {c} ({name}): 나이={age_avg:.0f}, "
          f"소득={income_avg:.0f}, 소비점수={spending_avg:.0f}")

# 비즈니스 전략 제안
print("\n--- 마케팅 전략 제안 ---")
for c, name in cluster_names.items():
    group = df[df["cluster_kmeans"] == c]
    print(f"\n[{name}] ({len(group)}명, {len(group)/len(df)*100:.1f}%)")
    print(f"  평균 방문: {group['visit_frequency'].mean():.1f}회")
    print(f"  평균 구매: ${group['avg_purchase'].mean():.1f}")
K-Means는 구형(spherical) 군집에 적합하고 군집 수를 미리 지정해야 합니다. DBSCAN은 임의 형태의 군집을 찾을 수 있고 노이즈를 자동으로 분리합니다. 데이터 분포가 복잡하거나 이상치가 많으면 DBSCAN이, 군집이 비교적 균일하면 K-Means가 유리합니다. 자세한 비교는 클러스터링을 참고하세요.
내부 지표로는 실루엣 점수(높을수록 좋음), Calinski-Harabasz Index, Davies-Bouldin Index를 사용합니다. 그러나 비즈니스 관점에서 “군집이 실제로 의미 있고 행동 가능한가”가 가장 중요한 평가 기준입니다.

체크리스트

  • 클러스터링 전 스케일링의 중요성을 이해하고 적용할 수 있다
  • 엘보우 방법과 실루엣 점수로 최적 군집 수를 결정할 수 있다
  • PCA로 고차원 군집 결과를 시각화할 수 있다
  • 군집 결과를 비즈니스 관점에서 해석할 수 있다

다음 문서