简介:这份资源是面向数据科学初学者与算法实践者的K-means聚类可视化Python代码包,聚焦聚类分析中簇数选择这一核心难点,通过肘部法则与轮廓系数两条路径帮助读者判断最优K值,适用于课程作业、项目调参与算法原理学习。压缩包共27个文件,约10.04MB,其中7个py脚本承载聚类主流程与示例代码,18张png图表记录不同参数下的聚类效果与评估曲线,另附txt依赖清单与md说明文档,便于快速理解目录结构与运行方式。目前已有165人学习下载。读者可获得一套可直接运行的完整演示程序,涵盖数据生成、模型训练、肘部法则与轮廓系数对比分析及多维度可视化输出,既能对照图表理解聚类质量评价指标,也能将脚本迁移到自己的数据集上做参数调优与结果展示。
1. K-means 聚类可视化:肘部法则和轮廓系数到底怎么配合用
手上有一批用户行为数据,几百到几万行,想分组但不知道分几组——这是 K-means 最常见的落地场景。K-means 本身不复杂,真正让人纠结的是 K 值:分 3 类还是 5 类?拍脑袋定 K 是新手最容易翻车的地方。肘部法则和轮廓系数就是解决这个问题的两把尺子,前者看簇内误差随 K 变化的拐点,后者看每个点跟自己簇的贴合度跟别的簇的分离度。把这两个指标画在同一张图里对照,K 值就不再是玄学。这篇面向用 Python 做数据分析的从业者,从数据生成、K 值评估到可视化落地,给一套能直接抄的代码,也把参数怎么调、图怎么读、哪里容易踩坑讲清楚。环境用 scikit-learn + matplotlib,Python 3.8 以上都能跑。
2. 肘部法则和轮廓系数的计算原理与选型逻辑
2.1 肘部法则看的是什么:SSE 拐点为什么能定 K
K-means 的目标函数是最小化簇内平方误差和(SSE,也叫 inertia)。K 从 1 开始增大,每个簇会更紧凑,SSE 单调下降。但下降速度不是均匀的:K 小于真实簇数时,每多分一类,SSE 掉得很快;K 超过真实簇数后,再增加 K 只是把本来该在一起的簇硬拆开,SSE 下降变缓。把 K 作为横轴、SSE 作为纵轴画出来,曲线会在某个位置出现明显的「拐弯」,这个拐点对应的 K 就是候选值。
这里有个容易误解的点:肘部法则给的是候选区间,不是精确值。实际数据里拐点经常是圆滑的,K=3 和 K=4 的 SSE 差距可能只有几个百分点,光看肘部图定不下来。所以它必须跟轮廓系数配合,一个看簇内紧致度的边际收益,一个看簇间分离质量。
计算 SSE 在 sklearn 里就是KMeans(n_clusters=k).fit(X).inertia_,不需要自己写。要注意的是n_init参数:老版本默认 10,新版本(1.4 以后)默认改成"auto"。K-means 对初始质心敏感,n_init表示用不同随机种子跑几轮取最优,设太小可能落到局部最优,SSE 偏高,肘部图会失真。我一般显式写n_init=10,保证结果稳定可复现。
2.2 轮廓系数怎么算:一个点跟自己簇有多亲、跟别的簇有多远
轮廓系数(Silhouette Coefficient)对每个样本点算一个值,公式是s = (b - a) / max(a, b)。其中 a 是该点到同簇其他点的平均距离(凝聚度),b 是该点到最近邻簇所有点的平均距离(分离度)。s 的取值范围是 -1 到 1:
- 接近 1:点离自己簇很近,离别的簇很远,分得好。
- 接近 0:点在两个簇的边界上,归属模糊。
- 小于 0:点可能被分错了簇,离别的簇反而更近。
把所有点的 s 求平均,就是整体轮廓系数。对每个候选 K 算一遍,取平均值最大的 K。轮廓系数的计算复杂度是 O(n²),数据量大时(比如超过 1 万行)会明显变慢,这时候可以抽样算,或者改用silhouette_score的sample_size参数。
轮廓系数有个陷阱:它偏爱凸形、大小均匀的簇。如果你的数据是长条形或者密度差异很大,轮廓系数可能给出偏小的 K,这时候要结合业务判断,别死盯指标。另外 K=1 时轮廓系数无定义,评估要从 K=2 开始。
2.3 两个指标怎么配合:先看肘部定范围,再用轮廓系数定值
实操里的顺序是这样的:先跑 K=1 到 K=10 的 SSE,画出肘部图,圈定 2 到 3 个候选 K;再对这几个候选 K 算轮廓系数,选平均值最高的那个。如果两个指标指向同一个 K,基本可以定;如果冲突,优先信轮廓系数,同时回头看数据的业务含义——比如电商用户分群,分成 4 组和 5 组在运营策略上有没有本质区别,这比指标差 0.02 更重要。
下面这张表是我常用的判断对照,遇到指标打架时按这个逻辑走:
| 情况 | 肘部法则表现 | 轮廓系数表现 | 建议 |
|---|---|---|---|
| 两指标一致 | 拐点清晰 | 峰值在同一 K | 直接采用 |
| 肘部模糊 | 曲线平滑无拐点 | 峰值明确 | 以轮廓系数为准 |
| 轮廓系数偏低 | 拐点清晰 | 所有 K 都低于 0.3 | 数据可能不适合 K-means,考虑 DBSCAN |
| 大 K 占优 | 拐点在 3 | 峰值在 8 | 检查是否有离群点拉高分离度 |
3. 用 Python 跑通 K 值评估:从造数据到双指标曲线
3.1 环境准备与依赖安装
先把库装齐。scikit-learn 负责聚类和指标,matplotlib 负责画图,numpy 负责数据生成。如果你还没配好 Python 环境,用 pip 一条命令搞定:
pip install scikit-learn matplotlib numpy如果下载慢,可以指定国内镜像源,比如清华源-i https://pypi.tuna.tsinghua.edu.cn/simple。装完在终端跑python -c "import sklearn; print(sklearn.__version__)"确认版本,1.0 以上都支持本文代码。用 VS Code 或 PyCharm 的话,记得把解释器切到装了这些库的那个环境,否则会报 ModuleNotFoundError。
3.2 生成带结构的测试数据
为了让你能直观看到肘部拐点和轮廓峰值,我用make_blobs造一批有明确簇结构的数据,再故意加一点噪声,模拟真实场景:
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 # 固定随机种子,保证每次跑结果一致 np.random.seed(42) # 生成 4 个簇,每簇 300 个点,簇内标准差 0.8 X, y_true = make_blobs( n_samples=1200, centers=4, cluster_std=0.8, random_state=42 ) # 加入 5% 的均匀噪声点,模拟离群值 n_noise = 60 noise = np.random.uniform( low=X[:, 0].min(), high=X[:, 0].max(), size=(n_noise, 2) ) X = np.vstack([X, noise]) print("数据形状:", X.shape)make_blobs的centers=4表示真实簇数是 4,cluster_std控制簇的松散程度,值越大簇越重叠、越难分。加噪声是为了让你看到离群点对轮廓系数的影响——真实数据里几乎不可能没有噪声。random_state=42是固定种子,你跑出来的图应该跟我描述的一致。
3.3 循环计算 SSE 和轮廓系数
核心就一个循环,K 从 2 遍历到 10,每个 K 训练一次模型,同时记录 inertia 和轮廓系数:
k_range = range(2, 11) sse_list = [] sil_list = [] for k in k_range: # n_init=10 表示用 10 组不同初始质心,取 SSE 最小的那次 km = KMeans(n_clusters=k, n_init=10, random_state=42) labels = km.fit_predict(X) sse_list.append(km.inertia_) # 簇内平方误差和 sil_list.append(silhouette_score(X, labels)) # 整体轮廓系数 # 打印结果对照 for k, sse, sil in zip(k_range, sse_list, sil_list): print(f"K={k:2d} SSE={sse:9.2f} 轮廓系数={sil:.4f}")fit_predict一步完成训练和打标签。inertia_是 sklearn 训练后自动存的属性,不用额外算。silhouette_score接收原始数据和标签,返回平均值。注意轮廓系数在 K=2 到 10 都要算,不能只算候选值,因为峰值可能出现在你没想到的位置。
跑完你会看到类似这样的输出(数值因数据而异):K=2 时 SSE 很大,K=3 到 4 之间 SSE 掉得最猛,K=5 以后每次下降的幅度明显变小;轮廓系数通常在 K=4 附近达到峰值,因为真实簇数就是 4。噪声点会拉低整体轮廓系数,但不影响峰值位置。
3.4 把肘部图和轮廓系数图画在一起
单看数字不够直观,画成双轴图一眼就能定 K:
fig, ax1 = plt.subplots(figsize=(9, 5)) # 左轴:SSE 肘部曲线 ax1.plot(k_range, sse_list, 'o-', color='#2c7fb8', label='SSE (inertia)') ax1.set_xlabel('K (聚类数)', fontsize=12) ax1.set_ylabel('SSE', color='#2c7fb8', fontsize=12) ax1.tick_params(axis='y', labelcolor='#2c7fb8') # 右轴:轮廓系数曲线 ax2 = ax1.twinx() ax2.plot(k_range, sil_list, 's--', color='#d95f0e', label='轮廓系数') ax2.set_ylabel('轮廓系数', color='#d95f0e', fontsize=12) ax2.tick_params(axis='y', labelcolor='#d95f0e') # 标出轮廓系数最大的 K best_k = list(k_range)[int(np.argmax(sil_list))] ax2.axvline(x=best_k, color='gray', linestyle=':', alpha=0.7) ax2.annotate(f'最佳 K={best_k}', xy=(best_k, max(sil_list)), xytext=(best_k + 0.5, max(sil_list)), arrowprops=dict(arrowstyle='->', color='gray')) plt.title('肘部法则与轮廓系数联合定 K', fontsize=13) fig.tight_layout() plt.savefig('k_selection.png', dpi=150) plt.show()twinx()创建共享横轴的第二个纵轴,两条曲线叠在一张图上。argmax找轮廓系数峰值对应的 K,用虚线标出来。dpi=150保证保存的图够清晰,直接放进报告没问题。如果你遇到横坐标太密集的情况,把k_range改成range(2, 11, 1)并设置ax1.set_xticks(list(k_range))强制显示每个整数刻度。
3.5 用最优 K 做最终聚类并可视化分群结果
定下 K 之后,用这个 K 重新训练,把分群结果画成散点图:
best_km = KMeans(n_clusters=best_k, n_init=10, random_state=42) final_labels = best_km.fit_predict(X) plt.figure(figsize=(8, 6)) scatter = plt.scatter(X[:, 0], X[:, 1], c=final_labels, cmap='viridis', s=20, alpha=0.7) # 画出质心 plt.scatter(best_km.cluster_centers_[:, 0], best_km.cluster_centers_[:, 1], c='red', marker='X', s=200, edgecolors='black', label='质心') plt.colorbar(scatter, label='簇编号') plt.legend() plt.title(f'K-means 聚类结果 (K={best_k})') plt.savefig('cluster_result.png', dpi=150) plt.show()cluster_centers_是训练后的质心坐标,用红色 X 标出来方便看每个簇的中心位置。alpha=0.7让点半透明,重叠区域能看出密度。如果某些簇明显混在一起,说明 K 偏大或者数据本身不适合 K-means,这时候该考虑 DBSCAN 这类基于密度的算法。
4. 避坑与排查:K-means 评估里最容易翻车的 5 个地方
4.1 轮廓系数在 K=1 时报错
现象:循环从 K=1 开始,跑到silhouette_score直接抛ValueError: Number of labels is 1。
原因:轮廓系数需要至少两个簇才能算分离度,K=1 时 b 无定义。
解决:评估循环从 K=2 开始。SSE 可以从 K=1 算起用于画肘部图,但轮廓系数必须跳过 K=1。代码里把k_range设成range(2, 11)就避开了。
4.2 每次跑出来的肘部拐点不一样
现象:同样的数据,不设随机种子,两次运行 SSE 曲线形状不同,拐点位置飘。
原因:K-means 初始质心随机,不同初始值收敛到不同局部最优,SSE 有波动。
解决:固定random_state,同时把n_init设成 10 或更大。n_init越大结果越稳,代价是训练时间线性增加。数据量在几万行以内,n_init=10足够。
4.3 轮廓系数峰值出现在很大的 K 上
现象:K=8 或 K=10 的轮廓系数反而比 K=3、K=4 高,跟肘部图矛盾。
原因:数据里有离群点或者簇的密度差异大,大 K 把离群点单独分成小簇,这些簇的分离度极高,拉高了平均值。
解决:先做离群点检测(比如用 IQR 或 IsolationForest)剔除异常值再评估;或者改用silhouette_samples看每个点的分布,如果少数点贡献了大部分高分,说明峰值不可信。这种情况下优先信肘部法则。
4.4 数据量上万后轮廓系数算得极慢
现象:K 循环跑到一半卡住,silhouette_score单次耗时几十秒。
原因:轮廓系数要算所有点对之间的距离,复杂度 O(n²),n=5 万时计算量爆炸。
解决:用silhouette_score(X, labels, sample_size=5000, random_state=42)抽样计算,5000 个样本足够反映整体分布。或者先对数据做 PCA 降维再算,维度降低也能提速。
4.5 忘了标准化,大尺度特征主导了距离
现象:聚类结果全被某一个特征决定,其他特征几乎没起作用。
原因:K-means 用欧氏距离,量纲大的特征(比如收入几万 vs 年龄几十)会主导距离计算。
解决:聚类前用StandardScaler做标准化:
from sklearn.preprocessing import StandardScaler X_scaled = StandardScaler().fit_transform(X)标准化后再跑评估流程。注意标准化参数要在训练集上 fit,如果后续有新数据要预测,用同一个 scaler 做 transform,不能重新 fit。
5. 进阶技巧:用轮廓图逐簇诊断,别只看一个平均值
整体轮廓系数是个平均数,它会把问题掩盖掉。一个簇分得很好、另一个簇完全混在一起,平均下来可能还有 0.5,看着不错,实际有一半样本归属存疑。真正靠谱的做法是画轮廓图(silhouette plot),把每个簇的轮廓系数分布摊开看。
from sklearn.metrics import silhouette_samples import matplotlib.cm as cm def plot_silhouette(X, labels, k): sil_vals = silhouette_samples(X, labels) fig, ax = plt.subplots(figsize=(8, 6)) y_lower = 10 for i in range(k): # 取出第 i 簇所有点的轮廓系数并排序 ith_vals = np.sort(sil_vals[labels == i]) size_i = ith_vals.shape[0] y_upper = y_lower + size_i color = cm.nipy_spectral(float(i) / k) ax.fill_betweenx(np.arange(y_lower, y_upper), 0, ith_vals, facecolor=color, alpha=0.7) ax.text(-0.05, y_lower + 0.5 * size_i, str(i)) y_lower = y_upper + 10 # 画整体平均线 ax.axvline(x=sil_vals.mean(), color='red', linestyle='--', label=f'平均={sil_vals.mean():.3f}') ax.set_xlabel('轮廓系数') ax.set_ylabel('簇编号') ax.set_title(f'轮廓图 (K={k})') ax.legend() plt.savefig(f'silhouette_k{k}.png', dpi=150) plt.show() plot_silhouette(X, final_labels, best_k)读这张图有三个要点:每个簇的「刀片」越宽、越靠右,说明该簇越紧致;如果某个簇的刀片大部分在 0 左边,这个簇的样本大概率分错了;各簇刀片宽度差异大,说明簇大小不均衡,K-means 对这种情况处理不好,可以考虑用 K-means++ 初始化或者换高斯混合模型。
我自己的习惯是:肘部图和轮廓系数曲线定候选 K,轮廓图做最终确认。有一次做用户分群,整体轮廓系数 0.52 看着还行,画了轮廓图发现有一个簇的系数全在 0.1 以下,拆开看是两类行为完全不同的用户被硬塞进了一个簇,把 K 加一之后整体系数只涨了 0.01,但那个问题簇消失了,业务侧的解释性好了很多。指标是参考,图才是证据。希望这套流程帮你在下一个聚类任务里少走点弯路。
本文还有配套的精品资源,点击获取