K-Means 算法 Python 3.12 实战:3 种 K 值选择方法对比与可视化实现

当数据科学家面对无标签数据集时,K-Means 聚类往往是第一个跃入脑海的解决方案。这个看似简单的算法却隐藏着一个令人头疼的难题:如何确定最佳的 K 值?选择不当的簇数可能导致模型完全偏离数据的真实结构。本文将带你深入探索三种主流的 K 值选择方法,并提供可直接运行的 Python 3.12 实现代码。

1. K 值选择的核心挑战

K-Means 算法要求我们在运行前就指定簇的数量 K,但这个看似简单的参数却直接影响着整个聚类结果的质量。选择太小的 K 值会导致不同类别的数据被强行合并,而选择过大的 K 值则会造成过度分割,将本应属于同一类的数据分散到多个簇中。

在实际项目中,我经常遇到这样的情况:同一个数据集,不同团队成员会给出完全不同的 K 值建议。这种主观性正是我们需要量化方法来解决的问题。下面这三种方法各有利弊,适用于不同场景:

  • 肘部法则 :直观易懂,适合数据分布有明显"拐点"的情况
  • 轮廓系数 :量化评估每个数据点的归属质量,适合中等规模数据集
  • Gap Statistic :通过比较实际数据与随机分布的差异,适合复杂分布的数据

提示:没有一种方法是万能的,实际应用中建议结合多种方法的结果综合判断。

2. 数据准备与基础实现

在深入探讨 K 值选择方法前,让我们先建立一个可重复的实验环境。我们将使用 scikit-learn 的 make_blobs 函数生成模拟数据,并实现一个基础的 K-Means 聚类流程。

import numpy as np
import matplotlib.pyplot as plt
from sklearn.datasets import make_blobs
from sklearn.cluster import KMeans
from sklearn.metrics import silhouette_score
from sklearn.preprocessing import StandardScaler

# 生成模拟数据
X, y = make_blobs(n_samples=500, centers=4, cluster_std=1.2, random_state=42)
X = StandardScaler().fit_transform(X)  # 标准化数据

# 基础K-Means实现
kmeans = KMeans(n_clusters=4, init='k-means++', n_init=10, random_state=42)
kmeans.fit(X)
labels = kmeans.labels_
centers = kmeans.cluster_centers_

# 可视化结果
plt.figure(figsize=(10, 6))
plt.scatter(X[:, 0], X[:, 1], c=labels, cmap='viridis', alpha=0.7)
plt.scatter(centers[:, 0], centers[:, 1], c='red', s=200, marker='X')
plt.title('基础K-Means聚类结果')
plt.xlabel('特征1')
plt.ylabel('特征2')
plt.grid(True)
plt.show()

这段代码生成了一个包含4个明显簇的数据集,并展示了基础聚类结果。但现实中,我们很少能如此清晰地知道数据的真实簇数,这就是为什么需要系统的方法来确定 K 值。

3. 肘部法则实现与解读

肘部法则(Elbow Method)是最直观的 K 值选择方法,它通过观察不同 K 值下模型的惯性(Inertia,即样本到其最近聚类中心的平方距离和)变化来确定最佳簇数。

# 肘部法则实现
inertias = []
k_range = range(1, 11)

for k in k_range:
    kmeans = KMeans(n_clusters=k, random_state=42)
    kmeans.fit(X)
    inertias.append(kmeans.inertia_)

# 绘制肘部曲线
plt.figure(figsize=(10, 6))
plt.plot(k_range, inertias, 'bo-')
plt.xlabel('簇数量 K')
plt.ylabel('惯性(Inertia)')
plt.title('肘部法则:寻找最佳K值')
plt.xticks(k_range)
plt.grid(True)
plt.show()

解读肘部曲线时,我们需要寻找那个"肘点"——惯性下降速度明显变缓的点。在实际分析中,这个点可能不像理论中那么明显,这时可以考虑以下辅助判断方法:

  • 百分比变化法 :计算惯性变化的百分比,选择变化率显著下降的点
  • 角度法 :计算连续点之间连线的角度,选择角度最大的点

下表展示了不同 K 值下的惯性值及其变化率:

K值 惯性值 变化量 变化百分比
1 980.2 - -
2 720.5 259.7 26.5%
3 480.3 240.2 33.3%
4 310.8 169.5 35.3%
5 290.1 20.7 6.7%

从表中可以看出,K=4 到 K=5 时变化百分比显著下降,这支持了 K=4 是最佳选择的结论。

4. 轮廓系数分析实战

轮廓系数(Silhouette Coefficient)提供了另一种评估聚类质量的视角,它同时考虑了簇内的凝聚度和簇间的分离度。对于每个样本,轮廓系数计算如下:

s(i) = (b(i) - a(i)) / max(a(i), b(i))

其中:

  • a(i) 是样本i到同簇其他样本的平均距离(簇内不相似度)
  • b(i) 是样本i到其他各簇样本的最小平均距离(簇间不相似度)
# 轮廓系数分析实现
silhouette_scores = []
k_range = range(2, 11)  # 轮廓系数要求至少2个簇

