第11回 クラスタリング
はじめに
本シリーズ「Pythonで学ぶデータ分析実践」の第11回では、クラスタリングについて解説します。
前回のクラス分類は「教師あり学習」でしたが、クラスタリングは教師なし学習に分類されます。正解ラベルが存在しないデータに対して、類似した特徴を持つデータ同士をグループ化する手法です。顧客セグメンテーション、画像処理、異常検知など幅広い分野で活用されています。
本記事では、階層的クラスタリングと非階層的クラスタリング(k-means)の仕組みと実装、エルボー法やシルエット分析によるクラスタ数の決定方法を実践的に解説します。
シリーズ全体の構成(全12回)
| 回 | テーマ |
|---|---|
| 第1回 | データ分析とは |
| 第2回 | データの前処理 |
| 第3回 | データの把握 |
| 第4回 | 相関分析 |
| 第5回 | 統計的推定 |
| 第6回 | 統計的検定 |
| 第7回 | 分散分析 |
| 第8回 | 回帰分析 |
| 第9回 | 時系列データ分析 |
| 第10回 | クラス分類 |
| 第11回 | クラスタリング(本記事) |
| 第12回 | 次元削減 |
11-1. クラスタリングとは
クラスタリングは、データの類似性に着目して、対象データを複数のグループ(クラスタ)に分ける手法です。教師なし学習に分類され、正解ラベルなしでデータの構造を発見します。
クラス分類との違い
| 項目 | クラス分類(教師あり) | クラスタリング(教師なし) |
|---|---|---|
| 正解ラベル | 必要 | 不要 |
| 目的 | 既知のカテゴリに分類 | 未知のグループ構造を発見 |
| 例 | 迷惑メール判定 | 顧客セグメンテーション |
クラスタリングの種類
| 種類 | 説明 | 代表手法 |
|---|---|---|
| 階層的クラスタリング | 木構造でデータを階層的にグループ化 | 凝集型(AgglomerativeClustering) |
| 非階層的クラスタリング | 事前にクラスタ数を指定して分割 | k-means法 |
データ間の類似度(距離尺度)
クラスタリングでは「距離」を用いてデータ間の類似度を測定します。
| 距離尺度 | 説明 | 特徴 |
|---|---|---|
| ユークリッド距離 | 直線距離(最も一般的) | 小さいほど類似度が高い |
| マンハッタン距離 | 格子状の道に沿った距離 | 外れ値の影響を受けにくい |
| コサイン類似度 | ベクトル間の角度 | テキスト分析でよく使用(-1〜1) |
import numpy as np
from scipy.spatial.distance import euclidean, cityblock, cosine
a = np.array([1, 2, 3])
b = np.array([4, 5, 6])
print(f"ユークリッド距離: {euclidean(a, b):.4f}")
print(f"マンハッタン距離: {cityblock(a, b):.4f}")
print(f"コサイン距離: {cosine(a, b):.4f}")
print(f"コサイン類似度: {1 - cosine(a, b):.4f}")
クラスタ間の距離計算方法
| 方法 | 説明 | 特徴 |
|---|---|---|
| 最短距離法(Single) | 2クラスタ間で最も近い2点の距離 | チェーン状クラスタを形成しやすい |
| 最長距離法(Complete) | 2クラスタ間で最も遠い2点の距離 | コンパクトなクラスタを形成 |
| 群平均法(Average) | 全点間距離の平均 | 外れ値に比較的強い |
| ウォード法(Ward) | クラスタ内分散の増加を最小化 | バランスの良いクラスタを形成(最もよく使用) |
11-2. 階層的クラスタリング
階層的クラスタリングは、データ間の類似度に基づいて段階的にクラスタを統合(または分割)し、木構造(樹形図)を形成する手法です。
処理の流れ(凝集型)
1. 各データ点を独立したクラスタとして初期化
2. 最も距離が近い2つのクラスタを統合
3. 統合を繰り返し、全データが1つのクラスタになるまで継続
4. デンドログラムで結果を可視化
5. 適切な位置で切断してクラスタ数を決定
Pythonでの実装
import numpy as np
import pandas as pd
from sklearn.datasets import load_iris
from sklearn.cluster import AgglomerativeClustering
from sklearn.preprocessing import StandardScaler
from scipy.cluster.hierarchy import dendrogram, linkage
import matplotlib.pyplot as plt
import japanize_matplotlib
# Irisデータの準備
iris = load_iris()
X = pd.DataFrame(iris.data, columns=iris.feature_names)
y_true = iris.target
# スケーリング
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)
# 階層的クラスタリング
agg = AgglomerativeClustering(
n_clusters=3, # クラスタ数
metric='euclidean', # 距離尺度
linkage='ward' # クラスタ間距離の計算方法
)
labels_agg = agg.fit_predict(X_scaled)
print("=== 階層的クラスタリング結果 ===")
print(f"クラスタ数: 3")
print(f"各クラスタのデータ数: {np.bincount(labels_agg)}")
デンドログラムの可視化
# scipy.cluster.hierarchyを使用
linked = linkage(X_scaled, method='ward', metric='euclidean')
plt.figure(figsize=(14, 6))
dendrogram(linked,
truncate_mode='lastp', # 最後のp個のクラスタのみ表示
p=30,
leaf_rotation=90,
leaf_font_size=9,
color_threshold=7)
plt.xlabel("サンプル")
plt.ylabel("距離")
plt.title("デンドログラム(ウォード法)")
plt.axhline(y=7, color='red', linestyle='--', label='切断位置(クラスタ数=3)')
plt.legend()
plt.tight_layout()
plt.show()
リンク方法の比較
fig, axes = plt.subplots(2, 2, figsize=(14, 10))
methods = ['ward', 'complete', 'average', 'single']
for ax, method in zip(axes.ravel(), methods):
linked = linkage(X_scaled, method=method)
dendrogram(linked, ax=ax, truncate_mode='lastp', p=20,
leaf_rotation=90, leaf_font_size=8)
ax.set_title(f"リンク方法: {method}", fontsize=12)
ax.set_ylabel("距離")
plt.tight_layout()
plt.show()
11-3. 非階層的クラスタリング(k-means)
k-means法は、事前に指定したK個のクラスタにデータを分割する、最も代表的な非階層的クラスタリング手法です。
k-meansのアルゴリズム
1. K個のクラスタ中心(セントロイド)をランダムに配置
2. 各データ点を最も近いセントロイドのクラスタに割り当て
3. 各クラスタの重心を再計算し、新しいセントロイドとする
4. 割り当てが変化しなくなるまで2-3を繰り返す(収束)
k-means++法
初期セントロイドの選択を工夫した改良版です。最初のセントロイド選択後、次のセントロイドは既存のセントロイドから遠い点が選ばれやすくなります。これにより収束が早まり、精度も向上します(scikit-learnのデフォルト)。
Pythonでの実装
from sklearn.cluster import KMeans
# k-meansクラスタリング
kmeans = KMeans(
n_clusters=3, # クラスタ数
init='k-means++', # 初期化方法
n_init=10, # 異なる初期値での試行回数
max_iter=300, # 最大反復回数
random_state=42
)
labels_km = kmeans.fit_predict(X_scaled)
print("=== k-meansクラスタリング結果 ===")
print(f"クラスタ数: 3")
print(f"各クラスタのデータ数: {np.bincount(labels_km)}")
print(f"SSE(慣性): {kmeans.inertia_:.2f}")
# クラスタ中心
centers = pd.DataFrame(
scaler.inverse_transform(kmeans.cluster_centers_),
columns=iris.feature_names
)
print(f"\nクラスタ中心(元のスケール):")
print(centers.round(2))
クラスタリング結果の可視化
from sklearn.decomposition import PCA
# PCAで2次元に削減して可視化
pca = PCA(n_components=2)
X_pca = pca.fit_transform(X_scaled)
centers_pca = pca.transform(kmeans.cluster_centers_)
fig, axes = plt.subplots(1, 2, figsize=(14, 5))
# k-meansの結果
scatter = axes[0].scatter(X_pca[:, 0], X_pca[:, 1], c=labels_km,
cmap='Set2', s=50, alpha=0.7, edgecolors='black')
axes[0].scatter(centers_pca[:, 0], centers_pca[:, 1], c='red',
marker='X', s=200, edgecolors='black', linewidths=2, label='セントロイド')
axes[0].set_title("k-meansクラスタリング結果", fontsize=12)
axes[0].set_xlabel("第1主成分")
axes[0].set_ylabel("第2主成分")
axes[0].legend()
# 正解ラベルとの比較
axes[1].scatter(X_pca[:, 0], X_pca[:, 1], c=y_true,
cmap='Set2', s=50, alpha=0.7, edgecolors='black')
axes[1].set_title("正解ラベル(参考)", fontsize=12)
axes[1].set_xlabel("第1主成分")
axes[1].set_ylabel("第2主成分")
plt.tight_layout()
plt.show()
11-4. エルボー法
k-means法ではクラスタ数Kを事前に指定する必要がありますが、最適なKを客観的に決定するためにエルボー法が使われます。
エルボー法の仕組み
異なるクラスタ数でSSE(クラスタ内誤差平方和)を計算し、SSEの減少が急に鈍化する「肘(エルボー)」の点を最適なクラスタ数とします。
$$SSE = \sum_{k=1}^{K}\sum_{x_i \in C_k} ||x_i - \mu_k||^2$$
# エルボー法の実装
sse_list = []
k_range = range(1, 11)
for k in k_range:
km = KMeans(n_clusters=k, init='k-means++', n_init=10, random_state=42)
km.fit(X_scaled)
sse_list.append(km.inertia_)
# 可視化
plt.figure(figsize=(8, 5))
plt.plot(k_range, sse_list, 'o-', color='steelblue', markersize=8, linewidth=2)
plt.xlabel("クラスタ数 K", fontsize=12)
plt.ylabel("SSE(クラスタ内誤差平方和)", fontsize=12)
plt.title("エルボー法によるクラスタ数の決定", fontsize=14)
plt.axvline(3, color='red', linestyle='--', alpha=0.7, label='エルボーポイント (K=3)')
plt.legend(fontsize=11)
plt.grid(alpha=0.3)
plt.xticks(k_range)
plt.show()
print("K=3でSSEの減少が急に緩やかになる → 最適クラスタ数は3")
11-5. シルエット分析
シルエット分析は、各データ点がそのクラスタにどの程度適切に割り当てられているかを評価する指標です。
シルエットスコア
$$s(i) = \frac{b(i) - a(i)}{\max(a(i), b(i))}$$
- $a(i)$: データ点iと同じクラスタ内の他のデータ点との平均距離
- $b(i)$: データ点iと最も近い別クラスタのデータ点との平均距離
- $s(i)$の範囲: -1〜1
| シルエットスコア | 意味 |
|---|---|
| 1に近い | 適切なクラスタに割り当てられている |
| 0付近 | クラスタの境界にある |
| 負の値 | 誤ったクラスタに割り当てられている可能性 |
from sklearn.metrics import silhouette_score, silhouette_samples
# 各クラスタ数でのシルエットスコア
sil_scores = []
for k in range(2, 11):
km = KMeans(n_clusters=k, init='k-means++', n_init=10, random_state=42)
labels = km.fit_predict(X_scaled)
sil_scores.append(silhouette_score(X_scaled, labels))
plt.figure(figsize=(8, 5))
plt.plot(range(2, 11), sil_scores, 'o-', color='steelblue', markersize=8, linewidth=2)
plt.xlabel("クラスタ数 K", fontsize=12)
plt.ylabel("シルエットスコア(平均)", fontsize=12)
plt.title("シルエット分析によるクラスタ数の決定", fontsize=14)
plt.axvline(2, color='red', linestyle='--', alpha=0.7)
plt.grid(alpha=0.3)
plt.xticks(range(2, 11))
plt.show()
best_k = range(2, 11)[np.argmax(sil_scores)]
print(f"シルエットスコアが最大のクラスタ数: K={best_k} (スコア={max(sil_scores):.4f})")
シルエットプロット
from sklearn.metrics import silhouette_samples
import matplotlib.cm as cm
fig, axes = plt.subplots(1, 3, figsize=(16, 4))
for idx, k in enumerate([2, 3, 4]):
km = KMeans(n_clusters=k, random_state=42, n_init=10)
labels = km.fit_predict(X_scaled)
sil_vals = silhouette_samples(X_scaled, labels)
y_lower = 10
for i in range(k):
cluster_sil = np.sort(sil_vals[labels == i])
y_upper = y_lower + len(cluster_sil)
color = cm.Set2(i / k)
axes[idx].fill_betweenx(np.arange(y_lower, y_upper), 0, cluster_sil,
facecolor=color, alpha=0.7)
axes[idx].text(-0.05, y_lower + 0.5 * len(cluster_sil), str(i))
y_lower = y_upper + 10
avg_score = silhouette_score(X_scaled, labels)
axes[idx].axvline(avg_score, color='red', linestyle='--')
axes[idx].set_title(f"K={k} (平均={avg_score:.3f})", fontsize=11)
axes[idx].set_xlabel("シルエットスコア")
axes[idx].set_xlim(-0.2, 1)
plt.tight_layout()
plt.show()
補足:クラスタリングの実践的なポイント
クラスタリング手法の選択ガイド
データの規模は?
├── 小〜中(〜数千件) → 階層的クラスタリング(結果を可視化しやすい)
└── 大(数千件〜) → k-means(計算が高速)
クラスタの形状は?
├── 球状 → k-means
├── 不定形 → DBSCAN
└── 階層構造 → 階層的クラスタリング
前処理の重要性
クラスタリングは距離ベースの手法なので、スケーリングが必須です。
from sklearn.preprocessing import StandardScaler
# 必ずスケーリングしてからクラスタリング
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)
クラスタリング結果の活用
# クラスタラベルを元データに付与して分析
df_result = X.copy()
df_result['cluster'] = labels_km
# クラスタごとの特徴を確認
print("=== クラスタごとの平均値 ===")
print(df_result.groupby('cluster').mean().round(2))
# クラスタごとの可視化
import seaborn as sns
for col in iris.feature_names:
plt.figure(figsize=(8, 3))
sns.boxplot(x='cluster', y=col, data=df_result, palette='Set2')
plt.title(f"クラスタ別: {col}")
plt.show()
まとめ
| 項目 | ポイント |
|---|---|
| クラスタリングとは | 類似データをグループ化する教師なし学習 |
| 階層的クラスタリング | デンドログラムで可視化、クラスタ数を後決定可能 |
| k-means | 高速でスケーラブル、クラスタ数を事前指定 |
| エルボー法 | SSEの変化からクラスタ数を決定 |
| シルエット分析 | クラスタの品質を-1〜1で評価 |
チェックリスト
- データのスケーリングを行ったか
- 複数の手法で試したか
- エルボー法とシルエット分析でクラスタ数を検討したか
- クラスタリング結果を可視化して妥当性を確認したか
- 各クラスタの特徴を分析したか
シリーズ記事一覧
- 【第1回】Pythonで学ぶデータ分析実践 〜データ分析とは〜
- 【第2回】データの前処理(欠損値・外れ値・ダミー変数・データスケーリングなど)
- 【第3回】データの把握(基本統計量・ヒストグラム・箱ひげ図・散布図など)
- 【第4回】相関分析(共分散・相関係数・相関比・連関係数など)
- 【第5回】統計的推定(母集団と標本・点推定・区間推定・信頼区間)
- 【第6回】統計的検定(仮説検定・p値・t検定・カイ二乗検定など)
- 【第7回】分散分析(一元分散分析・二元分散分析・多重比較)
- 【第8回】回帰分析(単回帰・重回帰・一般化線形モデル・モデル評価)
- 【第9回】時系列データ分析(トレンド・季節性・ARIMA・SARIMAなど)
- 【第10回】クラス分類(決定木・ランダムフォレスト・ロジスティック回帰など)
- 【第11回】クラスタリング(k-means・階層クラスタリング・エルボー法など)
- 【第12回】次元削減(主成分分析・寄与率・可視化)
- 【発展編】分析精度の向上(特徴量エンジニアリング・ハイパーパラメータ調整・アンサンブル学習など)
- 【発展編】ニューラルネットワーク入門(パーセプトロン・誤差逆伝播法・TensorFlow・Kerasなど)
次回の第12回では、高次元データの情報を保ちながら次元を削減する次元削減について解説します。主成分分析(PCA)、寄与率、累積寄与率による次元数の決定、そして低次元空間での可視化方法を学びます。