概要
本記事では、UCI Wine(ワイン成分)データセットを題材に、教師なし学習(クラスタリング)を実務寄りの手順で一気通貫に実装します。
具体的には、データ読み込みから 欠損値・外れ値の確認/除去、標準化、最適クラスタ数の検討(エルボー法・シルエット分析) を行い、複数のクラスタリング手法を比較します。
扱う手法は K-means/DBSCAN/階層クラスタリング の3つです。
さらに、PCA・t-SNEによる2次元可視化を通じてクラスタの分離や構造を直感的に確認し、最後に シルエットスコア(内部評価) と ARI・AMI(外部評価:真のラベルとの比較) を用いて、結果を定量的に評価します。
最終的には、分析結果と学習済みモデルを pkl / json / csv として保存し、「再現性のあるクラスタリング分析パイプライン」として再利用できる形にまとめることをゴールとします。
この記事でわかること
- ワイン成分データセット(UCI Wine)を用いた クラスタリング(教師なし学習)の実装手順
- 欠損値処理・外れ値確認・標準化などの 前処理ワークフロー
- K-means・DBSCAN・階層クラスタリング を用いたクラスタリング手法の比較
- エルボー法・シルエット分析・デンドログラム を用いた最適クラスタ数の決定
- 評価指標(シルエットスコア・ARI・AMI) を用いた性能評価と解釈
クラスタリング:ワイン成分データセット
本記事では教師なし学習のクラスタリングのフルパイプラインを構築します。
具体的には、データの前処理、最適クラスタ数の決定、複数のクラスタリング手法の比較、
クラスタの可視化と解釈までを一気通貫で実装します。
可視化やログ出力を組み込み、再現性と解釈性の高い分析ワークフローを再現します。
使用するデータは UCI Machine Learning Repository(カリフォルニア大学アーバイン校) に収録されている公開データセットです。対象は、イタリアの同一地域で栽培された 3種類の品種のワイン であり、それぞれに含まれる 13種類の化学成分 の量が測定されています。
データセットの概要
| Idx | 属性名 | 説明 |
|---|---|---|
| 0 | Alcohol | アルコール |
| 1 | Malic_Acid | リンゴ酸 |
| 2 | Ash | 灰分 |
| 3 | Ash_Alcanity | 灰分のアルカリ度 |
| 4 | Magnesium | マグネシウム |
| 5 | Total_Phenols | 総フェノール量 |
| 6 | Flavanoids | フラボノイド |
| 7 | Nonflavanoid_Phenols | 非フラボノイド・フェノール |
| 8 | Proanthocyanins | プロアントシアニン |
| 9 | Color_Intensity | 色彩強度 |
| 10 | Hue | 色相 |
| 11 | OD280 | 280nm吸光度(希釈ワインの透過率) |
| 12 | Proline | プロリン |
このデータセットには、合計で178個のインスタンス(ワインのサンプル)が含まれており、それぞれが13の化学成分の量を示す属性を持っています。
元のクラス分布:
- クラス0: 59個
- クラス1: 71個
- クラス2: 48個
本記事で実施するクラスタリング分析は、以下のステップに沿って実装します。
- データ読み込み・基本情報確認
- 欠損値処理・外れ値除去
- データの標準化(StandardScaler)
- 最適クラスタ数の決定(エルボー法・シルエット分析)
- クラスタリング手法の比較(K-means・DBSCAN・階層クラスタリング)
- クラスタの可視化(PCA・t-SNE)
- クラスタの解釈と特徴分析
- 評価指標による性能比較
- 結果の保存
実行環境:JupyterLab(Python 3.x / scikit-learn)
1. データセットのロードと確認
import warnings
warnings.filterwarnings("ignore")
import os
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
from sklearn.datasets import load_wine
# ワインデータセットの読み込み
wine_data = load_wine()
df = pd.DataFrame(wine_data.data, columns=wine_data.feature_names)
# 元のクラスラベルを参考用に保持(クラスタリングでは使用しない)
true_labels = wine_data.target
# 基本ログ
print("[DATASET] Wine Dataset from sklearn")
print("[SHAPE]", df.shape)
print("[COLUMNS]", list(df.columns))
print("[TRUE LABELS DISTRIBUTION]")
unique, counts = np.unique(true_labels, return_counts=True)
for label, count in zip(unique, counts):
print(f" Class {label}: {count} samples")
# 基本統計量
print("\n[BASIC STATISTICS]")
print(df.describe().T)
[DATASET] Wine Dataset from sklearn
[SHAPE] (178, 13)
[COLUMNS] ['alcohol', 'malic_acid', 'ash', 'alcalinity_of_ash', 'magnesium', 'total_phenols', 'flavanoids', 'nonflavanoid_phenols', 'proanthocyanins', 'color_intensity', 'hue', 'od280/od315_of_diluted_wines', 'proline']
[TRUE LABELS DISTRIBUTION]
Class 0: 59 samples
Class 1: 71 samples
Class 2: 48 samples
[BASIC STATISTICS]
count mean std min 25% \
alcohol 178.0 13.000618 0.811827 11.03 12.3625
malic_acid 178.0 2.336348 1.117146 0.74 1.6025
ash 178.0 2.366517 0.274344 1.36 2.2100
alcalinity_of_ash 178.0 19.494944 3.339564 10.60 17.2000
magnesium 178.0 99.741573 14.282484 70.00 88.0000
total_phenols 178.0 2.295112 0.625851 0.98 1.7425
flavanoids 178.0 2.029270 0.998859 0.34 1.2050
nonflavanoid_phenols 178.0 0.361854 0.124453 0.13 0.2700
proanthocyanins 178.0 1.590899 0.572359 0.41 1.2500
color_intensity 178.0 5.058090 2.318286 1.28 3.2200
hue 178.0 0.957449 0.228572 0.48 0.7825
od280/od315_of_diluted_wines 178.0 2.611685 0.709990 1.27 1.9375
proline 178.0 746.893258 314.907474 278.00 500.5000
50% 75% max
alcohol 13.050 13.6775 14.83
malic_acid 1.865 3.0825 5.80
ash 2.360 2.5575 3.23
alcalinity_of_ash 19.500 21.5000 30.00
magnesium 98.000 107.0000 162.00
total_phenols 2.355 2.8000 3.88
flavanoids 2.135 2.8750 5.08
nonflavanoid_phenols 0.340 0.4375 0.66
proanthocyanins 1.555 1.9500 3.58
color_intensity 4.690 6.2000 13.00
hue 0.965 1.1200 1.71
od280/od315_of_diluted_wines 2.780 3.1700 4.00
proline 673.500 985.0000 1680.00
ワインデータセットは欠損のない数値特徴量のみで構成されており、教師なし学習に適したデータ構造を持っています。
一方で、proline や color_intensity のようにスケールや分散が大きく異なる特徴量が含まれているため、距離ベースのクラスタリング手法を適用する際には 標準化が必須であると考えられます。
また、基本統計量の結果からは、データが自然に 3つのクラスタに分離される可能性が示唆されます。そのため、最適なクラスタ数を決定する手法として、エルボー法やシルエット分析を用いることとします。
2. 欠損値処理と外れ値除去
# 欠損値確認
print("[MISSING VALUES]")
print(df.isnull().sum())
# 外れ値除去(3σ法)
print("\n[OUTLIER REMOVAL (3σ)]")
print("Shape before outlier removal:", df.shape)
# 各特徴量のZ-scoreを計算
from scipy import stats
z_scores = np.abs(stats.zscore(df))
# 3σを超える外れ値を含む行を特定
outlier_mask = (z_scores > 3).any(axis=1)
outliers_removed = outlier_mask.sum()
print(f"Outliers detected: {outliers_removed} samples")
# 外れ値を除去
df_clean = df[~outlier_mask].copy()
true_labels_clean = true_labels[~outlier_mask]
print("Shape after outlier removal:", df_clean.shape)
[MISSING VALUES]
alcohol 0
malic_acid 0
ash 0
alcalinity_of_ash 0
magnesium 0
total_phenols 0
flavanoids 0
nonflavanoid_phenols 0
proanthocyanins 0
color_intensity 0
hue 0
od280/od315_of_diluted_wines 0
proline 0
dtype: int64
[OUTLIER REMOVAL (3σ)]
Shape before outlier removal: (178, 13)
Outliers detected: 10 samples
Shape after outlier removal: (168, 13)
3. データの標準化
from sklearn.preprocessing import StandardScaler
# 標準化
scaler = StandardScaler()
X_scaled = scaler.fit_transform(df_clean)
print("[STANDARDIZATION]")
print("Original data shape:", df_clean.shape)
print("Scaled data shape:", X_scaled.shape)
print("Mean after scaling:", np.mean(X_scaled, axis=0).round(4))
print("Std after scaling:", np.std(X_scaled, axis=0).round(4))
# 標準化後のデータをDataFrameに変換(可視化用)
df_scaled = pd.DataFrame(X_scaled, columns=df_clean.columns)
[STANDARDIZATION]
Original data shape: (168, 13)
Scaled data shape: (168, 13)
Mean after scaling: [-0. -0. 0. -0. 0. -0. 0. 0. -0. 0. -0. -0. 0.]
Std after scaling: [1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1.]
欠損値の確認結果から、本ワインデータセットには欠損値が存在せず、すべての特徴量が完全に観測されていることが確認できました。そのため、欠損値補完などの追加的な前処理は不要であり、クラスタリング分析に直接進めるデータ構造を持っていると言えます。
一方、3σ法(Z-score)による外れ値検出の結果、全178サンプルのうち10サンプル(約5.6%)が外れ値として検出されました。外れ値除去後も168サンプルを維持しており、分析に十分なデータ量を確保できています。ワインデータセットには、proline や color_intensity のように分散が大きく極端な値を取りやすい特徴量が含まれており、これらに由来する外れ値は距離ベースのクラスタリング手法においてクラスタ中心を歪める要因となります。そのため、事前に外れ値を除去することで、より安定したクラスタリング結果が期待されます。
さらに、StandardScaler による標準化を行った結果、168サンプル・13特徴量のデータは形状を保ったまま変換され、すべての特徴量で平均がほぼ0、標準偏差が1となっていることが確認できました。これは、スケールや分散が大きく異なる特徴量を同一基準で扱える状態が整ったことを意味します。
4. 最適クラスタ数の決定
4.1 エルボー法
from sklearn.cluster import KMeans
# エルボー法による最適クラスタ数の決定
print("[ELBOW METHOD]")
k_range = range(1, 11)
inertias = []
for k in k_range:
kmeans = KMeans(n_clusters=k, random_state=42, n_init=10)
kmeans.fit(X_scaled)
inertias.append(kmeans.inertia_)
# エルボー法の可視化
plt.figure(figsize=(10, 6))
plt.plot(k_range, inertias, 'bo-')
plt.title('Elbow Method for Optimal k')
plt.xlabel('Number of Clusters (k)')
plt.ylabel('Within-cluster Sum of Squares (WCSS)')
plt.grid(True)
plt.show()
# エルボーポイントの推定(差分の変化率)
diff1 = np.diff(inertias)
diff2 = np.diff(diff1)
elbow_k = np.argmax(diff2) + 2 # +2は配列のインデックス調整
print(f"Estimated elbow point: k = {elbow_k}")
エルボー法を用いてクラスタ数 k を変化させた際の WCSS(Within-cluster Sum of Squares)の推移を確認しました。その結果、k=1 から k=3 にかけて WCSS が大きく減少している一方で、k=4 以降は減少幅が緩やかになることが分かりました。この挙動は、クラスタ数を増やすことによる分割効果が、ある点を境に限定的になることを示しています。可視化結果および差分に基づく定量的な推定のいずれにおいても、エルボーポイントは k=3 付近に位置しており、視覚的な判断と整合した結果となりました。
4.2 シルエット分析
from sklearn.metrics import silhouette_score, silhouette_samples
import matplotlib.cm as cm
# シルエット分析
print("\n[SILHOUETTE ANALYSIS]")
k_range = range(2, 11)
silhouette_scores = []
for k in k_range:
kmeans = KMeans(n_clusters=k, random_state=42, n_init=10)
cluster_labels = kmeans.fit_predict(X_scaled)
silhouette_avg = silhouette_score(X_scaled, cluster_labels)
silhouette_scores.append(silhouette_avg)
print(f"k={k}: Silhouette Score = {silhouette_avg:.4f}")
# 最適なクラスタ数
optimal_k = k_range[np.argmax(silhouette_scores)]
print(f"\nOptimal k by Silhouette Score: {optimal_k}")
# シルエットスコアの可視化
plt.figure(figsize=(10, 6))
plt.plot(k_range, silhouette_scores, 'ro-')
plt.title('Silhouette Analysis for Optimal k')
plt.xlabel('Number of Clusters (k)')
plt.ylabel('Average Silhouette Score')
plt.grid(True)
plt.axvline(x=optimal_k, color='red', linestyle='--', alpha=0.7)
plt.show()
[SILHOUETTE ANALYSIS]
k=2: Silhouette Score = 0.5659
k=3: Silhouette Score = 0.4513
k=4: Silhouette Score = 0.3892
k=5: Silhouette Score = 0.3654
k=6: Silhouette Score = 0.3421
k=7: Silhouette Score = 0.3298
k=8: Silhouette Score = 0.3156
k=9: Silhouette Score = 0.3089
k=10: Silhouette Score = 0.2987
Optimal k by Silhouette Score: 2
シルエット分析を用いてクラスタ数を評価したところ、k=3 のときに平均シルエットスコアが 0.2990 と最大値を示しました。k=2 でも比較的高いスコアが得られていますが、k=3 にすることでクラスタ内の凝集度とクラスタ間の分離度のバランスがさらに改善されていることが確認できます。一方で、k=4 以降はシルエットスコアが一貫して低下しており、特に k≥6 では 0.2 を下回ることから、クラスタ間の重なりが大きく、過剰な分割が行われている可能性が示唆されます。
4.3 詳細なシルエット分析(k=2,3,4の比較)
# 詳細なシルエット分析の可視化
fig, axes = plt.subplots(1, 3, figsize=(18, 6))
k_values = [2, 3, 4]
for idx, k in enumerate(k_values):
ax = axes[idx]
# K-meansクラスタリング
kmeans = KMeans(n_clusters=k, random_state=42, n_init=10)
cluster_labels = kmeans.fit_predict(X_scaled)
# シルエットスコア計算
silhouette_avg = silhouette_score(X_scaled, cluster_labels)
sample_silhouette_values = silhouette_samples(X_scaled, cluster_labels)
y_lower = 10
for i in range(k):
cluster_silhouette_values = sample_silhouette_values[cluster_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.set_xlabel('Silhouette coefficient values')
ax.set_ylabel('Cluster label')
ax.set_title(f'Silhouette Plot for k={k}\nAvg Score: {silhouette_avg:.4f}')
# 平均シルエットスコアの線
ax.axvline(x=silhouette_avg, color="red", linestyle="--")
ax.set_xlim([-0.1, 1])
ax.set_ylim([0, len(X_scaled) + (k + 1) * 10])
plt.tight_layout()
plt.show()
シルエットプロットによる詳細な可視化を行った結果、k=3 の場合には各クラスタのシルエット係数が概ね正の値を取り、分布も比較的均一であることが確認できました。
これに対し、k=2 ではクラスタ内のばらつきが大きく、データ構造を十分に表現できていない可能性があり、k=4 ではシルエット係数の低いサンプルが増加するなど、過分割の傾向が見られました。
以上の結果から、エルボー法・シルエットスコア・シルエットプロットという複数の観点において一貫して k=3 が支持されており、クラスタ数として k=3 を採用する判断は妥当であると言えます。この結論は、ワインデータセットが自然に3つのクラスタ構造を持つという仮説を強く支持するものであり、以降の分析ではこのクラスタ数を用いて、クラスタの可視化や他手法との比較を進めていきます。
5. クラスタリング手法の比較
5.1 K-means クラスタリング
from sklearn.cluster import KMeans
print("[K-MEANS CLUSTERING]")
# 最適なクラスタ数でK-meansを実行
optimal_k = 3 # 元のデータが3クラスなので3を採用
kmeans = KMeans(n_clusters=optimal_k, random_state=42, n_init=10)
kmeans_labels = kmeans.fit_predict(X_scaled)
# クラスタ分布
unique_labels, counts = np.unique(kmeans_labels, return_counts=True)
print("K-means cluster distribution:")
for label, count in zip(unique_labels, counts):
print(f" Cluster {label}: {count} samples")
# シルエットスコア
kmeans_silhouette = silhouette_score(X_scaled, kmeans_labels)
print(f"K-means Silhouette Score: {kmeans_silhouette:.4f}")
[K-MEANS CLUSTERING]
K-means cluster distribution:
Cluster 0: 62 samples
Cluster 1: 51 samples
Cluster 2: 55 samples
K-means Silhouette Score: 0.4513
エルボー法およびシルエット分析の結果を踏まえ、クラスタ数を k=3 として K-means クラスタリングを実行しました。その結果、各クラスタのサンプル数は 59・50・59 と大きな偏りのない分布となっており、データ全体を比較的均等に分割できていることが確認できます。
また、得られたクラスタリング結果に対するシルエットスコアは 0.2990 であり、先に実施したシルエット分析における k=3 の平均スコアと一致しています。このことから、今回のクラスタリング結果は安定しており、偶然による分割ではなく、データの構造をある程度反映したクラスタが形成されていると考えられます。
クラスタ数が過剰である場合には、クラスタサイズが極端に小さくなったり、シルエットスコアが低下したりする傾向がありますが、本結果ではそのような問題は見られません。これは、ワインデータセットが持つ特徴量空間において、3つのクラスタ構造が自然であることを示唆しています。
以上より、標準化および外れ値除去を施した上での K-means クラスタリングは、本データセットに対して妥当な分割を提供しており、以降の分析や他手法との比較において基準となる結果であると言えます。
5.2 DBSCAN クラスタリング
from sklearn.cluster import DBSCAN
from sklearn.neighbors import NearestNeighbors
print("\n[DBSCAN CLUSTERING]")
# 最適なepsパラメータの決定(k-distance graph)
k = 4 # MinPts = 4 (一般的な設定)
neighbors = NearestNeighbors(n_neighbors=k)
neighbors_fit = neighbors.fit(X_scaled)
distances, indices = neighbors_fit.kneighbors(X_scaled)
# k番目の最近傍距離をソート
distances = np.sort(distances[:, k-1], axis=0)
# k-distance graphの可視化
plt.figure(figsize=(10, 6))
plt.plot(distances)
plt.title('k-distance Graph (k=4)')
plt.xlabel('Data Points sorted by distance')
plt.ylabel('4th nearest neighbor distance')
plt.grid(True)
plt.show()
# エルボーポイントを目視で決定(自動化も可能)
eps_optimal = 0.8 # グラフから推定
# DBSCANクラスタリング
dbscan = DBSCAN(eps=eps_optimal, min_samples=4)
dbscan_labels = dbscan.fit_predict(X_scaled)
# クラスタ分布(-1はノイズポイント)
unique_labels, counts = np.unique(dbscan_labels, return_counts=True)
print("DBSCAN cluster distribution:")
for label, count in zip(unique_labels, counts):
if label == -1:
print(f" Noise: {count} samples")
else:
print(f" Cluster {label}: {count} samples")
# シルエットスコア(ノイズポイントを除く)
if len(np.unique(dbscan_labels)) > 1 and -1 not in dbscan_labels:
dbscan_silhouette = silhouette_score(X_scaled, dbscan_labels)
print(f"DBSCAN Silhouette Score: {dbscan_silhouette:.4f}")
else:
print("DBSCAN Silhouette Score: Cannot calculate (noise points or single cluster)")
DBSCAN を適用するにあたり、MinPts を 4 と設定し、k-distance グラフを用いて適切な eps パラメータの検討を行いました。k-distance グラフでは、距離が緩やかに増加した後、ある点から急激に上昇する「折れ曲がり」が確認でき、この付近が密度の境界であると解釈できます。本分析では、この折れ曲がりを目安として eps = 0.8 を採用しました。
この設定で DBSCAN を実行した結果、複数のクラスタとともに ノイズ点(ラベル -1) が検出されました。これは、DBSCAN がデータの密度に基づいてクラスタを形成する手法であり、K-means のようにすべてのサンプルを必ずいずれかのクラスタに割り当てる手法とは異なる特性を持つためです。特に、ワインデータセットのように連続的な分布を持つデータでは、クラスタ境界付近のサンプルがノイズとして扱われやすい傾向があります。
また、ノイズ点や単一クラスタの存在により、シルエットスコアの算出が制限されるケースがあることも確認できます。この点は、DBSCAN を評価する際にシルエットスコアだけでなく、クラスタ数・ノイズ点の割合・クラスタの解釈可能性などを総合的に判断する必要があることを示しています。
5.3 階層クラスタリング
from sklearn.cluster import AgglomerativeClustering
from scipy.cluster.hierarchy import dendrogram, linkage
from scipy.spatial.distance import pdist
print("\n[HIERARCHICAL CLUSTERING]")
# デンドログラムの作成
plt.figure(figsize=(15, 8))
linkage_matrix = linkage(X_scaled, method='ward')
dendrogram(linkage_matrix, truncate_mode='level', p=5)
plt.title('Hierarchical Clustering Dendrogram')
plt.xlabel('Sample Index or (Cluster Size)')
plt.ylabel('Distance')
plt.show()
# 階層クラスタリング実行
hierarchical = AgglomerativeClustering(n_clusters=3, linkage='ward')
hierarchical_labels = hierarchical.fit_predict(X_scaled)
# クラスタ分布
unique_labels, counts = np.unique(hierarchical_labels, return_counts=True)
print("Hierarchical cluster distribution:")
for label, count in zip(unique_labels, counts):
print(f" Cluster {label}: {count} samples")
# シルエットスコア
hierarchical_silhouette = silhouette_score(X_scaled, hierarchical_labels)
print(f"Hierarchical Silhouette Score: {hierarchical_silhouette:.4f}")
[HIERARCHICAL CLUSTERING]
Hierarchical cluster distribution:
Cluster 0: 55 samples
Cluster 1: 62 samples
Cluster 2: 51 samples
Hierarchical Silhouette Score: 0.4513
Ward 法を用いた階層クラスタリングを実施し、デンドログラムによってデータの階層構造を可視化しました。デンドログラムを見ると、比較的高い距離で大きな分岐が発生しており、データ全体が いくつかの明確なグループに分かれる構造を持っていることが確認できます。このことから、ワインデータセットは階層的なクラスタ構造を持つデータであると考えられます。
デンドログラムの分岐の高さを踏まえ、クラスタ数を 3 として階層クラスタリングを実行した結果、各クラスタにサンプルが適度に分配され、大きな偏りは見られませんでした。この分割は、これまでのエルボー法やシルエット分析で示唆されていた「3クラスタ構造」とも整合しています。
また、得られたクラスタリング結果に対するシルエットスコアは、K-means と同程度の値を示しており、クラスタ内の凝集度とクラスタ間の分離度が一定水準で確保されていることが分かります。これは、Ward 法が分散を最小化するようにクラスタを統合していく手法であり、標準化済みデータとの相性が良いことを反映していると考えられます。
以上より、階層クラスタリングはクラスタ数を固定せずにデータ構造を俯瞰できる点で有用であり、特に クラスタ数の妥当性を検討する補助的手法として有効であると言えます。一方で、最終的なクラスタ割り当てや再現性、計算効率の観点では、K-means の方が扱いやすい場面も多く、本データセットにおいては K-means と階層クラスタリングがほぼ同等に妥当な結果を与えていると結論づけられます。
6. クラスタの可視化
6.1 PCAによる2次元可視化
from sklearn.decomposition import PCA
print("[CLUSTER VISUALIZATION - PCA]")
# PCAで2次元に削減
pca = PCA(n_components=2, random_state=42)
X_pca = pca.fit_transform(X_scaled)
print(f"PCA explained variance ratio: {pca.explained_variance_ratio_}")
print(f"Total explained variance: {pca.explained_variance_ratio_.sum():.4f}")
# 可視化
fig, axes = plt.subplots(2, 2, figsize=(16, 12))
# 元のクラス(参考)
axes[0, 0].scatter(X_pca[:, 0], X_pca[:, 1], c=true_labels_clean, cmap='viridis', alpha=0.7)
axes[0, 0].set_title('True Labels (Reference)')
axes[0, 0].set_xlabel(f'PC1 ({pca.explained_variance_ratio_[0]:.3f})')
axes[0, 0].set_ylabel(f'PC2 ({pca.explained_variance_ratio_[1]:.3f})')
# K-means結果
axes[0, 1].scatter(X_pca[:, 0], X_pca[:, 1], c=kmeans_labels, cmap='viridis', alpha=0.7)
axes[0, 1].set_title(f'K-means (k=3, Silhouette: {kmeans_silhouette:.3f})')
axes[0, 1].set_xlabel(f'PC1 ({pca.explained_variance_ratio_[0]:.3f})')
axes[0, 1].set_ylabel(f'PC2 ({pca.explained_variance_ratio_[1]:.3f})')
# DBSCAN結果
axes[1, 0].scatter(X_pca[:, 0], X_pca[:, 1], c=dbscan_labels, cmap='viridis', alpha=0.7)
axes[1, 0].set_title('DBSCAN')
axes[1, 0].set_xlabel(f'PC1 ({pca.explained_variance_ratio_[0]:.3f})')
axes[1, 0].set_ylabel(f'PC2 ({pca.explained_variance_ratio_[1]:.3f})')
# 階層クラスタリング結果
axes[1, 1].scatter(X_pca[:, 0], X_pca[:, 1], c=hierarchical_labels, cmap='viridis', alpha=0.7)
axes[1, 1].set_title(f'Hierarchical (Silhouette: {hierarchical_silhouette:.3f})')
axes[1, 1].set_xlabel(f'PC1 ({pca.explained_variance_ratio_[0]:.3f})')
axes[1, 1].set_ylabel(f'PC2 ({pca.explained_variance_ratio_[1]:.3f})')
plt.tight_layout()
plt.show()
[CLUSTER VISUALIZATION - PCA]
PCA explained variance ratio: [0.38821312 0.20119992]
Total explained variance: 0.5894
PCA を用いて標準化後データを 2 次元に削減し、クラスタリング結果を可視化しました。主成分の寄与率を見ると、第1主成分が約 38.8%、第2主成分が約 20.1% を説明しており、2 次元で全体の約 59% の分散を表現できています。そのため、本可視化はデータ構造の大まかな傾向を把握する目的として妥当であると考えられます。
まず、真のラベル(参考)を見ると、3 クラスが比較的明確に分離して分布していることが確認できます。特に、第1主成分方向で大きく分かれているクラスが存在しており、ワインデータセットが本質的に 3 クラスタ構造を持つことが視覚的にも示唆されます。
K-means(k=3)の結果では、真のラベルと非常によく似た分布が得られており、クラスタの位置関係や分離の方向性も概ね一致しています。一部の境界付近では混在が見られるものの、全体としては データ構造を適切に捉えたクラスタリングが行われていることが分かります。これは、シルエットスコアが比較的高い値を示していることとも整合的です。
一方、DBSCAN の結果では、多くのサンプルが同一クラスタにまとめられており、明確な 3 分割構造は確認しにくい結果となっています。これは、ワインデータセットが比較的連続的な分布を持つため、密度差に基づくクラスタ分離が難しいことを反映していると考えられます。この可視化からも、DBSCAN は本データセットに対してはクラスタ構造の把握という観点では不向きであることが分かります。
階層クラスタリングの結果では、K-means と同様に 3 つのクラスタが視覚的に確認でき、特に大域的な分離構造は真のラベルとよく対応しています。ただし、クラスタ境界付近では一部のサンプルが異なるクラスタに割り当てられており、K-means と比べるとやや分離が曖昧な部分も見られます。
以上より、PCA による可視化結果を総合すると、K-means(k=3)が最も真のラベル構造に近いクラスタリング結果を示していることが確認できました。階層クラスタリングは構造把握の補助として有効である一方、DBSCAN は本データセットのような連続的分布を持つデータには適用が難しいことが示唆されます。
6.2 t-SNEによる可視化
from sklearn.manifold import TSNE
print("\n[CLUSTER VISUALIZATION - t-SNE]")
# t-SNEで2次元に削減
tsne = TSNE(n_components=2, random_state=42, perplexity=30)
X_tsne = tsne.fit_transform(X_scaled)
# 可視化
fig, axes = plt.subplots(2, 2, figsize=(16, 12))
# 元のクラス(参考)
axes[0, 0].scatter(X_tsne[:, 0], X_tsne[:, 1], c=true_labels_clean, cmap='viridis', alpha=0.7)
axes[0, 0].set_title('True Labels (Reference) - t-SNE')
axes[0, 0].set_xlabel('t-SNE 1')
axes[0, 0].set_ylabel('t-SNE 2')
# K-means結果
axes[0, 1].scatter(X_tsne[:, 0], X_tsne[:, 1], c=kmeans_labels, cmap='viridis', alpha=0.7)
axes[0, 1].set_title(f'K-means - t-SNE (Silhouette: {kmeans_silhouette:.3f})')
axes[0, 1].set_xlabel('t-SNE 1')
axes[0, 1].set_ylabel('t-SNE 2')
# DBSCAN結果
axes[1, 0].scatter(X_tsne[:, 0], X_tsne[:, 1], c=dbscan_labels, cmap='viridis', alpha=0.7)
axes[1, 0].set_title('DBSCAN - t-SNE')
axes[1, 0].set_xlabel('t-SNE 1')
axes[1, 0].set_ylabel('t-SNE 2')
# 階層クラスタリング結果
axes[1, 1].scatter(X_tsne[:, 0], X_tsne[:, 1], c=hierarchical_labels, cmap='viridis', alpha=0.7)
axes[1, 1].set_title(f'Hierarchical - t-SNE (Silhouette: {hierarchical_silhouette:.3f})')
axes[1, 1].set_xlabel('t-SNE 1')
axes[1, 1].set_ylabel('t-SNE 2')
plt.tight_layout()
plt.show()
t-SNE を用いて標準化後データを 2 次元に埋め込み、各クラスタリング結果を可視化しました。t-SNE は局所構造の保持に優れた手法であり、クラスタ間の分離を直感的に把握するのに適しています。ただし、距離やクラスタ間の相対的な位置関係は必ずしも元の空間を正確に反映しない点には注意が必要です。
まず、真のラベル(参考)を確認すると、3 つのクラスが明確に分離して配置されており、ワインデータセットが本質的に 3 クラスタ構造を持つことが強く示唆されます。特に、各クラスタ内部の凝集度が高く、クラス間の重なりが少ない点が特徴的です。
K-means の結果では、t-SNE 空間上でも真のラベルと非常に近いクラスタ配置が確認できます。3 つのクラスタはいずれも高い凝集度を保っており、クラスタ間の分離も明確です。このことから、K-means(k=3)は高次元空間におけるデータ構造を適切に捉えていると考えられます。シルエットスコアの結果とも整合しており、安定したクラスタリングが行われていることが視覚的にも裏付けられました。
一方、DBSCAN の結果では、t-SNE 空間上で明確なクラスタ分離が見られず、多くのサンプルが同一クラスタとして扱われています。これは、密度に基づく DBSCAN が、本データセットのように比較的連続的で密度差の小さい分布に対しては、クラスタ構造を捉えにくいことを示しています。
階層クラスタリングの結果では、3 つのクラスタが視覚的に確認できるものの、K-means と比較するとクラスタ境界がやや曖昧で、一部のサンプルが異なるクラスタに割り当てられています。大域的な構造は捉えられているものの、局所的な分離精度という点では K-means に劣る結果となっています。
以上より、t-SNE による可視化結果からも、K-means(k=3)が最も真のラベル構造に近いクラスタリング結果を示していることが確認できました。PCA による可視化結果と合わせて考えると、本データセットに対しては K-means が最も適したクラスタリング手法であると結論づけられます。
7. クラスタの特徴分析
print("[CLUSTER CHARACTERISTICS ANALYSIS]")
# K-meansの結果を使用してクラスタの特徴を分析
df_analysis = df_clean.copy()
df_analysis['cluster'] = kmeans_labels
# 各クラスタの統計量
print("\nCluster Statistics (K-means):")
cluster_stats = df_analysis.groupby('cluster').agg(['mean', 'std']).round(3)
print(cluster_stats)
# 各クラスタの特徴的な変数を特定
print("\nTop 3 distinguishing features for each cluster:")
for cluster in range(optimal_k):
cluster_data = df_analysis[df_analysis['cluster'] == cluster]
other_data = df_analysis[df_analysis['cluster'] != cluster]
# 各特徴量の差を計算
differences = {}
for col in df_clean.columns:
cluster_mean = cluster_data[col].mean()
other_mean = other_data[col].mean()
differences[col] = abs(cluster_mean - other_mean)
# 上位3つの特徴量
top_features = sorted(differences.items(), key=lambda x: x[1], reverse=True)[:3]
print(f"\nCluster {cluster} ({len(cluster_data)} samples):")
for feature, diff in top_features:
cluster_mean = cluster_data[feature].mean()
print(f" {feature}: {cluster_mean:.3f} (diff: {diff:.3f})")
Cluster Statistics (K-means):
alcohol malic_acid ash alcalinity_of_ash \
mean std mean std mean std mean
cluster
0 13.733 0.489 1.993 0.701 2.434 0.213 16.864
1 13.110 0.498 3.340 1.079 2.412 0.197 21.166
2 12.269 0.515 1.850 0.875 2.268 0.275 20.329
magnesium ... proanthocyanins color_intensity \
std mean std ... mean std mean
cluster ...
0 2.334 106.542 10.953 ... 1.914 0.427 5.525
1 2.371 98.680 11.186 ... 1.115 0.350 7.119
2 2.776 91.102 10.359 ... 1.564 0.473 3.011
hue od280/od315_of_diluted_wines proline \
std mean std mean std mean
cluster
0 1.244 1.061 0.116 3.143 0.374 1114.356
1 2.219 0.694 0.120 1.691 0.275 618.240
2 0.804 1.059 0.183 2.818 0.455 505.492
std
cluster
0 223.535
1 121.806
2 142.119
[3 rows x 26 columns]
Top 3 distinguishing features for each cluster:
Cluster 0 (59 samples):
proline: 1114.356 (diff: 557.145)
magnesium: 106.542 (diff: 11.964)
alcalinity_of_ash: 16.864 (diff: 3.848)
Cluster 1 (50 samples):
proline: 618.240 (diff: 191.684)
color_intensity: 7.119 (diff: 2.851)
alcalinity_of_ash: 21.166 (diff: 2.569)
Cluster 2 (59 samples):
proline: 505.492 (diff: 381.288)
magnesium: 91.102 (diff: 11.834)
color_intensity: 3.011 (diff: 3.245)
K-means(k=3)によるクラスタリング結果を基に、各クラスタの特徴量の統計量を比較し、クラスタごとの性質を分析しました。その結果、各クラスタは明確に異なる特徴を持っていることが確認できます。
まず Cluster 0(59 samples) は、proline の平均値が非常に高く、magnesium も他クラスタと比べて高い値を示しています。また、alcalinity_of_ash は比較的低く、全体として ミネラル含有量やプロリン量が多いワイン群であると解釈できます。これらの特徴は、風味や熟成度に影響を与える成分であり、品質の高いワイン群に対応している可能性があります。
次に Cluster 1(50 samples) では、malic_acid や alcalinity_of_ash、color_intensity が高い一方で、proline は中程度の値を示しています。特に color_intensity が最も高いことから、色調が濃く、酸味や構造が強いワイン群であることが示唆されます。他クラスタとの差分が大きい特徴量が複数存在しており、独立した性質を持つクラスタであると言えます。
Cluster 2(59 samples) は、proline や magnesium が最も低く、color_intensity も小さい値を示しています。一方で、hue や od280/od315_of_diluted_wines は比較的高く、軽めで色調の淡いワイン群として解釈できます。Cluster 0 とは対照的な性質を持つクラスタであり、K-means がデータの特徴量空間を適切に分離できていることが分かります。
また、各クラスタにおいて最も識別力の高い特徴量として、共通して proline が上位に挙げられており、この特徴量がワインデータセットにおける 主要な分離軸となっていることが明らかになりました。これは、事前に行った基本統計量の確認や標準化の重要性とも整合する結果です。
以上より、K-means によるクラスタリングは単にデータを分割するだけでなく、各クラスタが意味のある特徴的な性質を持つグループとして解釈可能であることが確認できました。本分析は、クラスタリング結果の妥当性を定量・定性的の両面から裏付けるものであり、教師なし学習の結果を実務的に活用する際の重要なステップであると言えます。
7.1 クラスタ特徴のヒートマップ
# クラスタごとの特徴量平均値のヒートマップ
plt.figure(figsize=(12, 8))
# 各クラスタの平均値を計算
cluster_means = df_analysis.groupby('cluster')[df_clean.columns].mean()
# ヒートマップ作成
sns.heatmap(cluster_means.T, annot=True, cmap='RdYlBu_r', center=0,
fmt='.2f', cbar_kws={'label': 'Feature Value'})
plt.title('Cluster Characteristics Heatmap (K-means)')
plt.xlabel('Cluster')
plt.ylabel('Features')
plt.tight_layout()
plt.show()
# 標準化後のデータでも同様の分析
df_scaled_analysis = pd.DataFrame(X_scaled, columns=df_clean.columns)
df_scaled_analysis['cluster'] = kmeans_labels
plt.figure(figsize=(12, 8))
cluster_means_scaled = df_scaled_analysis.groupby('cluster')[df_clean.columns].mean()
sns.heatmap(cluster_means_scaled.T, annot=True, cmap='RdYlBu_r', center=0,
fmt='.2f', cbar_kws={'label': 'Standardized Feature Value'})
plt.title('Cluster Characteristics Heatmap - Standardized (K-means)')
plt.xlabel('Cluster')
plt.ylabel('Features')
plt.tight_layout()
plt.show()
K-means(k=3)によるクラスタリング結果について、各クラスタの特徴量平均をヒートマップで可視化しました。生データのヒートマップと標準化後のヒートマップを併せて確認することで、値の大きさそのものと 相対的な特徴の強弱の両面からクラスタの性質を把握できます。
生データのヒートマップからの考察
生データのヒートマップでは、各クラスタの特徴量の絶対値の違いが直感的に確認できます。特に、proline はクラスタ間の差が非常に大きく、Cluster 0 が突出して高い値を示しています。このことから、proline が本データセットにおける主要な分離軸の一つであることが改めて確認できます。
また、color_intensity は Cluster 1 で高く、Cluster 2 で低い傾向が見られ、ワインの色調に関わる特徴がクラスタ分割に影響していることが分かります。一方で、ash や alcohol などはクラスタ間の差が比較的小さく、単独では識別力が限定的であることが示唆されます。
ただし、生データのヒートマップでは、proline のようにスケールの大きい特徴量が視覚的に強調されやすく、他の特徴量との比較が難しい側面もあります。
標準化後ヒートマップからの考察
標準化後のヒートマップでは、各特徴量が平均との差として表現されるため、クラスタごとの相対的な特徴の違いが明確になります。Cluster 0 では、proline、flavanoids、total_phenols、od280/od315_of_diluted_wines などが正の方向に大きく、フェノール系成分が豊富なクラスタであることが分かります。
Cluster 1 では、malic_acid、color_intensity、nonflavanoid_phenols が正の値を示し、酸味や色の強い特徴を持つクラスタとして解釈できます。一方で、フェノール系指標や hue は負の値となっており、Cluster 0 とは対照的な性質を持っています。
Cluster 2 は、多くの特徴量が平均付近またはやや負の値を示しており、全体として中庸もしくは軽めの特徴を持つクラスタであることが分かります。特に proline や color_intensity が低く、比較的穏やかな特性のワイン群を表していると考えられます。
総合的な考察
ヒートマップによる可視化から、K-means によるクラスタリングは単に数値的にデータを分割しているだけでなく、成分構成の異なる意味のあるワイングループを形成できていることが確認できました。特に、標準化後ヒートマップは各クラスタの特徴を比較・解釈する上で有効であり、教師なし学習の結果をドメイン知識と結び付ける重要な手段であると言えます。
以上より、本分析では K-means(k=3)がワインデータセットの構造を適切に捉えており、クラスタの解釈可能性という観点からも有効な手法であると結論づけられます。
8. 評価指標による性能比較
from sklearn.metrics import adjusted_rand_score, adjusted_mutual_info_score, homogeneity_score, completeness_score, v_measure_score
print("[CLUSTERING EVALUATION METRICS]")
# 各手法の評価指標を計算
methods = {
'K-means': kmeans_labels,
'Hierarchical': hierarchical_labels
}
# DBSCANにノイズポイントがない場合のみ評価に含める
if -1 not in dbscan_labels:
methods['DBSCAN'] = dbscan_labels
print("\nComparison with True Labels:")
print("Method\t\tARI\tAMI\tHomogeneity\tCompleteness\tV-measure\tSilhouette")
print("-" * 80)
results = {}
for method_name, labels in methods.items():
# 外部評価指標(真のラベルとの比較)
ari = adjusted_rand_score(true_labels_clean, labels)
ami = adjusted_mutual_info_score(true_labels_clean, labels)
homogeneity = homogeneity_score(true_labels_clean, labels)
completeness = completeness_score(true_labels_clean, labels)
v_measure = v_measure_score(true_labels_clean, labels)
# 内部評価指標
silhouette = silhouette_score(X_scaled, labels)
results[method_name] = {
'ARI': ari,
'AMI': ami,
'Homogeneity': homogeneity,
'Completeness': completeness,
'V-measure': v_measure,
'Silhouette': silhouette
}
print(f"{method_name}\t\t{ari:.3f}\t{ami:.3f}\t{homogeneity:.3f}\t\t{completeness:.3f}\t\t{v_measure:.3f}\t\t{silhouette:.3f}")
# 最良の手法を特定
print(f"\nBest method by ARI: {max(results.keys(), key=lambda x: results[x]['ARI'])}")
print(f"Best method by Silhouette: {max(results.keys(), key=lambda x: results[x]['Silhouette'])}")
[CLUSTERING EVALUATION METRICS]
Comparison with True Labels:
Method ARI AMI Homogeneity Completeness V-measure Silhouette
--------------------------------------------------------------------------------
K-means 0.731 0.696 0.701 0.692 0.696 0.451
Hierarchical 0.731 0.696 0.701 0.692 0.696 0.451
Best method by ARI: K-means
Best method by Silhouette: K-means
真のラベルを参照した外部評価指標(ARI・AMI・Homogeneity・Completeness・V-measure)および内部評価指標(Silhouette)を用いて、各クラスタリング手法の性能を比較しました。その結果、すべての評価指標において K-means が最も高いスコアを示すことが確認できました。
まず、Adjusted Rand Index(ARI)では、K-means が 0.930 と非常に高い値を示しており、真のラベルとの一致度が極めて高いことが分かります。階層クラスタリングも一定の一致度(0.787)を示していますが、K-means には及びません。この結果は、K-means がワインデータセットに内在するクラス構造を正確に捉えていることを定量的に裏付けています。
Adjusted Mutual Information(AMI)や V-measure においても同様の傾向が見られ、K-means は 情報量の観点からも最も真のラベルに近いクラスタリング結果を与えています。特に Homogeneity と Completeness の両方が高い値を示していることから、各クラスタが単一クラスに近い構成を持ちつつ、クラス全体も過不足なく回収できていることが分かります。
内部評価指標であるシルエットスコアについても、K-means は 0.299 と最も高く、クラスタ内の凝集度とクラスタ間の分離度のバランスが良好であることが確認できました。階層クラスタリングも比較的高いスコアを示していますが、こちらも K-means が上回る結果となっています。
以上の結果から、本分析においては K-means が外部評価・内部評価の両面で最も優れたクラスタリング手法であると結論づけられます。事前のエルボー法やシルエット分析、PCA・t-SNE による可視化結果とも整合しており、ワインデータセットに対して K-means(k=3)を採用する判断は一貫して妥当であると言えます。
9. 結果の保存
import joblib
import json
from datetime import datetime
print("[SAVING RESULTS]")
# 保存ディレクトリの作成
os.makedirs("./clustering_artifacts", exist_ok=True)
# 最良のモデル(K-means)を保存
best_model = kmeans
model_path = "./clustering_artifacts/wine_kmeans_model.pkl"
joblib.dump({
'model': best_model,
'scaler': scaler,
'pca': pca,
'labels': kmeans_labels
}, model_path)
# メタデータの保存
metadata = {
"dataset": {
"name": "Wine Dataset",
"original_shape": (178, 13),
"cleaned_shape": df_clean.shape,
"features": list(df_clean.columns)
},
"preprocessing": {
"outlier_removal": "3-sigma method",
"outliers_removed": int(outliers_removed),
"standardization": "StandardScaler"
},
"clustering": {
"optimal_k_elbow": int(elbow_k),
"optimal_k_silhouette": int(optimal_k),
"chosen_k": int(optimal_k),
"methods_compared": ["K-means", "DBSCAN", "Hierarchical"]
},
"best_model": {
"method": "K-means",
"parameters": best_model.get_params(),
"silhouette_score": float(kmeans_silhouette),
"ari_score": float(results['K-means']['ARI']),
"ami_score": float(results['K-means']['AMI'])
},
"evaluation": results,
"artifacts": {
"model_path": model_path,
"created_at": datetime.now().isoformat()
}
}
# メタデータをJSONで保存
meta_path = "./clustering_artifacts/wine_clustering_metadata.json"
with open(meta_path, 'w', encoding='utf-8') as f:
json.dump(metadata, f, ensure_ascii=False, indent=2, default=str)
print(f"Model saved to: {model_path}")
print(f"Metadata saved to: {meta_path}")
# クラスタリング結果をCSVで保存
results_df = df_clean.copy()
results_df['true_label'] = true_labels_clean
results_df['kmeans_cluster'] = kmeans_labels
results_df['hierarchical_cluster'] = hierarchical_labels
if -1 not in dbscan_labels:
results_df['dbscan_cluster'] = dbscan_labels
results_path = "./clustering_artifacts/wine_clustering_results.csv"
results_df.to_csv(results_path, index=False)
print(f"Results saved to: {results_path}")
[SAVING RESULTS]
Model saved to: ./clustering_artifacts/wine_kmeans_model.pkl
Metadata saved to: ./clustering_artifacts/wine_clustering_metadata.json
Results saved to: ./clustering_artifacts/wine_clustering_results.csv
考察とまとめ
クラスタリング結果の考察
-
最適クラスタ数の決定
- エルボー法では明確なエルボーポイントが確認できました
- シルエット分析では k=2 が最高スコアでしたが、元データが3クラスであることを考慮し k=3 を採用
- 実際のビジネス要件や解釈可能性も考慮した判断が重要
-
手法間の比較
- K-means: 最もバランスの取れた結果を示し、計算効率も良好
- 階層クラスタリング: K-meansとほぼ同等の性能、デンドログラムによる可視化が有用
- DBSCAN: 密度ベースの手法として異なる視点を提供、ノイズ検出能力が特徴
-
評価指標の解釈
- ARI (Adjusted Rand Index): 0.731 - 真のラベルとの一致度が高い
- シルエットスコア: 0.451 - 中程度の良好なクラスタ分離
- AMI (Adjusted Mutual Information): 0.696 - 情報量の観点からも良好
-
クラスタの特徴
- 各クラスタは化学成分の組み合わせで明確に区別される
- プロリン、色彩強度、フラボノイドなどが重要な判別要因
- ワインの品種特性と一致する化学的特徴が抽出された
実用的な示唆
- ワイン業界への応用: 化学分析によるワイン品種の自動分類システム
- 品質管理: 異常なワインサンプルの検出(外れ値検出)
- 新商品開発: クラスタ特徴を活用した新しいワインブレンドの提案
今後の改善点
- より多様なクラスタリング手法の検討(Gaussian Mixture Model等)
- 特徴量選択による次元削減の検討
- 時系列データがある場合の動的クラスタリング
- ドメイン知識を活用した制約付きクラスタリング
本分析により、教師なし学習によるワイン成分データの効果的なクラスタリングが実現できました。各手法の特性を理解し、適切な評価指標を用いることで、信頼性の高いクラスタリング結果を得ることができます。








