做数据分析的人应该都有过这种体验:df.corr()一甩,控制台出来一整排密密麻麻的小数,矩阵稍微大一点就眼花缭乱,根本看不出哪个特征和哪个特征关系紧密。直到我开始用seaborn.heatmap,才发现原来一张热力图就能把这些数字变成一眼能读懂的图。这个工具的价值说白了就一句话:把二维表格里的数值按照大小映射成颜色,让你用直觉就能捕捉数据规律。
这篇文章我打算把seaborn.heatmap从零到实战讲透,包括它到底能干什么、画之前的数据要长什么样、几个必调的核心参数、三个最常见的业务场景,还有我自己踩过的坑和排查方法。不管你是第一次接触这个函数,还是已经画过几版但总觉得图不够顺眼,这篇都可以直接当工具手册来翻。
1. 为什么是热力图:先搞清楚它能解决什么问题
1.1 从"一行数字"到"一眼看懂"
热力图本质上是把二维数组的数值映射成色块。数值小的地方颜色暗,数值大的地方颜色亮,中间值落在某个中间色调上。这个思路很像温度计:低温是蓝色,高温是红色,一眼扫过去就知道哪里热哪里冷。
在数据分析里,最典型的应用是相关性矩阵。假设你有8个特征,corr()得到的矩阵有64个数字,人眼逐行扫一遍很费劲。但把它丢给sns.heatmap(df.corr()),你会立刻看到对角线上有一条亮色从左上到右下,某些特征的交叉处颜色明显深,某些地方浅得发白。此时你不需要精确到小数点后两位,你只需要知道"哦,这两列是正相关而且很强"就足够了。
我见过很多初学者一上来就盯着数字看半天,最后得出的结论还是模模糊糊。热力图的价值不是替代精确计算,而是帮你建立整体直觉。先看图,再回到数字里确认,这个顺序效率高得多。
1.2 和plt.imshow相比,优势到底在哪
有经验的读者可能知道,matplotlib 里imshow也能画出类似的矩阵色块图。那为什么要用 seaborn 的 heatmap?核心区别在于,heatmap是专门为"数据表格"设计的。
imshow默认按图像的逻辑处理数据,需要你自己处理坐标轴标签、数值范围映射、颜色条等一堆细节。heatmap则直接把 DataFrame 传进去,index 自动变成 y 轴标签,columns 自动变成 x 轴标签,它还内置了颜色条、标签布局、掩码这些常见需求的处理。
用一个不太严谨但很好理解的类比:imshow像一块裸的画布,什么都要自己调;heatmap像是画布上带了辅助线、调色板和贴纸,专治"数据表格可视化"这一种场景,所以更顺手。
1.3 什么场景适合,什么场景不建议用
heatmap最适合的输入是二维规则矩阵,也就是说每一行和每一列都有明确含义。常见场景包括:
- 特征相关性矩阵
- 分类模型的混淆矩阵
- 基因表达量或用户行为数值矩阵
- 城市间的距离矩阵
- 时间 × 类别的数值透视表
但如果你只是画一条折线或者一堆散点,那该用lineplot或scatterplot就换工具,没必要硬上热力图。另外,如果矩阵特别稀疏、大部分格子是空值或0,热力图很容易变成一片“殖民地地图”,此时优先考虑稀疏矩阵专门的呈现方式,比如只显示非零部分,或者干脆用交互式表格。
2. 画之前要做的准备:数据格式和第一张图
2.1 数据格式三原则
heatmap对数据格式要求其实很简单,但正因为简单,很多人栽在第一步。它接受三种常见输入:
- 二维
DataFrame:最推荐,行和列名字会自动变成标签 - 二维
numpy.ndarray:可以用,但标签需要自己另传 - 二维列表:能用,但同样没有标签
实际编码时我几乎总是用 DataFrame。就算原始数据是数组,我也会临时包一层 DataFrame 再传进去。这样xticklabels和yticklabels的显示问题就一并解决了,不用额外处理。
另外有一个容易忽略的细节:数据必须是数值型。如果 DataFrame 里有字符串列,heatmap会直接报错或画出一堆空白。遇到这种情况,可以先df.select_dtypes(include=['number'])把数值列选出来,或者用pd.to_numeric先把类型转好。
2.2 一行代码画第一张热力图
先跑一个最小可复现的示例,感受一下整体流程:
import numpy as np import pandas as pd import matplotlib.pyplot as plt import seaborn as sns # 造一份模拟数据:10行8列 rng = np.random.default_rng(42) data = pd.DataFrame( rng.normal(size=(10, 8)), columns=[f'特征_{i}' for i in range(8)] ) # 人为让最后一列和第一列正相关 data['特征_7'] = data['特征_0'] * 0.8 + rng.normal(size=10) sns.heatmap(data) plt.show()运行这段代码,你会看到一张默认配色的热力图,每一格颜色深浅代表数值大小,右侧自动生成颜色条。这个过程里 seaborn 自动完成了三件事:把 DataFrame 的列名放到 x 轴、把行索引号放到 y 轴、把数值映射到颜色。
注意:如果你的 seaborn 是最新版,画出来的默认样式可能会带白色网格线,这是正常的,不是报错。
2.3 annot 和 fmt:把数字写进格子里
默认热力图只有颜色,没有数字。想在格子里同时显示数值,需要打开annot:
sns.heatmap(data, annot=True)打开后你会发现数字确实显示出来了,但可能带着好几位小数。这是fmt参数在起作用,它的默认值是'.2g',也就是保留两位有效数字的通用格式。
实际使用时分三种情况:
- 显示原始数值,希望保留两位小数:
fmt='.2f' - 显示整数分类计数,比如混淆矩阵:
fmt='d' - 显示百分比:
fmt='.0%'
我自己用得最多的是'.2f'和'd'。相关性矩阵通常保留两位小数就够了,混淆矩阵必须显示整数,否则每个格子会变成7.000000这样的难看样子。
annot_kws参数能微调数字样式,比如改字号和颜色。我常用的设置是annot_kws={'size': 9},格子小的时候避免文字挤成一团。文字颜色可以在annot_kws里传color='white',适合深色背景格。
2.4 颜色映射 cmap:不要乱选,先想清楚语义
cmap决定颜色从低到高怎么变化,这是热力图观感的关键。seaborn 里可以传任何 matplotlib colormap 的名字。
我一般按语义选:
- 数据有正有负、有方向性(比如相关性):用发散色图,最常用
'RdBu_r'或'coolwarm' - 数据从0到某个最大值(比如计数、距离):用顺序色图,比如
'Blues'、'viridis'、'YlGnBu' - 数据是分类标签或布尔值:用少量离散颜色,或者直接
'Greys'
有个新手容易犯的错:所有数据都用viridis默认配色。倒不是说不行,但如果你画的是一个以0为中心的相关性矩阵,从紫色到黄色你很难直觉判断正负方向。相关性矩阵我最常用'RdBu_r',因为红色代表正相关,蓝色代表负相关,暖色调=正,冷色调=负,看一次就记住了。
2.5 范围三件套:vmin、vmax 与 center
热力图的颜色映射范围,默认跟随数据的最小值和最大值。这带来一个问题:当你想对比多张图时,每张图颜色尺度不一样,很难公平对比。
解决方法是用vmin和vmax手动固定范围:
sns.heatmap(data, vmin=-3, vmax=3)这样颜色深浅的"刻度"就固定了,几张图放在一起一目了然。这里有个细节值得说:vmin/vmax 控制的是颜色条两端对应的数据值,中间值按线性映射。如果数据里有极端离群值,比如某个格子是100,其他都在0到1之间,默认映射会把所有正常值都压成同一个深色,这时可以开robust=True,它会用分位数代替极值来设定范围,抗离群值能力会好一些。
center参数专用于发散色图,它的作用是设定颜色条的中心点。比如相关性矩阵,范围本身是从-1到1,0是天然中心。设置center=0后,颜色条会以0为分界,左右两侧颜色相对称。如果不设置center,seaborn 会以数据均值为中心,可能导致正负颜色面积不对称,语义就拧巴了。
3. 从"能看"到"好看":进阶样式调整
3.1 先做全局主题设置,再画图
我见过很多人的热力图其实是"功能完整,但颜值欠费"。提升观感第一步不是调 heatmap 的参数,而是设好 seaborn 的全局主题。
sns.set_theme(style='white', font_scale=1.1)style='white'会把背景置成白色,去掉默认的灰色网格线,热力图会显得干净很多。font_scale控制全局字体大小,画布不大的时候调到1.1左右,标签会清晰一些。
如果你是做 PPT 用的图,我还会先把matplotlib的字体和负号显示问题解决掉,这个在第五章专门讲。总之,先定主题再画图,不会吃亏。
3.2 格子的形状与间距:square、linewidth、linecolor
默认情况下热力图的格子是长方形,会填满整个画布。想让格子变成正方形,用:
sns.heatmap(data, square=True)相关性矩阵我强烈推荐square=True,因为它本质是一个方阵,正方形格子配合对称的标签,视觉上最舒服。对于行列数不等的矩阵,square=True通常会导致画布被拉长或压扁,此时可以只把它当作一个风格偏好来用。
镂空效果由linewidths控制。它设置格子之间线条的宽度,配合linecolor控制线条颜色。常见做法:
sns.heatmap(corr, square=True, linewidths=0.8, linecolor='white')这个组合能让格子之间出现一圈白线,类似表格描边,对比度高,看久了不累。需要注意:新版 seaborn(0.13及以上)把参数重命名为linewidth,linewidths仍能用但会提示弃用。我的建议是新的代码直接写linewidth,省得未来版本报错。
3.3 把轴标签转个方向
当列名很长或者数量很多时,x 轴标签会打架,叠在一起完全看不清。解决办法有两个。
第一种是直接旋转刻度标签:
plt.xticks(rotation=45, ha='right') plt.yticks(rotation=0)第二种是用xticklabels参数控制标签是否显示,配合旋转:
sns.heatmap(data, xticklabels=True, yticklabels=True)我的经验是:20列以内可以全部显示标签并旋转45度;超过20列,直接设xticklabels=False或者隔几个显示一个,否则就算旋转了也是一堆密集文字。另外,ha='right'这个细节很重要。旋转45度后默认的对齐方式会让标签看起来错位,设置为右对齐之后,标签末端对齐到刻度点,整齐很多。
3.4 用 mask 隐藏不想看的部分
不是所有格子都值得展示。最常见的是相关性矩阵,它天生对称,上三角和下三角内容基本重复。只显示一半能让图更轻量,用mask参数很容易实现:
corr = data.corr() mask = np.triu(np.ones_like(corr, dtype=bool)) sns.heatmap(corr, mask=mask, annot=True, cmap='RdBu_r', center=0)np.triu(np.ones_like(corr, dtype=bool))会生成一个上三角为 True 的矩阵,mask里为 True 的位置对应格子不显示颜色和数字。同理,想隐藏下三角就把np.triu换成np.tril。
mask 的实际场景非常多,比如只想展示达到显著性的格子,或者只对某些区块感兴趣,都可以构造自定义布尔数组。这里要特别注意:mask 的 shape 必须和 data 一致,否则报错或结果完全错乱。
3.5 控制 colorbar,以及多图并排
colorbar 不是装饰品,它是热力图的"图例"。默认情况下 seaborn 自动加在右侧。想调整它的高度、宽度或方向,用cbar_kws:
sns.heatmap(data, cbar_kws={'label': '相关系数', 'shrink': 0.8})shrink控制颜色条长度占图的比例,0.8 是我常用的值。如果想横着放,加'orientation': 'horizontal'。如果完全不需要颜色条,比如缺失值可视化,直接cbar=False。
多图并排的场景更常见。比如你想对比两个模型预测结果的差异,或者对比训练集和验证集的相关性结构。这时需要把 heatmap 放进子图:
fig, axes = plt.subplots(1, 2, figsize=(12, 5)) sns.heatmap(corr_train, ax=axes[0], cmap='RdBu_r', center=0, vmin=-1, vmax=1) sns.heatmap(corr_test, ax=axes[1], cmap='RdBu_r', center=0, vmin=-1, vmax=1)多图对比时一定要统一vmin、vmax和center,否则两幅图颜色含义不一致,对比就没有意义了。还有一个小技巧:当两个子图共享色标时,可以让其中一个cbar=False,只保留一个 colorbar 在边上,版面更干净。
3.6 热力图和普通图表混排
一个容易忽略却非常实用的点是,heatmap 本质还是一个 axes 上的图,可以和其他图表自由组合。我经常在论文里用左图热力图、右图折线图或箱线图并排展示同一个指标的不同侧面。
fig, axes = plt.subplots(1, 2, figsize=(14, 4)) sns.heatmap(matrix, ax=axes[0], cmap='YlGnBu') sns.lineplot(data=trend, ax=axes[1])4. 三个高频实战场景拆解
4.1 场景一:相关性矩阵热力图
这应该是 heatmap 最出圈的使用场景。完整性流程可以这样写:
# 只取数值列,避免字符串列导致报错 numeric_cols = df.select_dtypes(include=[np.number]).columns corr = df[numeric_cols].corr() # 画图 plt.figure(figsize=(10, 8)) sns.heatmap( corr, annot=True, fmt='.2f', cmap='RdBu_r', center=0, vmin=-1, vmax=1, square=True, linewidth=0.5, linecolor='white' ) plt.title('特征相关性热力图') plt.tight_layout() plt.show()这段代码里我把vmin=-1, vmax=1写死,是因为相关性矩阵的取值范围天然就是-1到1。如果不写死,seaborn 会根据实际数值自适应,在某些没有极强相关的矩阵里,颜色对比度会很弱,看起来灰蒙蒙一片。写死后,颜色从亮红到亮蓝整条色带都在起作用,对比度会好很多。
4.2 场景二:混淆矩阵热力图
做分类模型时,混淆矩阵是必看的图。大部分教程用matplotlib的imshow或table来画,步骤繁琐。用 heatmap 可以三行搞定:
from sklearn.metrics import confusion_matrix cm = confusion_matrix(y_true, y_pred) labels = ['类A', '类B', '类C'] sns.heatmap( cm, annot=True, fmt='d', cmap='Blues', xticklabels=labels, yticklabels=labels, cbar_kws={'label': '样本数'} ) plt.xlabel('预测标签') plt.ylabel('真实标签') plt.show()混淆矩阵的格子是计数,不可能是小数,所以fmt='d'必须写。cmap='Blues'的语义也很明确:颜色越深代表数量越大,一眼就能看出哪些类被大量混到了一起。我还习惯把annot_kws={'size': 12}调大字号,因为混淆矩阵的数值通常只有几行几列,格子空间比较大,数字太小显得空。
4.3 场景三:缺失值可视化
数据清洗阶段,df.isnull()会得到一个全布尔值的矩阵。想快速知道哪些行哪些列缺失最严重,可以画缺失值热力图:
missing = df.isnull() plt.figure(figsize=(12, 6)) sns.heatmap(missing, cbar=False, cmap='Greys') plt.title('缺失值分布') plt.show()布尔值会被自动映射成0和1,颜色只有黑白两阶,有缺失的位置显示灰色或黑色。看着这张图,你能立刻判断缺失是随机散布还是集中在某些列。这比df.isnull().sum()那串数字直观得多。
如果行数特别多(比如几十万行),直接画会糊成一片。此时可以先按列计算缺失率,用条形图代替热力图,或者先对行做采样再画。我在一个实际项目里遇到过20万行数据画缺失热力图,画出来就是一条横贯图的灰线,没有任何有效信息,后来改用按行的统计量才看出规律。
5. 新手最容易踩的坑:常见问题排查记录
5.1 画出来全是空白或报错
最常见的原因有两个:DataFrame 里有非数值列,或者数据全是 NaN。前者用select_dtypes过滤即可。后者要检查数据是否有干净的值,必要时先df.ffill()填充或者删除全空行。
排查优先序:先打印
data.dtypes看类型,再用data.isnull().sum().sum()看有没有 NaN,最后再考虑是不是参数错了。
5.2 开了 annot 却没有数字
如果你设置annot=True但格子内空白,十有八九是fmt格式和数据类型冲突。比如明明是整数,你写了fmt='.2f',这是能显示的。更隐蔽的问题是数据里带 NaN,NaN 的格子显示不出数字是正常表现。
第二个可能性是数字太小被其他元素遮挡,尤其是又加了annot_kws={'size': 5}导致字号过小。这时调大字号就行。
5.3 颜色太淡,看不出对比
颜色对比度弱通常是vmin/vmax范围被离群值拉大了。比如大部分数据在0到1之间,某个异常值是100,默认映射把0到1区域压缩成了几乎同一个色。解决方案是写死vmin/vmax或者使用robust=True。
还有一个容易被忽略的坑:当你使用coolwarm这类发散色图时,如果center没设置,seaborn 会把数据的平均值当中心,导致颜色分布不对称,看起来一片偏红一片偏蓝。发散色图一定要显式设置center。
5.4 中文标签变成方块
这是中文环境下最常见的乱码问题。热力图本身没有锅,锅在 matplotlib 的默认字体不支持中文。解决方式是设置中文字体:
plt.rcParams['font.sans-serif'] = ['SimHei', 'Microsoft YaHei', 'PingFang SC'] plt.rcParams['axes.unicode_minus'] = Falseaxes.unicode_minus是为了防止负号显示成方块。这个设置要在画图之前执行。如果你在服务器上跑代码,字体名可能不一样,先用matplotlib.font_manager查看系统有哪些中文字体再选。
5.5 mask 用错导致大片空白
mask要求传入一个和data同形状的布尔数组,True 表示隐藏。很多新手把 mask 和np.ma.masked_invalid混淆,传了掩码数组进去,结果所有格子都没了颜色。记住:mask 只是一个布尔数组,不需要包装成 masked array。
5.6 速查表
| 现象 | 原因 | 解决办法 |
|---|---|---|
| 全部空白 | 数据含 NaN 或类型不对 | 检查 dtype,填充或过滤 NaN |
| annot 无数字 | fmt 格式问题或格子为 NaN | 改成fmt='.2f'或fmt='d' |
| 颜色对比度弱 | vmin/vmax 被离群值拉大 | 手动固定范围或robust=True |
| 中文变方块 | 缺少中文字体配置 | 设置font.sans-serif |
| cbar 太长占空间 | 默认高度不协调 | cbar_kws={'shrink': 0.8} |
| 标签重叠 | 列数太多 | 旋转标签或减少展示数量 |
| 颜色方向误导 | 发散色图无 center | 显式设center=0 |
6. 从静态热力图到聚类热力图
6.1 为什么需要行和列聚类
热力图虽然直观,但当行或列本身有内在顺序时,原始排列常常不能突出数据规律。比如做基因表达量分析,100个基因在50个样本里的表达值如果按基因名的字母顺序排,画出来是一团乱麻。但如果我们按表达模式相似度重新排列行和列,就能看到清晰的区块结构——有的基因在样本A群高表达,在样本B群低表达,视觉上一分为二。
这种"按相似度重新排序"就是聚类。seaborn 提供clustermap把聚类和热力图合二为一,画出来的图会自动在左侧和顶部额外生成树状图(dendrogram)表示聚类关系。
6.2 最小可用案例
sns.clustermap( data, method='average', metric='euclidean', cmap='viridis', figsize=(10, 8) )method决定聚类的连接方式('average'、'ward'、'complete'等),metric决定相似度度量(欧氏距离、相关系数等)。默认情况下行和列都会聚类。如果你只想对行聚类、保留列原始顺序,用col_cluster=False。
clustermap返回的对象可以直接用来控制树的显示细节,比如设置叶子标签字号。实际项目中我经常结合z-score归一化之后再聚类,因为不同列可能存在量纲差异,不归一化聚类结果会偏向量纲大的特征。
6.3 什么情况下用 clustermap
clustermap适合行列数较多、且你怀疑存在分组结构的数据。比如:
- 用户对不同类型商品的消费金额(行是用户,列是商品)
- 不同时间点的基因表达量
- 不同区域在不同时段的关键指标
但如果你的行列本来就有明确顺序(比如按月排、按流程步骤排),强行聚类反而会打乱顺序,此时老老实实保留原排列画heatmap就好。聚类热力图的价值在于"发现结构",而普通热力图的价值在于"展示结构",两者定位不同。
我在实际项目里通常先画普通 heatmap 看一眼整体,再画 clustermap 看看有没有隐藏的聚类模式。两相对照之后,再决定最终呈现用哪种图。如果你刚开始用这个功能,建议先在同一份数据上对比这两个图,体会一下"重排列"带来的信息差异。
最后再分享一个小技巧:clustermap画完之后的图通常比较大,如果你要把聚类结果和对应原始数据一起看,可以在 clustermap 的row_cluster或col_cluster得到之后,把排序后的索引取出来,用这个顺序重新绘制普通 heatmap。这样既能保留聚类的排序结果,又不用每次都挂着一堆树状图,版面更灵活。