for k in k_range:
    kmeans = KMeans(n_clusters=k, random_state=42)
    labels = kmeans.fit_predict(X)
    score = silhouette_score(X, labels)
    silhouette_scores.append(score)
    print(f"K={k}时,轮廓系数={score:.4f}")

# 绘制轮廓系数曲线
plt.figure(figsize=(10, 6))
plt.plot(k_range, silhouette_scores, 'go-')
plt.xlabel('簇数量 K')
plt.ylabel('平均轮廓系数')
plt.title('轮廓系数分析:寻找最佳K值')
plt.xticks(k_range)
plt.grid(True)
plt.show()

轮廓系数的取值范围在[-1, 1]之间,越接近1表示聚类效果越好。实际应用中,我们选择轮廓系数最大的 K 值。值得注意的是:

  • 轮廓系数计算量较大,不适合超大规模数据集
  • 当数据分布非常密集或重叠时,轮廓系数的区分度可能不高
  • 可以结合样本级别的轮廓系数分析,识别聚类不佳的特定样本

5. Gap Statistic 方法详解

Gap Statistic 是一种更复杂但理论上更严谨的方法,它通过比较实际数据的聚类质量与参考分布(通常采用均匀分布)下期望的聚类质量来确定最佳 K 值。

# Gap Statistic实现
def compute_gap_statistic(X, k_max=10, n_refs=20, random_state=42):
    gaps = []
    sdk = []
    
    # 计算实际数据的惯性
    actual_inertia = []
    for k in range(1, k_max+1):
        kmeans = KMeans(n_clusters=k, random_state=random_state)
        kmeans.fit(X)
        actual_inertia.append(kmeans.inertia_)
    
    # 生成参考分布并计算期望惯性
    ref_inertias = np.zeros((n_refs, k_max))
    for i in range(n_refs):
        # 生成均匀分布参考数据
        reference = np.random.random_sample(size=X.shape)
        for k in range(1, k_max+1):
            kmeans = KMeans(n_clusters=k, random_state=random_state)
            kmeans.fit(reference)
            ref_inertias[i, k-1] = kmeans.inertia_
    
    # 计算Gap统计量
    gap = np.log(np.mean(ref_inertias, axis=0)) - np.log(actual_inertia)
    sk = np.sqrt(1 + 1/n_refs) * np.std(np.log(ref_inertias), axis=0)
    
    return gap, sk

# 计算并可视化Gap Statistic
k_max = 10
gap, sk = compute_gap_statistic(X, k_max=k_max)

plt.figure(figsize=(10, 6))
plt.plot(range(1, k_max+1), gap, 'bo-')
plt.errorbar(range(1, k_max+1), gap, yerr=sk, fmt='o', capsize=5)
plt.xlabel('簇数量 K')
plt.ylabel('Gap值')
plt.title('Gap Statistic分析')
plt.xticks(range(1, k_max+1))
plt.grid(True)
plt.show()

Gap Statistic 的选择规则是:选择最小的 K 使得 Gap(K) ≥ Gap(K+1) - s_{K+1}。这种方法特别适合以下场景:

  • 数据分布不均匀或簇形状不规则
  • 簇大小差异较大
  • 需要更客观的统计依据支持决策

6. 综合对比与实战建议

三种方法各有优劣,下表总结了它们的主要特点和适用场景:

方法 优点 缺点 适用场景
肘部法则 直观易懂,计算简单 主观性强,拐点不明显 数据分布有明显"肘点"时
轮廓系数 量化评估,范围明确 计算量大,对重叠簇敏感 中等规模,簇区分度较高
Gap Statistic 统计严谨,理论基础强 实现复杂,计算成本高 复杂分布,需要客观依据

在实际项目中,我通常会采用以下工作流程:

  1. 初步探索 :使用肘部法则快速获取 K 值的可能范围
  2. 精细评估 :在候选 K 值范围内计算轮廓系数
  3. 验证确认 :对关键决策使用 Gap Statistic 进行验证
  4. 业务对齐 :最终选择需要结合业务理解和实际需求
# 综合可视化函数
def plot_cluster_results(X, k, method_name):
    kmeans = KMeans(n_clusters=k, random_state=42)
    labels = kmeans.fit_predict(X)
    centers = kmeans.cluster_centers_
    
    plt.figure(figsize=(10, 6))
    plt.scatter(X[:, 0], X[:, 1], c=labels, cmap='viridis', alpha=0.7)
    plt.scatter(centers[:, 0], centers[:, 1], c='red', s=200, marker='X')
    plt.title(f'{method_name}推荐K={k}的聚类结果')
    plt.xlabel('特征1')
    plt.ylabel('特征2')
    plt.grid(True)
    plt.show()

# 展示三种方法推荐的K值结果
plot_cluster_results(X, 4, '肘部法则')
plot_cluster_results(X, 4, '轮廓系数') 
plot_cluster_results(X, 4, 'Gap Statistic')

在真实业务场景中,K 值的选择往往不是纯技术决策。例如在客户细分项目中,即使技术指标支持 K=5,但业务部门可能更倾向于 K=4,因为这与他们现有的市场策略更匹配。好的数据科学家应该能够平衡技术指标与业务需求。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