拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

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

Python聚类可视化实战:从KMeans散点图到层次聚类热力图

Python聚类可视化实战:从KMeans散点图到层次聚类热力图

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_samples=500, centers=4, cluster_std=0.8, random_state=42 ) # 标准化:让每个特征对距离的贡献公平 scaler = StandardScaler() X_scaled = scaler.fit_transform(X) # KMeans聚类 kmeans = KMeans(n_clusters=4, random_state=42, n_init=10) kmeans.fit(X_scaled) # 可视化 plt.figure(figsize=(9, 7)) plt.scatter( X_scaled[:, 0], X_scaled[:, 1], c=kmeans.labels_, cmap='viridis', s=40, alpha=0.8 ) plt.scatter( kmeans.cluster_centers_[:, 0], kmeans.cluster_centers_[:, 1], c='red', marker='x', s=200, linewidths=2, label='聚类中心' ) plt.title('KMeans 聚类结果散点图') plt.xlabel('标准化后特征1') plt.ylabel('标准化后特征2') plt.legend() plt.grid(alpha=0.3) plt.show()

这套代码跑起来没什么问题,但有几个点需要解释一下为什么这么做。

第一个是标准化。数据里如果两个特征的量纲差很多,比如一个是"年龄"(20-60),一个是"年收入"(5万-100万),直接算欧式距离时年龄的贡献会被收入完全淹没,聚类结果基本等于只按收入分。标准化之后每个特征均值0、方差1,距离计算才公平。这不是可选项,是必选项。

第二个是n_init=10。KMeans 的初始质心是随机的,不同初始值可能收敛到不同的局部最优解。n_init表示从不同随机初始状态跑多少次,最后取 SSE 最小的结果。老版本 sklearn 默认是 10,新版本改成了'auto',建议手动写死,保证可复现。

第三个是颜色映射。cmap='viridis'是 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 计算层次聚类:method='ward' 表示按方差增量最小合并 Z = linkage(X_scaled, method='ward') # 树状图 plt.figure(figsize=(12, 6)) dendrogram( Z, truncate_mode='level', p=5, leaf_rotation=90, leaf_font_size=10 ) plt.title('层次聚类树状图 (Ward 连接法)') plt.xlabel('样本编号(截断显示)') plt.ylabel('簇间距离') plt.show() # 用 fcluster 从树状图中切出固定数量的类 labels = fcluster(Z, t=4, criterion='maxclust')

method='ward'这个参数值得单独说说。Ward 方法的合并规则是:每次合并两个簇时,选择让合并后簇内方差增加最小的那个组合。它对噪声相对不敏感,聚类出来的簇形状比较紧凑均匀,是实践里适用性最好的连接方法。相比之下,'single'(单连接)容易链式蔓延,'complete'(全连接)对离群点敏感,新手直接用'ward'是风险最低的选择。

树状图的纵轴表示"簇间距离",你可以理解为两个簇合并时的代价。纵轴上距离越大,说明两个簇越不相似。看图的时候注意一个技巧:找一条水平线横着切过去,它穿过的竖线数量就是对应的簇数量。比如你希望分4类,就找到纵轴高度约等于第4高合并线的地方切一刀。这个"看图切分"的方法比盲目试 K 值直观得多。

还有一个要注意的点:当样本量很大的时候,树状图底部会密密麻麻全是叶子标签,根本看不清。这时候用truncate_mode='level'加上p=5,只显示上面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, cmap='RdYlBu_r', standard_scale=1, # 对每一行做z-score标准化 figsize=(8, 8), row_colors=None # 可以传入一组颜色标记,给样本分组标注 ) plt.show()

standard_scale=1这个参数是精髓。它表示对每一行做 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_clusters=3)或者看图觉得"分两类挺顺眼",这在探索性分析里勉强能用,但一旦要给别人汇报,就得有数据依据。两个最常用的方法:肘部法则和轮廓系数。

肘部法则的思路是:随着 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_clusters=k, random_state=42, n_init=10) km.fit(X_scaled) inertia_list.append(km.inertia_) plt.figure(figsize=(8, 5)) plt.plot(K_range, inertia_list, marker='o') plt.xlabel('K值') plt.ylabel('簇内误差平方和 (inertia)') plt.title('肘部法则:选择K值') plt.grid(alpha=0.3) plt.show()

看这个曲线的时候,拐点有时候很明显,比如 K=3 之后曲线明显平缓了,那 K=3 就是答案。但真实数据往往没有这么理想,曲线可能是平滑递减的,这时候要结合业务判断。我在一个用户分群项目里就遇到过这种情况:肘部法则显示 K=5 附近有点拐,但业务方明确说"我们只需要三档用户:高、中、低",最后就取 K=3。结论是:肘部法则给的是统计上的建议,不是圣旨。

轮廓系数是另一个思路,它衡量每个样本与自己簇内其他样本的距离(内聚度)和与最近的其他簇样本的距离(分离度)之间的比例,取值范围 -1 到 1。系数接近 1 说明聚类效果好,接近 0 说明样本在两个簇的边界上,负值说明分错了簇。

