ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

K-means聚类k值选择:肘部法则与轮廓系数可视化实战

K-means聚类k值选择:肘部法则与轮廓系数可视化实战 简介这份资源是面向数据科学初学者与算法实践者的K-means聚类可视化Python代码包聚焦聚类分析中簇数选择这一核心难点通过肘部法则与轮廓系数两条路径帮助读者判断最优K值适用于课程作业、项目原型与自学练手。压缩包共27个文件约10.04MB其中7个py脚本承载聚类主流程与示例调用18张png图表记录不同参数下的聚类效果与评估曲线另附txt依赖清单与md说明文档便于快速理解目录结构与运行方式。目前已有165人学习下载。代码基于numpy、matplotlib、seaborn、scikit-learn与pandas构建读者可直接运行主程序查看完整演示并借助生成的肘部曲线、轮廓系数对比图直观感受聚类质量变化同时参考示例数据与结果目录把可视化分析思路迁移到自己的数据集上减少从零调试的成本。1. K-means 聚类可视化肘部法则和轮廓系数到底该怎么用跑完一个 K-means 聚类最常被问到的不是「模型怎么写的」而是「k 到底取几」。这个问题不解决后面所有的可视化都是自欺欺人——你画出来的簇边界再漂亮k 选错了业务解释就是空中楼阁。肘部法则和轮廓系数就是用来回答这个问题的两把尺子前者看簇内误差随 k 增大的下降速度后者看每个点跟自己簇的紧密程度与跟最近邻簇的分离程度。这篇内容面向已经会用 Python 做数据分析、但聚类调参还在凭感觉的从业者从零把 K-means 的可视化流程搭起来包含 SSE 肘部曲线、轮廓系数曲线、聚类散点图和轮廓图四张核心图每一步都给可复现的代码和参数说明。读完你能直接把这套流程套到自己的数据集上知道 k 怎么定、图怎么看、哪里容易翻车。2. 环境准备与数据生成把 K-means 可视化跑起来的最小闭环2.1 依赖库安装与版本确认做 K-means 聚类可视化核心依赖就四个numpy负责数值计算matplotlib负责画图scikit-learn提供KMeans和silhouette_scorepandas用来整理中间结果。如果你用的是 Anaconda这些基本都预装了如果是裸 Python 环境一条命令补齐pip install numpy matplotlib scikit-learn pandas装完之后建议在脚本开头打印版本避免因为 sklearn 版本差异导致KMeans参数行为不一致比如n_init在不同版本默认值不同这个后面避坑章节会细说import numpy as np import matplotlib.pyplot as plt import pandas as pd from sklearn.cluster import KMeans from sklearn.datasets import make_blobs from sklearn.metrics import silhouette_score, silhouette_samples from sklearn.preprocessing import StandardScaler import sklearn print(fnumpy: {np.__version__}) print(fsklearn: {sklearn.__version__}) print(fmatplotlib: {plt.matplotlib.__version__})这段代码的作用是确认环境可用。silhouette_samples和silhouette_score是两个不同粒度的函数前者返回每个样本的轮廓系数画轮廓图必须用它后者返回全局平均值用来画轮廓系数随 k 变化的曲线。很多人只 import 了silhouette_score到画轮廓图时才发现少东西这是最常见的低级翻车。2.2 用 make_blobs 造一份可控的聚类数据真实数据往往没有标签调试阶段不好判断聚类对不对。我一般先用make_blobs造一份已知簇数的数据这样能验证整套可视化流程是否正常工作。参数上n_samples控制样本量centers控制真实簇数cluster_std控制簇的松散程度random_state保证每次生成的数据一致# 生成 3 簇、每簇 300 个样本的二维数据 X, y_true make_blobs( n_samples900, centers3, cluster_std0.80, random_state42 ) # 标准化K-means 基于欧氏距离量纲差异会主导距离计算 scaler StandardScaler() X_scaled scaler.fit_transform(X) print(f数据形状: {X_scaled.shape}) print(f真实簇标签分布: {np.bincount(y_true)})这里有个关键决策要不要标准化。K-means 用欧氏距离衡量相似度如果两个特征的量纲差了几个数量级比如年龄 0-100 和收入 0-1000000距离会被大量纲特征完全主导聚类结果等于只用了那一个特征。所以只要特征量纲不统一StandardScaler是必须的。make_blobs造的数据本身量纲接近但养成标准化的习惯没坏处换成真实数据时不会忘。cluster_std0.80这个值是我调出来的太小比如 0.3三簇分得太开肘部曲线拐点不明显看不出效果太大比如 2.0三簇糊在一起轮廓系数全线偏低也不利于演示。0.8 左右能造出「肉眼能分但有重叠」的效果最接近真实业务数据的形态。3. 肘部法则SSE 曲线怎么画、拐点怎么读3.1 SSE 的计算逻辑与 KMeans 关键参数肘部法则的核心指标是 SSESum of Squared Errors也就是每个样本到其所属簇中心的距离平方和。k 增大时 SSE 必然下降——极端情况下每个点自成一簇SSE 为 0。所以不能只看 SSE 绝对值要看它的下降速率在真实簇数附近SSE 会有一个明显的「拐点」过了这个点再增加 kSSE 下降变缓说明多出来的簇没有带来实质性的紧凑度提升。用 sklearn 算 SSE 很直接KMeans拟合后通过inertia_属性拿到sse [] k_range range(1, 11) for k in k_range: km KMeans( n_clustersk, initk-means, # 智能初始化降低陷入局部最优的概率 n_init10, # 独立跑 10 次取最优sklearn 1.4 默认已是 auto max_iter300, # 单次迭代上限 random_state42 ) km.fit(X_scaled) sse.append(km.inertia_) # 打印每个 k 对应的 SSE for k, v in zip(k_range, sse): print(fk{k:2d} SSE{v:.2f})参数逐个说清楚。initk-means是初始化策略标准 K-means 随机选初始中心运气不好会收敛到很差的局部最优k-means 让初始中心尽量分散是现在的默认推荐。n_init10表示用不同随机种子跑 10 次取 SSE 最小的那次结果这个参数直接关系到结果稳定性——设成 1 的话同一份数据跑两次可能得到不同聚类这是很多人觉得 K-means「玄学」的根源。max_iter300是单次运行的迭代上限一般数据几十次就收敛了300 足够。3.2 肘部曲线的绘制与拐点判读拿到 SSE 列表后画折线图横轴 k纵轴 SSEplt.figure(figsize(8, 5)) plt.plot(k_range, sse, markero, linewidth2, color#2c7fb8) plt.xlabel(簇数 k, fontsize12) plt.ylabel(SSE簇内误差平方和, fontsize12) plt.title(肘部法则SSE 随 k 的变化, fontsize14) plt.xticks(list(k_range)) plt.grid(alpha0.3) # 标注拐点这里真实簇数是 3 plt.annotate(拐点 k3, xy(3, sse[2]), xytext(5, sse[2] 50), arrowpropsdict(arrowstyle-, colorred), fontsize11, colorred) plt.tight_layout() plt.savefig(elbow_method.png, dpi150) plt.show()画图本身不难难的是读图。肘部法则的「拐点」在数学上没有严格定义它是视觉判断曲线从陡峭下降转为平缓的那个转折位置。实操中我一般这样处理——先看曲线最明显的弯折处再结合业务约束。比如电商用户分群业务方可能只接受 3 到 5 个群体那就在这个范围内找拐点。如果曲线很平滑没有明显拐点真实数据经常这样肘部法则就不可靠了必须靠轮廓系数来交叉验证。提示SSE 曲线在 k 从 1 到 2、2 到 3 时下降幅度最大之后每增加一个 k 的边际收益递减。如果 k3 到 k4 的 SSE 降幅已经小于 10%继续增大 k 的意义就不大了。4. 轮廓系数从全局均值到单样本轮廓图4.1 轮廓系数的定义与计算轮廓系数衡量的是「一个样本跟自己簇的相似度」与「它跟最近邻簇的相似度」之间的相对关系。对单个样本 i设 a(i) 是它到同簇其他样本的平均距离凝聚度b(i) 是它到最近邻簇所有样本的平均距离分离度则s(i) (b(i) - a(i)) / max(a(i), b(i))s(i) 的取值范围是 -1 到 1。接近 1 说明该样本聚类合理接近 0 说明在两个簇边界上负值说明可能被分错了簇。全局轮廓系数就是所有样本 s(i) 的平均值。sil_scores [] for k in range(2, 11): # 轮廓系数要求 k2 km KMeans(n_clustersk, initk-means, n_init10, random_state42) labels km.fit_predict(X_scaled) score silhouette_score(X_scaled, labels, metriceuclidean) sil_scores.append(score) print(fk{k:2d} 轮廓系数{score:.4f})注意循环从 k2 开始因为 k1 时不存在「最近邻簇」轮廓系数无定义。metriceuclidean是距离度量跟 K-means 内部用的欧氏距离保持一致不要改成余弦距离否则两个指标衡量的不是同一件事。4.2 轮廓系数曲线与最优 k 的选择把轮廓系数随 k 的变化画出来峰值对应的 k 通常就是推荐值plt.figure(figsize(8, 5)) plt.plot(range(2, 11), sil_scores, markers, linewidth2, color#e6550d) plt.xlabel(簇数 k, fontsize12) plt.ylabel(平均轮廓系数, fontsize12) plt.title(轮廓系数随 k 的变化, fontsize14) plt.xticks(range(2, 11)) plt.grid(alpha0.3) best_k range(2, 11)[int(np.argmax(sil_scores))] plt.axvline(xbest_k, linestyle--, colorgray, alpha0.7) plt.annotate(f最优 k{best_k}, xy(best_k, max(sil_scores)), xytext(best_k 1.5, max(sil_scores)), arrowpropsdict(arrowstyle-, colorred), fontsize11, colorred) plt.tight_layout() plt.savefig(silhouette_scores.png, dpi150) plt.show()肘部法则和轮廓系数经常给出不同答案这很正常。我的处理原则是两者一致时直接用不一致时优先看轮廓系数因为它有明确的数值范围可比较性更强但如果轮廓系数峰值对应的 k 很小比如 k2而业务上明显需要更细的分群那就回到肘部曲线找次优拐点同时接受轮廓系数略低的事实。聚类没有唯一正确答案只有「在当前数据下相对合理」的答案。4.3 单样本轮廓图看清每个簇的质量全局轮廓系数是个平均值会掩盖簇与簇之间的质量差异。有的簇内部很紧凑有的簇拖着一堆边界样本平均下来看着还行实际有一个簇根本不能用。轮廓图silhouette plot就是用来暴露这个问题的def plot_silhouette(X, k, axNone): km KMeans(n_clustersk, initk-means, n_init10, random_state42) labels km.fit_predict(X) sil_vals silhouette_samples(X, labels, metriceuclidean) avg_score silhouette_score(X, labels, metriceuclidean) if ax is None: fig, ax plt.subplots(figsize(8, 6)) y_lower 10 colors plt.cm.Set2(np.linspace(0, 1, k)) for i in range(k): cluster_sil np.sort(sil_vals[labels i]) size cluster_sil.shape[0] y_upper y_lower size ax.fill_betweenx(np.arange(y_lower, y_upper), 0, cluster_sil, facecolorcolors[i], edgecolorcolors[i], alpha0.7) ax.text(-0.05, y_lower 0.5 * size, f簇 {i}, fontsize11) y_lower y_upper 10 ax.axvline(xavg_score, colorred, linestyle--, labelf平均{avg_score:.3f}) ax.set_xlabel(轮廓系数, fontsize12) ax.set_ylabel(样本按簇分组, fontsize12) ax.set_title(fk{k} 的轮廓图, fontsize14) ax.legend(locbest) return avg_score plot_silhouette(X_scaled, 3) plt.tight_layout() plt.savefig(silhouette_plot_k3.png, dpi150) plt.show()读轮廓图看三点一是每个簇的「刀片」是否都超过红色平均线如果某个簇大量样本的轮廓系数低于平均线甚至为负这个簇就有问题二是各簇刀片宽度是否均匀宽度代表样本量某个簇特别窄说明它可能不该独立存在三是刀片顶端是否尖锐越尖锐说明簇内样本越紧凑。我见过不少案例全局轮廓系数 0.55 看着不错但轮廓图一画有一个簇的样本全部在平均线以下这种结果拿去给业务方是要被打回来的。5. 聚类结果可视化与避坑排查5.1 二维散点图叠加簇中心二维数据可以直接画散点图每个点按簇标签着色再把簇中心标出来def plot_clusters(X, k): km KMeans(n_clustersk, initk-means, n_init10, random_state42) labels km.fit_predict(X) centers km.cluster_centers_ plt.figure(figsize(8, 6)) scatter plt.scatter(X[:, 0], X[:, 1], clabels, cmapSet2, s30, alpha0.6) plt.scatter(centers[:, 0], centers[:, 1], cred, markerX, s200, edgecolorsblack, linewidths1.5, label簇中心) plt.xlabel(特征 1标准化后, fontsize12) plt.ylabel(特征 2标准化后, fontsize12) plt.title(fK-means 聚类结果k{k}, fontsize14) plt.legend() plt.grid(alpha0.3) plt.tight_layout() plt.savefig(fcluster_result_k{k}.png, dpi150) plt.show() plot_clusters(X_scaled, 3)如果是三维以上数据不能直接画散点图常见做法是先做 PCA 降到二维再画但要在图注里说明「这是降维后的投影簇间距离不代表原始空间距离」。这一点经常被忽略导致业务方误读图上的重叠区域。5.2 避坑与常见问题排查现象一同一份数据跑两次聚类结果不一样。原因通常是n_init设得太小或没设K-means 对初始中心敏感不同随机种子会收敛到不同局部最优。解决办法是把n_init设到 10 以上同时固定random_state。如果数据本身簇结构不明显即使n_init10也可能不稳定这时候要回到数据层面检查特征是否选对了。现象二肘部曲线没有明显拐点一路平滑下降。原因一般是数据本身没有天然的簇结构或者簇的密度、大小差异太大。解决办法是不要硬找拐点改用轮廓系数或 Gap Statistic 交叉验证如果所有 k 的轮廓系数都低于 0.3说明这份数据可能不适合 K-means可以试试 DBSCAN 这类基于密度的聚类算法。现象三轮廓系数很高但业务方说分群没意义。原因是轮廓系数只衡量几何紧凑度不衡量业务可解释性。比如把「高消费低频」和「低消费高频」两个群体合并成一个簇几何上可能很紧凑但业务上是两类完全不同的人。解决办法是把聚类结果的簇中心还原到原始量纲逐个特征对比看每个簇在业务维度上是否有清晰画像。现象四标准化之后聚类结果反而变差了。原因是标准化把某些本来有意义的量纲差异抹掉了。比如收入这个特征高收入和低收入的绝对差距本身就是重要信号标准化后这个信号被压缩。解决办法是分情况处理量纲差异纯粹来自单位不同米 vs 厘米时必须标准化量纲差异本身携带业务信息时考虑用归一化到 [0,1] 或者干脆不处理但要确保没有量纲差几个数量级的特征混在一起。现象五k 增大时轮廓系数不升反降但 SSE 一直在降。这是正常的两个指标衡量维度不同。SSE 只看簇内紧凑度k 越大必然越小轮廓系数同时看凝聚度和分离度k 太大时簇与簇之间会挨得太近分离度下降轮廓系数就下来了。这种情况下以轮廓系数为准不要被 SSE 的持续下降误导。6. 把可视化流程封装成可复用函数上面每一步都拆开写了实际项目中我习惯把整套流程封装成一个函数输入原始数据和 k 的范围输出四张图和一份指标汇总表。这样换数据集时只改输入不用重写代码def kmeans_analysis(X, k_max10, prefixkm): X: 原始特征矩阵未标准化 k_max: 最大尝试簇数 prefix: 输出文件名前缀 返回: 包含 SSE 和轮廓系数的 DataFrame X_scaled StandardScaler().fit_transform(X) results [] for k in range(2, k_max 1): km KMeans(n_clustersk, initk-means, n_init10, max_iter300, random_state42) labels km.fit_predict(X_scaled) results.append({ k: k, SSE: km.inertia_, 轮廓系数: silhouette_score(X_scaled, labels) }) df pd.DataFrame(results) # 画肘部曲线 fig, axes plt.subplots(1, 2, figsize(14, 5)) axes[0].plot(df[k], df[SSE], markero, color#2c7fb8) axes[0].set_xlabel(k); axes[0].set_ylabel(SSE) axes[0].set_title(肘部法则); axes[0].grid(alpha0.3) axes[1].plot(df[k], df[轮廓系数], markers, color#e6550d) axes[1].set_xlabel(k); axes[1].set_ylabel(轮廓系数) axes[1].set_title(轮廓系数); axes[1].grid(alpha0.3) plt.tight_layout() plt.savefig(f{prefix}_metrics.png, dpi150) plt.show() return df # 用法 df_result kmeans_analysis(X, k_max10, prefixdemo) print(df_result.to_string(indexFalse))这个封装里有两个设计取舍值得说。一是标准化放在函数内部做保证每次调用行为一致避免调用方忘记标准化如果你的数据已经标准化过把StandardScaler那行去掉即可。二是返回 DataFrame 而不是直接打印方便后续筛选和存档——我一般会把这份表存成 CSV跟聚类结果一起归档下次有人问「为什么选 k3」直接翻表给数据。最后说一个我踩过的坑silhouette_score的计算复杂度是 O(n²)样本量上万时单次计算就要几十秒如果 k 从 2 试到 10光轮廓系数就要跑好几分钟。样本量大的时候我一般先随机采样 5000 条算轮廓系数确认 k 的大致范围后再用全量数据跑最终的 K-means。这个采样步骤不影响 k 的选择但能把调试时间从十分钟压到一分钟以内。希望帮到你。本文还有配套的精品资源点击获取
返回列表