← 返回数据分析目录
← 返回学习笔记首页
聚类分析(KMeans/DBSCAN/层次聚类)
数据分析专题 · 无监督学习的数据分组
专题: Python数据分析系统学习 - 无监督学习篇
关键词: 聚类, KMeans, DBSCAN, 层次聚类, 轮廓系数, 肘部法, 无监督学习, scikit-learn
一、聚类分析概述
聚类分析(Clustering Analysis)是无监督学习 中最核心的任务之一。它的目标是将数据集中相似的对象划分到同一个组(簇)中,使得同一簇内的样本尽可能相似,不同簇之间的样本尽可能不同。与分类任务不同,聚类在训练时不需要样本的标签信息,完全依赖数据自身的内在结构进行分组。
聚类分析在数据科学领域有着广泛的应用:市场细分中将消费者按行为特征分组、图像处理中将像素点聚合成区域、生物信息学中挖掘基因表达模式、推荐系统中发现用户兴趣群组、异常检测中识别偏离正常模式的离群点。可以说,凡是需要"发现数据中的自然分组"的场景,聚类都是首选的探索工具。
核心思想: 物以类聚,人以群分。聚类算法自动发现数据中隐藏的结构化分组信息,无需人工标注。
常见的聚类算法可以分为四大类:基于划分的聚类 (如KMeans,通过迭代优化簇内距离)、基于密度的聚类 (如DBSCAN,通过密度连通性发现任意形状的簇)、层次聚类 (如Agglomerative,自底向上或自顶向下构建聚类树)、以及基于模型的聚类 (如高斯混合模型GMM,假设数据服从概率分布)。
选择聚类算法时需要考虑数据的特性:簇的形状是否为凸形、数据是否包含噪声和离群点、簇的密度是否均匀、是否需要预先指定簇的数量、数据集规模有多大。这些因素直接影响不同算法在特定任务上的表现。下面我们将逐一深入剖析三种最主流、最具代表性的聚类算法。
二、KMeans聚类
2.1 算法原理
KMeans是应用最广泛的划分式聚类算法。它的目标是将n个样本划分到K个簇中,使得每个样本到其所属簇中心的距离平方和(即惯性 Inertia)最小。KMeans的核心假设是:簇是凸形的、各向同性的 ,且所有簇的方差大致相同。
其优化目标函数为:J = ∑₁⁓⁻⁶ ||xᵢ - μ₃(i)||² ,其中μ₃是第k个簇的质心(Centroid),r(i)表示样本i所属的簇编号。这个优化问题在计算上是NP难的,因此KMeans采用贪心的迭代启发式策略来近似求解。
2.2 E-M迭代过程
KMeans算法的本质是期望最大化(Expectation-Maximization, E-M)框架的一个特例。整个迭代过程分为两个交替进行的步骤:
E步骤(分配): 根据当前质心位置,将每个样本分配到距离最近的质心所属的簇。分配规则使用欧氏距离度量。
M步骤(更新): 重新计算每个簇的质心,即该簇中所有样本的均值向量。
算法收敛的判定条件通常有三种:质心位置的变化小于阈值、簇分配不再发生变化、或者达到预设的最大迭代次数。由于KMeans对初始质心敏感,算法通常需要多次运行并选取最优结果。
# KMeans算法的E-M迭代过程手写实现
import numpy as np
from sklearn.metrics import pairwise_distances_argmin
def kmeans_em_manual (X, n_clusters, max_iter=300, tol=1e-4):
"""
手动实现KMeans的E-M迭代过程
Parameters:
-----------
X : array-like, shape (n_samples, n_features)
n_clusters : int, 簇数量K
max_iter : int, 最大迭代次数
tol : float, 收敛阈值
Returns:
--------
centroids : 最终质心
labels : 每个样本的簇标签
inertia : 簇内平方和
"""
# 随机初始化质心(从样本中选择K个)
rng = np.random.RandomState(42)
idx = rng.permutation(X.shape[0])[:n_clusters]
centroids = X[idx].copy()
for i in range (max_iter):
# E步骤:分配每个样本到最近的质心
labels = pairwise_distances_argmin (X, centroids)
# M步骤:重新计算质心
new_centroids = np.zeros_like(centroids)
for k in range (n_clusters):
mask = (labels == k)
if np.any (mask):
new_centroids[k] = X[mask].mean(axis=0)
else :
new_centroids[k] = centroids[k]
# 检查是否收敛
shift = np.sqrt (((new_centroids - centroids) ** 2).sum(axis=1)).max ()
centroids = new_centroids
if shift < tol:
break
# 计算惯性(簇内平方和)
distances = np.zeros (X.shape[0])
for k in range (n_clusters):
mask = (labels == k)
if np.any (mask):
distances[mask] = ((X[mask] - centroids[k]) ** 2).sum(axis=1)
inertia = distances.sum ()
return centroids, labels, inertia
2.3 K值选择方法
KMeans需要预先指定K值,这在实际应用中往往是最困难的环节。以下介绍四种主流的K值选择方法:
肘部法(Elbow Method)
绘制不同K值对应的惯性(Inertia)曲线,选择曲线"拐点"对应的K值。拐点处表明继续增加K值带来的收益递减。
轮廓系数(Silhouette Score)
综合考虑簇内紧密度和簇间分离度,取值[-1, 1],越高越好。选择平均轮廓系数最大的K值。
Calinski-Harabasz指数
又称方差比准则,计算簇间离散度与簇内离散度的比值。该值越大,说明聚类效果越好,簇间分离越明显。
Davies-Bouldin指数
计算每个簇与其最相似簇之间的平均相似度。该值越小越好,最小值通常为0。DB指数对簇的形状更为敏感。
# 四种K值选择方法的完整实现
from sklearn.cluster import KMeans
from sklearn.metrics import silhouette_score, calinski_harabasz_score, davies_bouldin_score
import matplotlib.pyplot as plt
def evaluate_k_values (X, k_range=range (2, 11)):
"""综合评价不同K值的聚类效果"""
results = {'k' : [], 'inertia' : [], 'silhouette' : [],
'calinski_harabasz' : [], 'davies_bouldin' : []}
for k in k_range:
kmeans = KMeans(n_clusters=k, random_state=42, n_init=10)
labels = kmeans.fit_predict (X)
results['k' ].append (k)
results['inertia' ].append (kmeans.inertia_)
results['silhouette' ].append (
silhouette_score (X, labels))
results['calinski_harabasz' ].append (
calinski_harabasz_score (X, labels))
results['davies_bouldin' ].append (
davies_bouldin_score (X, labels))
return results
# 可视化四种评估指标
results = evaluate_k_values (X_scaled)
fig, axes = plt.subplots (2, 2, figsize=(12, 10))
axes[0, 0].plot (results['k' ], results['inertia' ], 'bo-' )
axes[0, 0].set_title ('肘部法 (Inertia)' )
axes[0, 0].set_xlabel ('K值' )
axes[0, 1].plot (results['k' ], results['silhouette' ], 'rs-' )
axes[0, 1].set_title ('轮廓系数 (越高越好)' )
axes[0, 1].set_xlabel ('K值' )
axes[1, 0].plot (results['k' ], results['calinski_harabasz' ], 'g^-' )
axes[1, 0].set_title ('CH指数 (越高越好)' )
axes[1, 0].set_xlabel ('K值' )
axes[1, 1].plot (results['k' ], results['davies_bouldin' ], 'mD-' )
axes[1, 1].set_title ('DB指数 (越低越好)' )
axes[1, 1].set_xlabel ('K值' )
plt.tight_layout ()
plt.show ()
实践建议: 实际项目中应综合多种指标选择K值。肘部法简单直观但有时拐点不明显(称为"平滑肘部"问题),此时应优先参考轮廓系数和CH指数。DB指数对簇的重叠程度更敏感,适合作为辅助验证。
2.4 k-means++ 初始化策略
传统的KMeans随机初始化质心可能导致收敛到局部最优解。k-means++是一种更为智能的初始化方法,其核心思想是:初始质心之间应该尽可能远 。具体算法流程如下:
从数据集中随机选取第一个质心
对于每个样本,计算其到最近已有质心的距离D(x)
以概率正比于D(x)²选择下一个质心(距离越远的样本越有可能被选为新质心)
重复上述步骤直到选出K个质心
k-means++初始化可以显著提高聚类结果的稳定性和质量,是scikit-learn中KMeans的默认初始化方式(init='k-means++')。实际应用中,即使使用k-means++,也建议设置n_init参数(如n_init=10)多次运行取最优。
# k-means++初始化与随机初始化的对比
from sklearn.cluster import KMeans
from sklearn.datasets import make_blobs
# 生成数据集
X, _ = make_blobs (n_samples=1000, n_features=2,
centers=5, cluster_std=1.5, random_state=42)
# k-means++初始化
kmeans_pp = KMeans(n_clusters=5, init='k-means++' ,
n_init=10, random_state=42)
labels_pp = kmeans_pp.fit_predict (X)
inertia_pp = kmeans_pp.inertia_
print (f"k-means++ 最终惯性: {inertia_pp:.2f}" )
# 随机初始化
kmeans_random = KMeans(n_clusters=5, init='random' ,
n_init=1, random_state=42)
labels_rand = kmeans_random.fit_predict (X)
inertia_rand = kmeans_random.inertia_
print (f"随机初始化 最终惯性: {inertia_rand:.2f}" )
# 输出对比
print (f"惯性降低比例: {(1 - inertia_pp/inertia_rand)*100:.1f}%" )
2.5 Mini-Batch K-Means
当数据集规模很大(百万级以上样本)时,标准KMeans的每次迭代都需要计算所有样本到所有质心的距离,计算开销巨大。Mini-Batch K-Means通过每次迭代随机采样一个小批次(mini-batch)来更新质心,显著降低了计算复杂度。
Mini-Batch K-Means的更新规则不同于标准KMeans:它使用学习率来控制历史信息和新批次的权重,公式为c = c * (1 - α) + x * α,其中α是该样本属于该簇的历史次数的倒数。与标准KMeans相比,Mini-Batch版本的收敛速度快1-2个数量级,但最终惯性通常略高(即质量略低)。
# Mini-Batch K-Means vs 标准KMeans 性能对比
from sklearn.cluster import MiniBatchKMeans, KMeans
import time
# 生成大数据集
X_large, _ = make_blobs (n_samples=100000, n_features=10,
centers=20, random_state=42)
# 标准KMeans
start = time.time ()
kmeans = KMeans(n_clusters=20, random_state=42, n_init=3)
kmeans.fit (X_large)
time_standard = time.time () - start
print (f"标准KMeans耗时: {time_standard:.2f}秒" )
# Mini-Batch K-Means
start = time.time ()
mbkmeans = MiniBatchKMeans(n_clusters=20, random_state=42,
batch_size=1024, n_init=3)
mbkmeans.fit (X_large)
time_minibatch = time.time () - start
print (f"Mini-Batch KMeans耗时: {time_minibatch:.2f}秒" )
print (f"加速比: {time_standard/time_minibatch:.1f}x" )
选择建议: 当样本数小于10万时,标准KMeans足够快;样本数超过10万时,优先考慮Mini-Batch K-Means。对于流式数据或在线学习场景,Mini-Batch K-Means是唯一可行的选择。
三、DBSCAN密度聚类
3.1 算法思想与基本概念
DBSCAN(Density-Based Spatial Clustering of Applications with Noise)是一种基于密度的聚类算法。与KMeans不同,它不需要预先指定簇的数量,能够发现任意形状的簇,并且可以自动识别噪声点。DBSCAN的核心思想是:聚类是由密度相连的最大样本集合构成的 。
DBSCAN定义了两个关键参数:
eps(ε): 邻域半径,用于定义某点的邻域范围
minPts: 核心点判定阈值,邻域内至少包含的样本数
基于这两个参数,DBSCAN将数据集中的样本分为三类:
核心点(Core Point): 在半径eps内至少包含minPts个样本(包含自身)
边界点(Border Point): 不是核心点,但落在某个核心点的eps邻域内
噪声点(Noise Point): 既不是核心点也不是边界点
# DBSCAN基本使用与参数探索
from sklearn.cluster import DBSCAN
from sklearn.datasets import make_moons
import numpy as np
# 生成非凸形状数据集 - 两个半圆形
X_moons, _ = make_moons (n_samples=500, noise=0.05, random_state=42)
# DBSCAN聚类
dbscan = DBSCAN(eps=0.3, min_samples=5)
labels = dbscan.fit_predict (X_moons)
# 分析聚类结果
n_clusters = len (set (labels)) - (1 if -1 in labels else 0)
n_noise = list (labels).count (-1)
print (f"发现的簇数量: {n_clusters}" )
print (f"噪声点数量: {n_noise}" )
print (f"噪声比例: {n_noise/len(labels)*100:.1f}%" )
# 核心点、边界点和噪声点的数量统计
core_samples_mask = np.zeros_like (labels, dtype=bool)
core_samples_mask[dbscan.core_sample_indices_] = True
n_core = np.sum (core_samples_mask)
n_border = len (labels) - n_core - n_noise
print (f"核心点: {n_core}, 边界点: {n_border}, 噪声点: {n_noise}" )
3.2 DBSCAN与KMeans的对比
DBSCAN和KMeans代表了两种截然不同的聚类范式。理解它们的区别是选择正确算法的关键:
对比维度 KMeans DBSCAN
簇形状 仅支持凸形(球形)簇 支持任意形状簇(包括非凸、环形、S形)
簇数量 需要预先指定K值 由算法自动发现
噪声处理 所有样本都被分配到一个簇 自动识别并标记噪声点(标签-1)
参数 K值(n_clusters) eps和minPts
密度均匀性 适用于密度均匀的数据 适用于密度变化大的数据
可重复性 稳定(多次运行结果一致) 确定性的(给定参数完全一致)
计算复杂度 O(n·K·d·I) O(n²)(默认),使用KD-Tree可优化到O(nlogn)
高维数据 尚可(但距离度量失效) 较差(维度灾难影响密度定义)
# KMeans vs DBSCAN 在非凸数据上的对比
from sklearn.cluster import KMeans, DBSCAN
from sklearn.datasets import make_circles, make_moons
import matplotlib.pyplot as plt
# 生成环形数据
X_circles, _ = make_circles (n_samples=500, factor=0.5, noise=0.05)
fig, axes = plt.subplots (2, 2, figsize=(10, 10))
# KMeans在环形数据上的失败案例
kmeans = KMeans(n_clusters=2, random_state=42)
labels_km = kmeans.fit_predict (X_circles)
axes[0, 0].scatter (X_circles[:, 0], X_circles[:, 1],
c=labels_km, cmap='viridis' )
axes[0, 0].set_title ('KMeans在环形数据 - 失败' )
# DBSCAN在环形数据上的成功案例
dbscan = DBSCAN(eps=0.15, min_samples=5)
labels_db = dbscan.fit_predict (X_circles)
axes[0, 1].scatter (X_circles[:, 0], X_circles[:, 1],
c=labels_db, cmap='viridis' )
axes[0, 1].set_title ('DBSCAN在环形数据 - 成功' )
# KMeans在半月亮数据上的失败
X_moons, _ = make_moons (n_samples=500, noise=0.05)
labels_km2 = KMeans(n_clusters=2, random_state=42).fit_predict (X_moons)
axes[1, 0].scatter (X_moons[:, 0], X_moons[:, 1],
c=labels_km2, cmap='viridis' )
axes[1, 0].set_title ('KMeans在半月数据 - 失败' )
# DBSCAN在半月亮数据上的成功
labels_db2 = DBSCAN(eps=0.2, min_samples=5).fit_predict (X_moons)
axes[1, 1].scatter (X_moons[:, 0], X_moons[:, 1],
c=labels_db2, cmap='viridis' )
axes[1, 1].set_title ('DBSCAN在半月数据 - 成功' )
plt.tight_layout ()
plt.show ()
3.3 eps参数的确定方法
选择合理的eps是DBSCAN使用的关键。一个常用的启发式方法是绘制k距离图(k-distance graph) :计算每个样本到其第k近邻的距离(k取minPts-1),将所有距离排序后绘制曲线。曲线中的"拐点"对应的距离通常是最合适的eps值。
# k距离图法选择eps参数
from sklearn.neighbors import NearestNeighbors
import numpy as np
import matplotlib.pyplot as plt
def plot_k_distance (X, k=5):
"""绘制k距离图,用于选择DBSCAN的eps参数"""
# 计算每个样本到第k近邻的距离
nbrs = NearestNeighbors (n_neighbors=k).fit (X)
distances, _ = nbrs.kneighbors (X)
# 取第k近的距离(从0开始索引,所以是k-1)
k_dist = np.sort (distances[:, k-1])
plt.figure (figsize=(8, 5))
plt.plot (k_dist)
plt.xlabel ('样本点(按距离排序)' )
plt.ylabel (f'第{k}近邻距离' )
plt.title ('k距离图 - 用于选择eps参数' )
plt.grid (True, alpha=0.3)
plt.show ()
print ("曲线拐点处对应的距离即为推荐的eps值" )
# 使用示例
# plot_k_distance(X_scaled, k=5)
DBSCAN局限性: 当数据集的密度差异非常大时(即存在密度极其不均的簇),单一的eps参数难以同时捕获所有簇。此时可考虑使用OPTICS算法(DBSCAN的改进版本),它不需要显式指定eps。此外,DBSCAN在高维数据上表现不佳,因为在高维空间中距离度量失去区分度(维度灾难)。
四、层次聚类
4.1 凝聚式层次聚类
层次聚类(Hierarchical Clustering)通过构建一个层次化的聚类树(树状图Dendrogram)来组织数据分组关系。最常用的策略是凝聚式(Agglomerative) 层次聚类:自底向上,开始时每个样本自成一簇,然后逐步合并距离最近的簇,直到所有样本合并为一个簇或达到预设的簇数量。
层次聚类的最大优势是:不需要预先指定K值,可以通过树状图直观地观察数据的层次结构,并灵活地选择切割位置来获得不同粒度的聚类结果。
4.2 Linkage策略
凝聚式层次聚类的核心是定义"簇间距离"的计算方式,即linkage准则。不同的linkage策略会导致截然不同的聚类结果:
Ward linkage: 合并使得合并后簇内方差增量最小的两个簇。倾向于产生大小相近的球形簇,对异常值敏感。计算复杂度O(n³)。
Complete linkage(最大链接): 簇间距离定义为两个簇中距离最远的样本对之间的距离。倾向于产生紧凑的簇,对异常值不敏感。
Average linkage(平均链接): 簇间距离定义为两个簇中所有样本对距离的平均值。在Ward和Complete之间取得平衡。
Single linkage(单链接): 簇间距离定义为两个簇中距离最近的样本对之间的距离。可以发现细长形状的簇,但容易出现链式效应。
# 层次聚类 - 不同linkage策略的对比
from sklearn.cluster import AgglomerativeClustering
from sklearn.datasets import make_blobs
import matplotlib.pyplot as plt
# 生成具有层次结构的数据集
X_hier, _ = make_blobs (n_samples=300, n_features=2,
centers=[[0,0],[3,3],[6,0],[9,3]],
cluster_std=0.6, random_state=42)
fig, axes = plt.subplots (2, 2, figsize=(12, 10))
linkage_methods = ['ward' , 'complete' , 'average' , 'single' ]
for ax, method in zip (axes.ravel(), linkage_methods):
model = AgglomerativeClustering (
n_clusters=4, linkage=method)
labels = model.fit_predict (X_hier)
ax.scatter (X_hier[:, 0], X_hier[:, 1], c=labels,
cmap='viridis' , edgecolors='k' , s=40)
ax.set_title (f'Linkage: {method}' )
ax.set_xlabel ('特征1' )
ax.set_ylabel ('特征2' )
plt.tight_layout ()
plt.show ()
4.3 Dendrogram树状图
树状图(Dendrogram)是层次聚类最具特色的可视化工具,它完整展示了一棵聚类树的合并过程。通过观察树状图,可以直观地判断数据的层次结构、选择合理的切割阈值、识别潜在的离群点。树状图的纵轴表示合并时的距离(或相异性),横轴表示样本或簇。
# 绘制Dendrogram树状图
from scipy.cluster.hierarchy import dendrogram, linkage
import matplotlib.pyplot as plt
# 计算链接矩阵
Z = linkage (X_hier, method='ward' )
# 绘制树状图
plt.figure (figsize=(12, 6))
plt.title ('层次聚类树状图 (Dendrogram)' )
plt.xlabel ('样本索引' )
plt.ylabel ('距离 (Ward)' )
# 绘制树状图并用红线标记切割阈值
dn = dendrogram (Z, leaf_rotation=90, leaf_font_size=8)
plt.axhline (y=3.0, color='r' , linestyle='--' ,
label='切割阈值 = 3.0' )
plt.legend ()
plt.tight_layout ()
plt.show ()
# 根据树状图确定的阈值切割得到4个簇
from scipy.cluster.hierarchy import fcluster
clusters = fcluster (Z, t=3.0, criterion='distance' )
print (f"阈值3.0切割得到 {len(set(clusters))} 个簇" )
树状图解读技巧: (1)纵轴距离越大,合并发生的层次越低(越不相似);(2)水平线的高度表示两个簇合并时的距离;(3)长的垂直线条表明该合并具有较强的结构支撑;(4)选择切割阈值时,寻找最长垂直线条未被水平线穿越的位置,这通常对应最自然的簇结构。
4.4 距离矩阵与计算复杂度
层次聚类需要计算样本间的距离矩阵,其空间复杂度为O(n²),这意味着当样本数超过数万时,层次聚类的计算开销会变得难以接受。这在很大程度上限制了层次聚类在大规模数据集上的应用。在scikit-learn中,当设置distance_threshold参数并配合ward linkage时,算法可以利用一些优化技巧来降低计算量。
# 距离矩阵可视化
from scipy.spatial.distance import pdist, squareform
import seaborn as sns
import matplotlib.pyplot as plt
# 计算成对距离矩阵
dist_matrix = squareform (pdist (X_hier[:30]))
plt.figure (figsize=(8, 6))
sns.heatmap (dist_matrix, cmap='viridis' , square=True )
plt.title ('样本间距离矩阵热图(前30个样本)' )
plt.xlabel ('样本索引' )
plt.ylabel ('样本索引' )
plt.show ()
# 从距离矩阵直接构建层次聚类
from sklearn.cluster import AgglomerativeClustering
model_dist = AgglomerativeClustering (
n_clusters=None,
distance_threshold=2.5,
linkage='ward'
)
labels_dist = model_dist.fit_predict (X_hier)
print (f"距离阈值2.5切割得到 {len(set(labels_dist))} 个簇" )
五、聚类评估指标
聚类评估是聚类分析中极具挑战性的环节——因为没有真实标签作为"标准答案"。评估指标分为两类:外部评估 (需要真实标签,用于衡量聚类结果与真实划分的匹配程度)和内部评估 (仅依赖数据本身的特征,衡量簇的分离度和紧密度)。
5.1 外部评估指标
NMI - 标准化互信息
标准化互信息(Normalized Mutual Information)衡量聚类标签与真实标签之间的信息共享程度。取值[0, 1],1表示完全一致,0表示不共享任何信息。NMI对簇的数量不敏感,适合比较不同K值下的聚类质量。
ARI - 调整兰德指数
调整兰德指数(Adjusted Rand Index)基于样本对的一致性来计算。它统计所有样本对中,聚类结果和真实标签在"是否属于同一簇"上保持一致的比例,并进行了随机调整(校正了随机一致性的期望值)。ARI的取值范围[-1, 1],1表示完全一致,0表示随机水平,负值表示比随机更差。
# 外部评估指标计算
from sklearn.metrics import normalized_mutual_info_score
from sklearn.metrics import adjusted_rand_score
from sklearn.metrics import homogeneity_score
from sklearn.metrics import completeness_score
from sklearn.metrics import v_measure_score
def evaluate_external (labels_true, labels_pred):
"""计算所有外部聚类评估指标"""
metrics = {
'NMI' : normalized_mutual_info_score (labels_true, labels_pred),
'ARI' : adjusted_rand_score (labels_true, labels_pred),
'同质性 (Homogeneity)' : homogeneity_score (labels_true, labels_pred),
'完整性 (Completeness)' : completeness_score (labels_true, labels_pred),
'V-Measure' : v_measure_score (labels_true, labels_pred),
}
for name, value in metrics.items ():
print (f"{name}: {value:.4f}" )
return metrics
# 使用示例:评估KMeans在已知标签数据上的表现
from sklearn.datasets import load_iris
from sklearn.cluster import KMeans
iris = load_iris ()
X_iris, y_iris = iris.data, iris.target
kmeans = KMeans(n_clusters=3, random_state=42, n_init=10)
y_pred = kmeans.fit_predict (X_iris)
print ("=== KMeans在Iris数据集上的外部评估 ===" )
metrics = evaluate_external (y_iris, y_pred)
关于同质性、完整性与V-Measure: 同质性要求每个簇只包含单一类别的样本;完整性要求同一类别的所有样本都被分到同一个簇中。V-Measure是同质性和完整性的调和平均数,可以理解为聚类结果的"综合质量"评分。
5.2 内部评估指标
内部评估指标不依赖于真实标签,是实际应用中最常用的评估方式:
Silhouette轮廓分析图
轮廓系数(Silhouette Coefficient)结合了簇内紧密度(a:样本到同簇其他样本的平均距离)和簇间分离度(b:样本到最近其他簇所有样本的平均距离)。每个样本的轮廓系数为(b - a) / max(a, b)。
轮廓分析图将每个簇的轮廓系数绘制在同一张图上,可以直观地识别簇的紧密度、宽度是否均衡、是否存在异常样本。如果一个簇的轮廓系数明显低于其他簇,或者出现负值样本,说明聚类质量不佳。
# Silhouette轮廓分析图
from sklearn.metrics import silhouette_samples, silhouette_score
import matplotlib.pyplot as plt
import numpy as np
def plot_silhouette_analysis (X, labels):
"""绘制轮廓分析图"""
n_clusters = len(set(labels)) - (1 if -1 in labels else 0)
n_samples = len(X)
# 计算每个样本的轮廓系数
silhouette_vals = silhouette_samples (X, labels)
avg_score = np.mean (silhouette_vals)
fig, ax = plt.subplots (figsize=(10, 6))
y_lower = 10
for i in range (n_clusters):
# 获取第i个簇的样本
cluster_vals = silhouette_vals[labels == i]
cluster_vals.sort ()
size = len (cluster_vals)
y_upper = y_lower + size
color = plt.cm .nipy_spectral(float (i) / n_clusters)
ax.fill_betweenx (np.arange (y_lower, y_upper),
0, cluster_vals,
facecolor=color, alpha=0.7)
ax.text (-0.05, y_lower + 0.5 * size, f'簇 {i}' )
y_lower = y_upper + 10
# 平均轮廓系数线
ax.axvline (x=avg_score, color='red' , linestyle='--' )
ax.set_xlabel ('轮廓系数值' )
ax.set_ylabel ('簇' )
ax.set_title (f'轮廓分析图 (平均轮廓系数 = {avg_score:.3f})' )
plt.show ()
return avg_score
# 使用示例
kmeans_3 = KMeans(n_clusters=3, random_state=42, n_init=10)
labels_3 = kmeans_3.fit_predict (X_scaled)
score = plot_silhouette_analysis (X_scaled, labels_3)
print (f"平均轮廓系数: {score:.4f}" )
轮廓系数的经验阈值: 大于0.7表示强结构(聚类效果优秀);0.5-0.7表示合理结构(可用但可优化);0.25-0.5表示弱结构(需要谨慎分析);小于0.25表示没有明显的结构(数据可能不适合聚类)。
六、实际应用场景
6.1 客户分群(Customer Segmentation)
客户分群是聚类分析最经典的应用场景。通过对用户的消费行为、活跃度、偏好等特征的聚类,企业可以将用户分为高价值客户、沉睡客户、流失风险客户等群组,从而制定差异化的营销策略。KMeans是客户分群中最常用的算法,配合PCA降维可以将高维行为数据可视化。
# 客户分群实战 - RFM模型+KMeans
import pandas as pd
from sklearn.cluster import KMeans
from sklearn.preprocessing import StandardScaler
# RFM特征:Recency(最近消费时间), Frequency(消费频率), Monetary(消费金额)
# 模拟客户数据
rfm_data = pd.DataFrame({
'recency' : [5, 30, 90, 2, 60, 120, 15, 45, 180, 7],
'frequency' : [50, 12, 3, 80, 8, 1, 30, 15, 2, 40],
'monetary' : [5000, 1200, 300, 8000, 600, 100, 3000, 1500, 200, 4000]
})
# 标准化
scaler = StandardScaler ()
rfm_scaled = scaler.fit_transform (rfm_data)
# 客户分群
kmeans_rfm = KMeans(n_clusters=3, random_state=42, n_init=10)
rfm_data['segment' ] = kmeans_rfm.fit_predict (rfm_scaled)
# 分析各群特征
rfm_analysis = rfm_data.groupby ('segment' ).agg ({
'recency' : 'mean' ,
'frequency' : 'mean' ,
'monetary' : 'mean' ,
'segment' : 'count'
}).rename (columns={'segment' : 'count' })
print ("=== 客户分群结果分析 ===" )
print (rfm_analysis)
6.2 图像分割(Image Segmentation)
在图像处理中,聚类算法可以将像素点按其颜色值(RGB)或空间位置进行分组,从而实现图像分割。每个像素点被视为三维空间(R, G, B)中的一个点,聚类后同一簇的像素赋予相同的颜色值,从而达到分割或压缩图像的效果。
# 使用KMeans进行图像颜色量化(图像分割)
from sklearn.cluster import KMeans
import numpy as np
from PIL import Image
def compress_image_colors (image_path, n_colors=16):
"""使用KMeans对图像进行颜色量化"""
# 加载图像并转换为RGB数组
img = Image.open (image_path).convert ('RGB' )
img_array = np.array (img)
h, w, c = img_array.shape
# 重塑为像素列表 (H*W, 3)
pixels = img_array.reshape(-1, 3)
# KMeans聚类
kmeans = KMeans(n_clusters=n_colors, random_state=42, n_init=3)
labels = kmeans.fit_predict (pixels)
# 用质心颜色替换每个像素
compressed = kmeans.cluster_centers_[labels].reshape(h, w, 3)
compressed = np.clip (compressed, 0, 255).astype(np.uint8)
return Image.fromarray (compressed)
# 使用示例(需替换为实际图像路径)
# compressed_img = compress_image_colors('input.jpg', n_colors=8)
# compressed_img.save('output_quantized.jpg')
print ("图像颜色量化完成: 原图颜色数 → 8种主色" )
6.3 文档聚类与主题发现
在文本挖掘中,聚类可以自动发现文档的潜在主题结构。通常的流程是:首先使用TF-IDF或Word2Vec将文档转换为向量,然后应用聚类算法将相似文档分组。层次聚类特别适合文档聚类,因为树状图可以直观地展示主题的层次结构。
# 文档聚类 - 新闻主题发现
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.cluster import KMeans
# 模拟文档数据
documents = [
"华为发布新款Mate系列手机支持卫星通信" ,
"苹果iPhone销量超预期股价上涨" ,
"央行宣布降准释放流动性支持实体经济" ,
"A股三大指数收涨新能源板块领涨" ,
"气候变暖导致北极冰层加速融化" ,
"新能源车企公布上月交付数据" ,
"环保组织呼吁减少塑料使用保护海洋" ,
"基金公司布局科技创新主题ETF" ,
]
# TF-IDF向量化
vectorizer = TfidfVectorizer (token_pattern=r'\w+' )
X_tfidf = vectorizer.fit_transform (documents)
# KMeans聚类发现主题
kmeans_doc = KMeans(n_clusters=3, random_state=42, n_init=10)
doc_labels = kmeans_doc.fit_predict (X_tfidf)
# 输出每个主题下的文档
for cluster_id in sorted (set (doc_labels)):
print (f"\n主题簇 {cluster_id}:" )
idx = [i for i, l in enumerate (doc_labels) if l == cluster_id]
for i in idx:
print (f" - {documents[i]}" )
6.4 异常检测(Anomaly Detection)
DBSCAN天然支持异常检测功能——被标记为-1(噪声点)的样本即被视为异常。这种方法特别适用于检测那些远离正常数据密集区域的离群点。与传统的统计方法(如Z-score、IQR)相比,DBSCAN的异常检测具有以下优势:
无需假设数据服从正态分布
可以检测出局部异常(偏离局部密度模式而非全局分布)
能够处理多模态数据中的异常
# DBSCAN异常检测
from sklearn.cluster import DBSCAN
import numpy as np
def detect_anomalies_dbscan (X, eps=0.5, min_samples=5):
"""使用DBSCAN进行异常检测"""
model = DBSCAN (eps=eps, min_samples=min_samples)
labels = model.fit_predict (X)
n_anomalies = np.sum (labels == -1)
anomaly_ratio = n_anomalies / len (labels)
print (f"检测到 {n_anomalies} 个异常点" )
print (f"异常比例: {anomaly_ratio:.2%}" )
return labels # -1表示异常
# 生成带有异常点的数据
rng = np.random.RandomState(42)
X_normal = rng.randn (200, 2) * 0.5
X_anomaly = rng.uniform (low=-3, high=3, size=(10, 2))
X_with_anomalies = np.vstack ([X_normal, X_anomaly])
labels = detect_anomalies_dbscan (X_with_anomalies, eps=0.3, min_samples=5)
七、聚类方法对比总结
维度 KMeans DBSCAN 层次聚类
簇形状 凸形/球形 任意形状 依赖linkage策略
需指定K值 是 否 否(切割时需指定)
噪声处理 不支持 原生支持 不支持
可重复性 依赖初始化 确定性的 确定性的
参数数量 少(K值) 中等(eps, minPts) 少(linkage, n_clusters)
可解释性 中等 高 很高(树状图)
计算复杂度 O(n·K·d·I) O(n²) ~ O(nlogn) O(n³) ~ O(n²)
大规模数据 优秀(含Mini-Batch) 中等 差
高维数据 一般 差 一般
Python实现 sklearn.cluster.KMeans sklearn.cluster.DBSCAN sklearn.cluster.AgglomerativeClustering
算法选择速查:
数据是球形簇、簇大小均匀、无噪声 → KMeans
簇形状复杂(环形、S形)、需要自动识别噪声 → DBSCAN
需要可视化层次结构、小数据集、不关心K值 → 层次聚类
百万级样本 → Mini-Batch KMeans
密度不均匀、eps难以选择 → OPTICS (DBSCAN的进阶版)
需要概率输出的柔性聚类 → Gaussian Mixture Model (GMM)
完整实战流程
下面是一个端到端的聚类分析pipeline,展示了从数据预处理到模型评估的完整流程:
# 完整的聚类分析pipeline
import numpy as np
import pandas as pd
from sklearn.preprocessing import StandardScaler
from sklearn.decomposition import PCA
from sklearn.cluster import KMeans, DBSCAN, AgglomerativeClustering
from sklearn.metrics import silhouette_score
class ClusteringPipeline :
"""聚类分析完整pipeline"""
def __init__ (self, data):
self.data = data
self.scaler = StandardScaler ()
self.scaled_data = self.scaler.fit_transform (data)
self.results = {}
def run_kmeans (self, n_clusters=range (2, 8)):
"""自动寻找最佳K值并运行KMeans"""
best_k, best_score = 2, -1
for k in n_clusters:
model = KMeans (n_clusters=k, random_state=42, n_init=10)
labels = model.fit_predict (self.scaled_data)
score = silhouette_score (self.scaled_data, labels)
if score > best_score:
best_k, best_score = k, score
model = KMeans (n_clusters=best_k, random_state=42, n_init=10)
self.results['kmeans' ] = {
'labels' : model.fit_predict (self.scaled_data),
'k' : best_k,
'silhouette' : best_score,
'model' : model
}
print (f"KMeans: K={best_k}, 轮廓系数={best_score:.4f}" )
def run_dbscan (self, eps=0.5, min_samples=5):
model = DBSCAN (eps=eps, min_samples=min_samples)
labels = model.fit_predict (self.scaled_data)
n_clusters = len (set (labels)) - (1 if -1 in labels else 0)
n_noise = list (labels).count (-1)
# 过滤噪声点后计算轮廓系数
mask = labels != -1
if np.sum (mask) > 1 and n_clusters > 1:
score = silhouette_score (self.scaled_data[mask], labels[mask])
else :
score = -1
self.results['dbscan' ] = {
'labels' : labels,
'n_clusters' : n_clusters,
'n_noise' : n_noise,
'silhouette' : score,
'model' : model
}
print (f"DBSCAN: 簇={n_clusters}, 噪声={n_noise}, 轮廓系数={score:.4f}" )
def run_hierarchical (self, n_clusters=3, linkage='ward' ):
model = AgglomerativeClustering (
n_clusters=n_clusters, linkage=linkage)
labels = model.fit_predict (self.scaled_data)
score = silhouette_score (self.scaled_data, labels)
self.results['hierarchical' ] = {
'labels' : labels,
'n_clusters' : n_clusters,
'silhouette' : score,
'model' : model
}
print (f"层次聚类: K={n_clusters}, 轮廓系数={score:.4f}" )
def summary (self):
"""输出所有聚类结果的对比摘要"""
print ("\n=== 聚类结果对比 ===" )
for method, res in self.results.items ():
print (f"{method}: 轮廓系数={res['silhouette']:.4f}" )
# 在Iris数据集上运行pipeline
pipeline = ClusteringPipeline (iris.data)
pipeline.run_kmeans (n_clusters=range (2, 8))
pipeline.run_dbscan (eps=0.5, min_samples=5)
pipeline.run_hierarchical (n_clusters=3, linkage='ward' )
pipeline.summary ()
学习建议: 聚类分析是数据科学中最富探索性的技术之一。建议读者在UCI机器学习库或Kaggle上寻找无标签数据集进行练习,重点培养以下能力:(1)观察数据分布特征选择合适的聚类算法;(2)运用多种评估指标综合判断聚类质量;(3)将聚类结果与业务知识结合形成可落地的洞察。聚类分析的真正价值不在于算法的技术细节,而在于从数据中发现有意义的、可解释的结构化信息。