
1. 项目核心思路与方案选型1.1 聚类图到底要解决什么问题先说个我自己的感受。做了几年 Python 数据分析画过的图没有一千也有八百但聚类图一直是我觉得看着简单、想画明白不容易的那一类。很多人以为聚类图就是把点画在图上、按颜色分个组真上手之后才发现用什么算法聚、聚成几类、坐标轴用什么、要不要标准化每一步都会影响最终图表的解释力。聚类图本质上承担的是一个翻译工作——把无监督学习算出来的分类结果转成肉眼能直接读懂的视觉语言。它要回答的不是我画了个图而是几个非常具体的问题这批样本到底分成几类才合理类与类之间的边界清不清晰有没有离群点把聚类结果带偏了不同类在特征空间里是挤成一团还是泾渭分明举个实际例子。我之前做过一个电商用户的分群项目后台拉出来几十万条订单数据每个用户有消费金额、购买频次、最近一次购买间隔三个特征。用 KMeans 跑完之后如果不画图只看聚类中心那几个数字你很难向业务同事解释第二类用户到底长什么样。但把聚类结果投影到二维散点上每个点代表一个用户颜色代表类别业务方一眼就能看懂右上角那群是高价值活跃用户左下角是沉睡用户中间一大坨是普通用户。这就是聚类图存在的意义——它不是给自己看的是给决策者看的。所以项目的核心拆解应该是这样先搞清楚要展示什么分类结果、层次关系、还是特征相关性再选合适的算法和图表形式最后才是调参和美化。顺序反了图再好看也是废的。1.2 技术栈选型为什么是 sklearn pandas matplotlib/seaborn聚类可视化这个场景Python 生态里可选的东西很多但我个人建议主力还是scikit-learn、pandas、matplotlib、seaborn这四件套理由很直接它们各自负责的环节边界清晰组合起来出图快、可复现性强。具体分工是这样的环节工具负责内容数据读取与清洗pandasDataFrame 读写、缺失值处理、列筛选聚类算法scikit-learnKMeans、层次聚类、DBSCAN、评估指标基础绘图matplotlib散点图、画布控制、坐标轴调整高级图表seaborn热力图、聚类树状图、统计增强层为什么不用 Plotly 或者 pyecharts不是说它们不好交互式图表在汇报演示时确实加分但聚类图这个场景有个特殊性你经常需要反复调整参数K 值、距离度量、标准化方式每次调整都要快速看结果。matplotlib和seaborn的静态图在这个迭代过程中刷新成本最低而且和 sklearn 的数据结构配合得最顺畅。交互式图适合做最终交付不适合做探索分析。另外有个细节容易忽略scipy.cluster.hierarchy里的dendrogram树状图和linkage函数在层次聚类可视化里几乎是标配但很多教程只讲 sklearn 的AgglomerativeClustering不讲 scipy 这套配套工具。实际做层次聚类图的时候scipy 的函数接口更底层、更灵活我建议两个都要会。环境这部分按最常用的方式装就行pip install pandas scikit-learn matplotlib seaborn scipy如果是 Anaconda 用户conda install也可以看个人习惯。注意画聚类图之前先确认你的 Python 版本和库版本 compat。之前遇到过一个很隐蔽的坑sklearn 1.2 版本把n_init参数的默认值改了导致 KMeans 每次跑出来结果不一样排查了半天才发现是版本更新的行为变化。建议在代码开头打印一下版本号给自己留个底。2. 三种主流聚类图实战拆解2.1 散点聚类图最直观的分类结果呈现散点图是聚类可视化里最常用、也最好上手的形式。它的逻辑很简单把每个样本按两个特征投影到二维平面用颜色区分不同簇用特殊标记标出聚类中心。适合展示 KMeans、DBSCAN 这类硬分类算法的结果。直接看代码。我用 sklearn 自带的make_blobs造一批演示数据然后跑 KMeans 聚类并可视化import numpy as np import pandas as pd import matplotlib.pyplot as plt from sklearn.datasets import make_blobs from sklearn.cluster import KMeans from sklearn.preprocessing import StandardScaler # 生成演示数据500个样本分成4簇 X, y_true make_blobs( n_samples500, centers4, cluster_std0.8, random_state42 ) # 标准化让每个特征对距离的贡献公平 scaler StandardScaler() X_scaled scaler.fit_transform(X) # KMeans聚类 kmeans KMeans(n_clusters4, random_state42, n_init10) kmeans.fit(X_scaled) # 可视化 plt.figure(figsize(9, 7)) plt.scatter( X_scaled[:, 0], X_scaled[:, 1], ckmeans.labels_, cmapviridis, s40, alpha0.8 ) plt.scatter( kmeans.cluster_centers_[:, 0], kmeans.cluster_centers_[:, 1], cred, markerx, s200, linewidths2, label聚类中心 ) plt.title(KMeans 聚类结果散点图) plt.xlabel(标准化后特征1) plt.ylabel(标准化后特征2) plt.legend() plt.grid(alpha0.3) plt.show()这套代码跑起来没什么问题但有几个点需要解释一下为什么这么做。第一个是标准化。数据里如果两个特征的量纲差很多比如一个是年龄20-60一个是年收入5万-100万直接算欧式距离时年龄的贡献会被收入完全淹没聚类结果基本等于只按收入分。标准化之后每个特征均值0、方差1距离计算才公平。这不是可选项是必选项。第二个是n_init10。KMeans 的初始质心是随机的不同初始值可能收敛到不同的局部最优解。n_init表示从不同随机初始状态跑多少次最后取 SSE 最小的结果。老版本 sklearn 默认是 10新版本改成了auto建议手动写死保证可复现。第三个是颜色映射。cmapviridis是 matplotlib 里对色盲友好度比较高的颜色映射不用默认的jet彩虹色因为jet的色带在某些情况下会产生视觉误导。2.2 层次聚类树状图看清类里面有类散点图适合展示扁平分类但如果你想知道类与类之间的亲疏关系就得用树状图。树状图来自层次聚类核心逻辑是先计算所有样本两两之间的距离然后反复合并最近的两类直到所有样本合成一个大类。这种图用的最多的场景是生物信息学里的基因表达聚类还有电商里的品类层级划分。我自己在分析商品销售数据时也用过一次把一个季度内所有 SKU 按销量走势聚类树状图一画出来哪些商品是旺季型、哪些是常青型、哪些是衰退型层级关系一目了然。用 scipy 画树状图的完整流程from scipy.cluster.hierarchy import linkage, dendrogram, fcluster import matplotlib.pyplot as plt # 假设 X_scaled 已经准备好n_samples x n_features # linkage 计算层次聚类methodward 表示按方差增量最小合并 Z linkage(X_scaled, methodward) # 树状图 plt.figure(figsize(12, 6)) dendrogram( Z, truncate_modelevel, p5, leaf_rotation90, leaf_font_size10 ) plt.title(层次聚类树状图 (Ward 连接法)) plt.xlabel(样本编号截断显示) plt.ylabel(簇间距离) plt.show() # 用 fcluster 从树状图中切出固定数量的类 labels fcluster(Z, t4, criterionmaxclust)methodward这个参数值得单独说说。Ward 方法的合并规则是每次合并两个簇时选择让合并后簇内方差增加最小的那个组合。它对噪声相对不敏感聚类出来的簇形状比较紧凑均匀是实践里适用性最好的连接方法。相比之下single单连接容易链式蔓延complete全连接对离群点敏感新手直接用ward是风险最低的选择。树状图的纵轴表示簇间距离你可以理解为两个簇合并时的代价。纵轴上距离越大说明两个簇越不相似。看图的时候注意一个技巧找一条水平线横着切过去它穿过的竖线数量就是对应的簇数量。比如你希望分4类就找到纵轴高度约等于第4高合并线的地方切一刀。这个看图切分的方法比盲目试 K 值直观得多。还有一个要注意的点当样本量很大的时候树状图底部会密密麻麻全是叶子标签根本看不清。这时候用truncate_modelevel加上p5只显示上面5层的聚类结构底层合并细节折叠成括号形式。这在向上汇报的时候特别有用不然一张图几万个标签谁也看不懂。2.3 聚类热力图矩阵数据的最佳拍档散点图和树状图都是点的视角处理的是独立的样本行。可如果你的数据是样本 x 特征的矩阵而且特征是几十上百个或者你的目标是想同时看样本聚类和特征聚类那热力图是更合适的选择。seaborn.clustermap就是干这个的。它做的事情很重内部先对行和列分别做层次聚类然后把矩阵重新排序相似的行挨在一起、相似的列挨在一起最后用颜色深浅表示数值大小。结果就是一张既能看到样本布局、又能看到特征模式的图。import seaborn as sns import pandas as pd # 构造一个示例矩阵10个样本8个特征 np.random.seed(0) df pd.DataFrame( np.random.rand(10, 8) * 10, columns[f特征_{i} for i in range(1, 9)], index[f样本_{i} for i in range(1, 11)] ) # clustermap 自动完成行和列的层次聚类 g sns.clustermap( df, cmapRdYlBu_r, standard_scale1, # 对每一行做z-score标准化 figsize(8, 8), row_colorsNone # 可以传入一组颜色标记给样本分组标注 ) plt.show()standard_scale1这个参数是精髓。它表示对每一行做 z-score 标准化就是让每个样本的各特征值变成相对于自己均值偏离了几个标准差。为什么要这样因为不同样本的整体量级可能差很多比如有的样本所有特征都是个位数有的样本所有特征都是几十。如果不做行内标准化热力图的颜色深浅会被样本自身量级主导你看到的不是特征模式而是谁数值大谁就红。做了行内标准化之后每个样本都是自己跟自己比红色表示该特征在自己内部偏高蓝色表示偏低聚类吃的就是这个结构。我用这个图做过一个实际的项目对某零售品牌的月度销售数据做聚类行是门店列是不同品类。clustermap画完之后非常直观右下角有一群门店聚类在一起它们的共同特征是生鲜占比极高、日用品占比低一眼定位出这类门店的品类结构问题。这种洞察如果用数字表格去排查不知道要花多长时间。3. 实操中的核心细节与参数掌控3.1 数据准备聚类图好看的前提是数据干净画聚类图遇到的最蠢的报错是什么不是算法崩了而是数据里混着非数值列sklearn 直接抛ValueError: could not convert string to float。真实业务数据几乎不可能干干净净直接塞进聚类算法所以数据准备这个环节我会多花一点时间。第一个是缺失值处理。KMeans 这类基于距离的算法不支持缺失值np.nan参与距离计算时会直接返回nan。处理方式要看缺失比例如果某一列的缺失比例超过30%建议直接删列如果在5%以下用中位数或均值填充即可如果介于中间需要结合业务判断可能需要用简单的填充模型。直接df.dropna()是最省事的做法但当数据宝贵的时候每删一行都是信息损失我会先看一眼缺失分布再决定。第二个是非数值列的处理。比如你有一个性别列值是男和女聚类算法不认。这时候有两个流派一个是LabelEncoder把男编码成0、女编码成1适合有序类别另一种是OneHotEncoder把性别拆成是否男是否女两列适合无序类别。从聚类效果上讲我建议无序类别用 one-hot因为它不会强加一个不存在的顺序关系。但这个选择会直接影响后续可视化——特征维度变多了散点图只能挑两个特征画热力图看起来更合适。第三个是标准化。这个前面反复强调过但我要再说得更细一点。标准的StandardScaler是把每个特征变成均值0、方差1公式是(x - mean) / std。但如果你的数据有明显偏态比如消费金额大部分人花得少、少数人花得多StandardScaler之后依然偏态这时用RobustScaler基于中位数和四分位距更稳。用哪个没有绝对标准我会先画一下特征的分布直方图再决定。实操心得数据标准化前后可以打印X_scaled[:5]看一眼数值应该在 -3 到 3 之间比较正常。如果出现 ±10 以上的值说明有极端离群点这时候聚类结果大概率会被这个点带偏要么处理离群点要么考虑用 DBSCAN 这类对离群点不敏感的算法。3.2 K 值怎么选不能只靠眼睛看聚类图最容易踩的坑就是 K 值选择。很多人直接KMeans(n_clusters3)或者看图觉得分两类挺顺眼这在探索性分析里勉强能用但一旦要给别人汇报就得有数据依据。两个最常用的方法肘部法则和轮廓系数。肘部法则的思路是随着 K 增大簇内误差平方和SSE即kmeans.inertia_会不断下降但下降的速度会越来越慢。画一个 K-SSE 曲线找到下降快和下降慢的拐点那个位置的 K 就是合理的聚类数。from sklearn.cluster import KMeans inertia_list [] K_range range(1, 11) for k in K_range: km KMeans(n_clustersk, random_state42, n_init10) km.fit(X_scaled) inertia_list.append(km.inertia_) plt.figure(figsize(8, 5)) plt.plot(K_range, inertia_list, markero) plt.xlabel(K值) plt.ylabel(簇内误差平方和 (inertia)) plt.title(肘部法则选择K值) plt.grid(alpha0.3) plt.show()看这个曲线的时候拐点有时候很明显比如 K3 之后曲线明显平缓了那 K3 就是答案。但真实数据往往没有这么理想曲线可能是平滑递减的这时候要结合业务判断。我在一个用户分群项目里就遇到过这种情况肘部法则显示 K5 附近有点拐但业务方明确说我们只需要三档用户高、中、低最后就取 K3。结论是肘部法则给的是统计上的建议不是圣旨。轮廓系数是另一个思路它衡量每个样本与自己簇内其他样本的距离内聚度和与最近的其他簇样本的距离分离度之间的比例取值范围 -1 到 1。系数接近 1 说明聚类效果好接近 0 说明样本在两个簇的边界上负值说明分错了簇。from sklearn.metrics import silhouette_score sil_list [] for k in range(2, 11): km KMeans(n_clustersk, random_state42, n_init10) labels km.fit_predict(X_scaled) sil_list.append(silhouette_score(X_scaled, labels)) plt.figure(figsize(8, 5)) plt.plot(range(2, 11), sil_list, markero) plt.xlabel(K值) plt.ylabel(轮廓系数) plt.title(轮廓系数选择K值) plt.grid(alpha0.3) plt.show()轮廓系数越高越好取最大值对应的 K。但注意它也不是万能的当数据本身没有明显簇结构时轮廓系数可能整体都低这种情况说明用聚类可能本身就不合适或者需要换算法。我见过有人硬凑 K 追求高分结果分出来的簇在业务上完全无法解释这就是本末倒置了。3.3 可视化细节把丑图调成能看的图聚类图的核心是信息准确但也不能太丑。几个 matplotlib 绘制细节我单独拎出来说都是平时容易忽略的。第一个是画布和坐标轴比例。散点图如果横纵轴单位尺度差太多比如横轴 0-1、纵轴 0-100图形会被压缩成细长条聚类之间的区分度很难看出来。两个办法一个是plt.figure(figsize(8, 6))手动控制画布宽高比另一个是用plt.axis(equal)强制坐标轴单位长度一致。对于标准化后的数据我会直接把figsize设成近正方形保证散点不扭曲。第二个是颜色和透明度。alpha0.8是我常用的散点透明度为什么不设成 1因为当某个簇的样本点非常多且重叠严重时全不透明会让后画的点完全盖住先画的点根本看不出密度分布。加点透明度两个簇有重叠的部分会显示为颜色混合你能直觉地感知哪些区域分类不够清晰。第三个是图例和标签。如果图上有多个子图比如左边是原始数据、右边是聚类结果记得给每个子图加标题和轴标签。leaf_rotation90是树状图里常用的参数防止叶子标签互相重叠。还有一个容易被忽略的matplotlib 默认不支持中文显示画图标题里有中文会显示成方框。解决方式是在脚本开头配置字体plt.rcParams[font.sans-serif] [SimHei] # Windows 黑体 plt.rcParams[axes.unicode_minus] False # 解决负号显示问题macOS 用户要把[SimHei]换成[Arial Unicode MS]Linux 用户可能需要先安装中文字体再指定名称。这个问题我在不同的操作系统上踩过好几次每次换电脑都得重新配一次。4. 常见问题与排查技巧实录4.1 高频报错与样式问题速查我把实操中真正遇到过的、以及身边同事问过最多的聚类图问题整理成一个速查表方便对照排查。现象常见原因解决办法KMeans 每次运行结果不一致初始质心随机n_init未固定或版本默认值变化设置random_state42, n_init10聚类图所有点聚成一坨未做标准化某个特征量纲过大主导距离用StandardScaler或RobustScalerValueError: could not convert string to float数据里有非数值列用LabelEncoder或OneHotEncoder转换或删除该列散点图图例显示No handles with labels found只有一个scatter未设置label但调用了legend()至少给一个散点加label参数树状图底部标签叠成一团样本量太大全部标签都显示用truncate_modelevel, p5截断显示图中文字显示为方框matplotlib 默认字体不支持中文配置rcParams[font.sans-serif]为中文字体热力图颜色深浅看不出差异数据量纲差异大未做标准化用standard_scale1或StandardScaler预处理silhouette_score 报错样本量小于 K 值或数据存在异常值减小 K 值检查并处理离群点这里我想特别强调一下聚类图所有点聚成一坨这个问题。很多人第一反应是我是不是应该换个算法但实际上 80% 的情况都是特征量纲问题。我之前处理过一个餐饮门店的数据特征是日营业额几百到几万和桌数5到50没标准化之前 KMeans 分出来的三簇每一簇的区别几乎全在营业额上桌数完全被淹没。标准化之后一画图才发现高营业额大桌数和高营业额小桌数其实是两类完全不同的门店形态。所以遇到聚类结果不合理先检查这个再动算法。4.2 聚类结果不合理时的排查思路图表画出来很漂亮不代表聚类结果是对的。我见过最多的情况是散点图上颜色分明但每个类别在业务语义上毫无意义。这时候不要急着美化图先回到数据层面排查。第一步打印每个簇的样本量和中心点from collections import Counter print(Counter(kmeans.labels_)) # 查看每个簇的中心注意如果是标准化后的数据这里也是标准化尺度 centers scaler.inverse_transform(kmeans.cluster_centers_) centers_df pd.DataFrame(centers, columns[特征1, 特征2]) print(centers_df)如果某个簇只剩几个样本另一些簇有一大堆分布极度不均衡说明 K 值可能偏大或者数据里有明显的离群点被单独分成了一个簇。第二步回到原始数据检查特征分布。画一下每个特征的直方图或箱线图看看是不是有偏态分布。如果有明显的长尾特征先做对数变换再聚类效果常会好很多。我在处理消费金额这类幂律分布数据时np.log1p()基本是标配操作。第三步考虑降维。当特征维度大于3时散点图无法直接投影全部信息。很多人只看前两个主成分的散点图然后进行解读但要注意PCA 之后的坐标是线性组合失去了原始特征的业务含义。如果你需要向业务方解释这个簇意味着什么建议回到原始特征视角去描述不要用 PC1、PC2 说事。seaborn里的pairplot也是一个替代方案把所有二维特征组合都画出来你对聚类结构的理解会更加立体。4.3 可复现性与性能优化聚类图在探索阶段要反复迭代可复现性是刚需。我的习惯是在项目开头设置一个全局随机种子np.random.seed(42)然后所有可能涉及随机性的操作都显式传random_state。这样同一个脚本跑出来的结果完全一致排查问题时才能确定改了参数导致变化还是随机性导致变化。数据量扩大之后还有一个性能问题要注意。原始的 KMeans 在十万级样本、几十个特征上还能跑但如果上了百万级每次fit可能要等很久。这时候建议改用MiniBatchKMeansfrom sklearn.cluster import MiniBatchKMeans mbk MiniBatchKMeans( n_clusters4, batch_size1024, n_init3, random_state42 ) mbk.fit(X_scaled)MiniBatchKMeans每次随机抽一小批样本来更新质心速度能快一个数量级代价是聚类精度略降。在探索阶段这个精度损失完全可接受先用它快速画图看个大概确定了 K 值和预处理方案之后再跑一遍完整 KMeans 做最终结果。聚类图也是一样的问题。当样本量超过几万直接plt.scatter画几万个点渲染会明显卡顿。两个技巧一是把s参数调小比如s10减少填充面积二是可以先抽样画图比如sample_idx np.random.choice(len(X_scaled), size5000, replaceFalse)确定最终方案后再全量出图。5. 结语一点个人体会从我自己的经验来看聚类图这个技能难的不是 matplotlib 的 API 怎么调而是你有没有真正理解这个图要回答的问题。算法参数调得再花哨如果最终的图传达给业务方的信息是错的或者根本没法解释那前面的工作就等于白做了。我之前有一个项目聚类图画得非常好看簇与簇之间边界清晰、颜色分明结果拿到业务评审会上同事问了一句那这个绿色的聚类群体我们下一步该做什么我一下答不上来。后来反思是因为我一直在追求统计指标和视觉表现忽略了聚类结果必须映射回业务行动。从那以后我每次画聚类图之前都会先问三个问题这些簇在业务上意味着什么它们之间的差异能用数据里的哪些特征解释看完这张图该做什么决策图是手段不是目的。如果你刚接触聚类可视化我的建议是先把最简单的散点聚类图跑通用make_blobs造数据玩明白 KMeans 的参数再去尝试树状图理解层次聚类的合并逻辑最后接触热力图把矩阵数据的聚类可视化拿下。这个路径是循序渐进的每一步都在为下一步打基础。本篇文章已经覆盖了最常用的三种聚类图形式也分享了我在实际项目中踩过的坑和积累的判断经验。剩下的就是打开你的 Jupyter Notebook找一份真实数据动手跑一遍。画图的过程难免磕磕绊绊但每解决一个问题你对数据、算法和可视化的理解都会再深一层。