from sklearn.metrics import silhouette_score sil_list = [] for k in range(2, 11): km = KMeans(n_clusters=k, random_state=42, n_init=10) 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, marker='o') plt.xlabel('K值') plt.ylabel('轮廓系数') plt.title('轮廓系数:选择K值') plt.grid(alpha=0.3) plt.show()

轮廓系数越高越好,取最大值对应的 K。但注意它也不是万能的:当数据本身没有明显簇结构时,轮廓系数可能整体都低,这种情况说明用聚类可能本身就不合适,或者需要换算法。我见过有人硬凑 K 追求高分,结果分出来的簇在业务上完全无法解释,这就是本末倒置了。

3.3 可视化细节:把丑图调成能看的图

聚类图的核心是信息准确,但也不能太丑。几个 matplotlib 绘制细节我单独拎出来说,都是平时容易忽略的。

第一个是画布和坐标轴比例。散点图如果横纵轴单位尺度差太多(比如横轴 0-1、纵轴 0-100),图形会被压缩成细长条,聚类之间的区分度很难看出来。两个办法:一个是plt.figure(figsize=(8, 6))手动控制画布宽高比,另一个是用plt.axis('equal')强制坐标轴单位长度一致。对于标准化后的数据,我会直接把figsize设成近正方形,保证散点不扭曲。

第二个是颜色和透明度。alpha=0.8是我常用的散点透明度,为什么不设成 1?因为当某个簇的样本点非常多且重叠严重时,全不透明会让后画的点完全盖住先画的点,根本看不出密度分布。加点透明度,两个簇有重叠的部分会显示为颜色混合,你能直觉地感知哪些区域分类不够清晰。

第三个是图例和标签。如果图上有多个子图(比如左边是原始数据、右边是聚类结果),记得给每个子图加标题和轴标签。leaf_rotation=90是树状图里常用的参数,防止叶子标签互相重叠。还有一个容易被忽略的: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_state=42, n_init=10
聚类图所有点聚成一坨未做标准化,某个特征量纲过大主导距离用StandardScaler或RobustScaler
ValueError: could not convert string to float数据里有非数值列用LabelEncoder或OneHotEncoder转换,或删除该列
散点图图例显示No handles with labels found只有一个scatter未设置label,但调用了legend()至少给一个散点加label参数
树状图底部标签叠成一团样本量太大,全部标签都显示用truncate_mode='level', p=5截断显示
图中文字显示为方框matplotlib 默认字体不支持中文配置rcParams['font.sans-serif']为中文字体
热力图颜色深浅看不出差异数据量纲差异大,未做标准化用standard_scale=1或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可能要等很久。这时候建议改用MiniBatchKMeans:

from sklearn.cluster import MiniBatchKMeans mbk = MiniBatchKMeans( n_clusters=4, batch_size=1024, n_init=3, random_state=42 ) mbk.fit(X_scaled)

MiniBatchKMeans每次随机抽一小批样本来更新质心,速度能快一个数量级,代价是聚类精度略降。在探索阶段,这个精度损失完全可接受,先用它快速画图看个大概,确定了 K 值和预处理方案之后,再跑一遍完整 KMeans 做最终结果。

聚类图也是一样的问题。当样本量超过几万,直接plt.scatter画几万个点,渲染会明显卡顿。两个技巧:一是把s参数调小,比如s=10,减少填充面积;二是可以先抽样画图,比如sample_idx = np.random.choice(len(X_scaled), size=5000, replace=False),确定最终方案后再全量出图。

5. 结语:一点个人体会

从我自己的经验来看,聚类图这个技能,难的不是 matplotlib 的 API 怎么调,而是你有没有真正理解这个图要回答的问题。算法参数调得再花哨,如果最终的图传达给业务方的信息是错的,或者根本没法解释,那前面的工作就等于白做了。

我之前有一个项目,聚类图画得非常"好看",簇与簇之间边界清晰、颜色分明,结果拿到业务评审会上,同事问了一句"那这个绿色的聚类群体,我们下一步该做什么?"我一下答不上来。后来反思,是因为我一直在追求统计指标和视觉表现,忽略了聚类结果必须映射回业务行动。从那以后,我每次画聚类图之前都会先问三个问题:这些簇在业务上意味着什么?它们之间的差异能用数据里的哪些特征解释?看完这张图该做什么决策?图是手段,不是目的。

如果你刚接触聚类可视化,我的建议是:先把最简单的散点聚类图跑通,用make_blobs造数据玩明白 KMeans 的参数;再去尝试树状图,理解层次聚类的合并逻辑;最后接触热力图,把矩阵数据的聚类可视化拿下。这个路径是循序渐进的,每一步都在为下一步打基础。

本篇文章已经覆盖了最常用的三种聚类图形式,也分享了我在实际项目中踩过的坑和积累的判断经验。剩下的,就是打开你的 Jupyter Notebook,找一份真实数据,动手跑一遍。画图的过程难免磕磕绊绊,但每解决一个问题,你对数据、算法和可视化的理解都会再深一层。

返回列表