Matplotlib colorbar与colormap完全指南:从入门到实战
写Python可视化相关的内容绕不开一个看起来很不起眼、实际上决定整张图质感的组件——colorbar。说得再直白一点就是色表colormap。很多新手一开始不重视它随手用默认的“jet”或者“viridis”等到图做出来才发现老板看不懂、论文审稿人说颜色误导、自己再看也想不起来当时标注的是什么。我在过去十几个数据可视化项目里几乎每一轮都有人过来问“你这个颜色是怎么调的”“为什么我的图一上色就糊成一片”。这篇就把Python里colorbar和colormap的完整细节一次讲清楚从分类、内置色表到截取、反转、自定义再到常见的坑和排查方法全部走一遍。适合刚入门Python可视化的人也适合已经用matplotlib画了很长时间但始终对配色心里没底的哥们儿。1. 从零认识colorbar它远不只是“配色好看”1.1 colorbar的本质是数据的“翻译器”很多人把colorbar当装饰品觉得加一个上去显得专业。但它的本质是一个映射关系把数据数值范围映射到颜色空间。你画的pcolor、imshow、contourf、scatter凡是带颜色映射的图本质上都是借助colormap把某个数值区间编码成肉眼可见的颜色。这个编码如果不合理直接后果就是本应该被一眼看出的分布规律被颜色扰动掩盖掉了。举个最典型的反面教材老的“jet”色表。它的色相变化里引入了亮度和饱和度的强烈波动导致同一数值附近的颜色跳变很剧烈人眼容易产生伪边缘、伪结构。很多老文献里的热力图就是用jet画的细看会发现颜色过渡有明显的“条纹感”这不是数据本身的问题而是色表编码方式的问题。正因为这个原因从matplotlib 2.0开始默认色表换成了viridis它属于感知均匀perceptually uniform的色表。所谓感知均匀指的就是数值每变化相同大小人眼感知到的颜色变化也大体相同。这个性质在做连续型数据分布图时非常重要。viridis、plasma、inferno、magma、cividis这几款都是按这个思路设计的实测下来比老花哨色表都要可靠。1.2 colormap的三大阵营顺序型、发散型、定性型要配好颜色先要搞清楚数据性质。我把colormap分成几大类对应不同场景顺序型Sequential适用于从低到高单向变化的数据比如温度、密度、概率、浓度。亮度从浅到深或从暗到亮逻辑非常自然。发散型Diverging适用于有明确中心值通常是零或平均值的数据比如温度距平、误差、涨跌幅。两侧颜色不同中间颜色最淡能清楚表达“高于中心”和“低于中心”。定性型Qualitative适用于类别数据比如不同城市、不同物种、不同分组。这类色表不表达大小只表达“不同”所以要用色相差明显的离散颜色不能用渐变。循环型Cyclic适用于角度、相位、一天内的时间这类首尾相接的数据。比如风向、24小时变化周期。这个分类不是死的但选错一定会让读者误会。比如用连续的蓝到红发散色表去画类别数据读者会自动脑补“红的大、蓝的小”如果类别本身没有大小关系这张图就在误导人。反过来用定性型色表画温度场也看不出高低梯度。所以拿到数据先问自己三个问题数据是连续还是离散有没有中心值有没有天然循环答完这三个问题选色表的方向基本就定了。2. Matplotlib内置色表全盘点从经典到冷门2.1 顺序型色表细节什么时候用viridis什么时候用“单色系”先说matplotlib 3.5之后查看所有内置色表最简单的方式import matplotlib.pyplot as plt cmaps plt.colormaps() for name in cmaps: print(name)plt.colormaps()会返回一个列表里面是所有可用色表的名字。老一点的版本里很多人用plt.cm.datad或dir(plt.cm)那个方式也能看但列表里会有很多内部对象不如现在的接口干净。顺序型色表里最稳妥的还是viridis、plasma、inferno、magma、cividis这五个兄弟。它们的共同点是感知均匀、无障碍友好色盲也能分辨。区别在于整体色调viridis偏蓝绿黄plasma偏紫红黄inferno和magma偏黑红黄cividis是专门为色觉障碍者优化的蓝黄渐变。如果再细分一般地图、科学可视化里我用viridis最多因为它暗部和亮部对比明显画地形、密度场、命中率热力都合适。plasma和magma在偏暗的底图上更突出画星空图、夜间灯光数据时视觉重心更清晰。cividis适合要打印成黑白或要满足无障碍标准的场合。除了这五个还有一系列“老牌单色系”色表比如Blues、Greens、Oranges、Reds、Purples、Greys以及带色相的YlOrRd、BuPu、GnBu这些。它们的共同特点是同色系渐变层次干净、不花哨画业务图表、论文配图时非常百搭。我个人的习惯是需要强调某一个方向幅度的数据例如用带色相的顺序型YlGnBu来画降水分布从浅黄到深蓝逐级加深需要低调、不想抢内容风头时就只用Reds或Blues。2.2 发散型色表有正有负的数据不要乱配发散型色表的经典代表有RdBu、RdYlBu、Spectral、coolwarm、bwr、seismic、PiYG、PRGn等。它们的共同结构是两端颜色深、中间颜色浅。中间这个浅色就是“零值”或“基准值”所在的位置。什么场景用哪种这里有几个经验地理气象类数据温度距平、降水偏差常用RdBu或RdBu_r。蓝表示偏低、红表示偏高符合多数人的直觉。金融或经济类涨跌数据红涨绿跌是国内习惯但欧美常用绿涨红跌画图前先确认观众背景。coolwarm和bwr是偏中性、低饱和度的发散色表适合科研论文里温和展示正负差异不会太刺眼。seismic的红蓝对比最强适合强调极端异常值画地震、突变类数据效果突出。发散型色表有个大坑如果你忘记设置vmin和vmax而数据本身不对称比如-1到100colorbar的中心颜色会落在数值范围的中间而不是零值。这样读者会认为“0附近”是那个浅色区实际显示的是50附近整个图的语义就错了。这个下面实操部分会详细展开。2.3 定性型色表类别数据别乱用渐变定性型色表的核心是“类间差异清晰”。matplotlib内置的有tab10、tab20、tab20b、tab20c、Set1、Set2、Set3、Paired、Accent、Dark2、Pastel1、Pastel2等。实际项目中tab10和tab20最常用因为它们的颜色来自Tableau的设计饱和度适中相邻色相区分明显。Paired适合天然成对出现的类别比如对照组和实验组、男女分组、治疗前后。Dark2颜色偏暗适合浅色背景上放大量元素。Pastel系列的饱和度高但明度高适合做PPT背景柔和那种风格。类别少了还好办一旦类别超过10个任何定性型色表都会出现视觉上难以区分的颜色。我的处理办法是先减少类别把不重要的小类合并成“其他”或“未知”这一类再用颜色较浅的灰色或单色表示。如果类别数量真的压缩不下来就用柱状图或分面图替代单张散点图别硬塞进一张图里。这个我在后面“常见问题”部分还会再提。2.4 循环型和其他特殊色表循环型色表主要就是twilight、twilight_shifted和hsv。twilight的感知均匀性比hsv好很多适合画角度、相位、风向、时间周期这类数据。hsv饱和度过高颜色之间跳跃感强不适合做连续数据映射除非你真的要做那种“彩虹大杂烩”式的展示否则不推荐。另外还有几个特殊用途色表值得一提turboGoogle设计的彩虹色表比老jet平滑很多但仍然是彩虹风格适合“演示用”和“大范围对比用”不适合严谨的定量分析。cubehelix这个色表很特别它从暗到亮、同时色相螺旋式变化适合打印成灰度图后依然能保持顺序感。画天文数据时经常见到它。terrain、gist_earth、ocean模拟地形地貌的专用色表适合画海拔、水深、地形分类图。flag、prism配色跳跃性极强视觉冲击大但基本没有定量意义一般只适合装饰性效果。matplotlib的colormap列表看起来很多真正用顺手的也就十几个。我的建议是不要追求用遍所有色表先掌握五六个核心的再根据项目场景拓展这样比盲目翻列表效率高得多。3. 动手实践在Python里把色表用明白3.1 5分钟快速查看并试用所有内置色表想知道某个色表长什么样子最简单的办法是直接生成一个渐变预览条import matplotlib.pyplot as plt import numpy as np def show_cmap(cmap_name): fig, ax plt.subplots(figsize(6, 1)) grad np.linspace(0, 1, 256).reshape(1, -1) ax.imshow(grad, aspectauto, cmapplt.get_cmap(cmap_name)) ax.set_xticks([]) ax.set_yticks([]) ax.set_title(cmap_name) plt.show() show_cmap(viridis) show_cmap(RdBu) show_cmap(tab20)这里我用了np.linspace生成一个从0到1的渐变数组再通过imshow的cmap参数渲染出来。你不需要真造一份有物理意义的数据纯用来认色表非常快。有一点要提醒新版本matplotlib里plt.get_cmap(name)已经被标记为过期推荐写法是matplotlib.colormaps[name]或matplotlib.colormaps.get_cmap(name)。为了兼容我一般写成下面这样from matplotlib import colormaps cmap colormaps[viridis]如果还是习惯plt.get_cmap也能跑但控制台会一直提示被弃用。建议尽早改掉这个习惯。3.2 完整示例热力图、等高线图与3D图三连直接上一个综合案例把主流的颜色映射场景都覆盖到。import numpy as np import matplotlib.pyplot as plt from matplotlib import colormaps # 造一组二维数据模拟地形高度 x np.linspace(-3, 3, 300) y np.linspace(-3, 3, 300) X, Y np.meshgrid(x, y) Z np.exp(-(X**2 Y**2)) * np.cos(4*X) * np.sin(4*Y) # 1) imshow热力图 fig, ax plt.subplots(1, 3, figsize(15, 4)) im ax[0].imshow(Z, extent[-3, 3, -3, 3], originlower, cmapcolormaps[viridis], aspectauto) ax[0].set_title(imshow viridis) fig.colorbar(im, axax[0], shrink0.8) # 2) contourf等高线填充图 cf ax[1].contourf(X, Y, Z, levels20, cmapcolormaps[coolwarm]) ax[1].set_title(contourf coolwarm) fig.colorbar(cf, axax[1], shrink0.8) # 3) 3D曲面图 ax3 fig.add_subplot(1, 3, 3, projection3d) surf ax3.plot_surface(X, Y, Z, cmapcolormaps[magma], linewidth0, antialiasedTrue) ax3.set_title(plot_surface magma) fig.colorbar(surf, axax3, shrink0.6, aspect15) plt.tight_layout() plt.show()这段代码执行后你会看到三种不同的可视化效果。imshow适合规则网格数据contourf适合看轮廓和趋势plot_surface适合三维形态展示。三种图加colorbar的方式都差不多fig.colorbar(绘图对象, ax对应坐标轴)就行。画3D图时额外注意colorbar的shrink和aspect参数。3D图自身坐标轴占的空间和2D不一样colorbar默认高度可能太长或太短我一般会把shrink设成0.6~0.8aspect设10~15出来的比例比较协调。3.3 截取、反转与拼接色表让内置色表为你服务很多时候内置的完整色表范围太大我只想用其中一部分。比如我用viridis但只想取它中间偏亮的那段用来和另一张图保持统一的色彩感受。这时可以按索引截取import matplotlib.colors as mcolors full_cmap colormaps[viridis] truncated mcolors.ListedColormap(full_cmap(np.linspace(0.3, 0.9, 256))) # 使用截取后的色表 plt.imshow(data, cmaptruncated)原理很简单先调用full_cmap(np.linspace(...))把一个连续色表在指定的数值区间上采样成256个RGBA颜色再用ListedColormap重新包装成一个新的离散色表。这样新色表只保留原色表的0.3到0.9区间。反转就更常见了。只要在色表名后面加_r后缀即可plt.imshow(data, cmapviridis_r) plt.imshow(data, cmapRdBu_r)_r后缀适用于所有内置色表也适用于你用ListedColormap创建的对象。它的意义不只是把颜色倒过来还决定了数据从低到高时颜色是从深到浅还是从浅到深能直接影响读者对趋势方向的判断。拼接两个色表也是实际中常遇到的需求。例如我想把“蓝到白”和“白到红”拼成一个发散型色表中心是白色import numpy as np import matplotlib.colors as mcolors # 采样蓝到白、白到红各256个颜色 blue_white colormaps[Blues_r](np.linspace(0.2, 1.0, 256)) white_red colormaps[Reds](np.linspace(0.2, 1.0, 256)) # 拼接 colors np.vstack([blue_white, white_red]) custom_diverging mcolors.ListedColormap(colors, namecustom_diverging)这里的关键是采样区间的选择。如果两侧都从0到1采样拼接处颜色会偏深中心不够白发散感大打折扣。我通常会从0.2左右开始截保留白到中间的那段过渡这样拼接后中心是真正的浅色视觉上才平滑。3.4 自定义colormap从颜色列表到渐变方案除了截取拼接自己从零定义colormap也很常用。最简单的场景是品牌色适配比如公司VI色是深蓝和金色你想做一张和公司风格统一的图。方法一LinearSegmentedColormap适合颜色数量少的平滑渐变from matplotlib.colors import LinearSegmentedColormap colors [#1a3a5c, #2e6f9e, #f0c75e] brand_cmap LinearSegmentedColormap.from_list(brand, colors, N256)from_list会自动在给出的几个颜色之间做线性插值生成256个过渡色。想要调整插值位置可以用字典形式控制每个颜色在0到1区间的位置cdict { red: [(0.0, 0.1, 0.1), (0.5, 0.3, 0.3), (1.0, 0.9, 0.9)], green: [(0.0, 0.2, 0.2), (0.5, 0.8, 0.8), (1.0, 0.8, 0.8)], blue: [(0.0, 0.4, 0.4), (0.5, 0.6, 0.6), (1.0, 0.2, 0.2)] } custom LinearSegmentedColormap(custom, cdict)这个字典里每一列从左到右分别是位置、左侧颜色分量值、右侧颜色分量值。一般情况下左右两侧保持一致就行不推荐把两个值设得不同否则会出现颜色跳变的段。方法二ListedColormap适合离散类别颜色from matplotlib.colors import ListedColormap discrete_cmap ListedColormap([#e74c3c, #f39c12, #2ecc71, #3498db])这种方式创建的色表在colorbar上显示为一段一段的色块适合类别型数据。配合BoundaryNorm使用可以让每个类别精确对应数值区间import matplotlib.colors as mcolors bounds [0, 1, 5, 10, 20] norm mcolors.BoundaryNorm(bounds, discrete_cmap.N) plt.scatter(x, y, cvalues, cmapdiscrete_cmap, normnorm)这样做的好处是colorbar的刻度会正好落在边界位置不会出现半个色块悬空的尴尬情况。4. 常见问题与排查技巧实录4.1 colorbar的范围和图像对不上这是最常遇到的问题之一。明明数据最大值是100colorbar却只显示到80或者数据的中心零值没有映射到颜色中心。根源通常是vmin和vmax没有设置。matplotlib在多数情况下会根据数据的min和max自动设定norm范围但当你对数据做了裁剪、掩膜、或者使用不同子集作图时自动范围就不一定是你要的范围。解决办法是显式传入vmin和vmaxplt.imshow(data, cmapRdBu, vmin-2, vmax2)对发散型色表这里尤其重要。你还应该用diverging norm或者额外交代中心位置。matplotlib里可以这样强制中心为零from matplotlib.colors import TwoSlopeNorm norm TwoSlopeNorm(vmin-1, vcenter0, vmax3) plt.imshow(data, cmapRdBu, normnorm)TwoSlopeNorm保证了0值正好落在色表中心即使vmin和vmax的绝对值不对称。这是处理正负不对称数据最优雅的方案我用它解决过好几次reviewer提出“中心颜色不对”的问题。4.2 保存PDF或矢量图后颜色出现色块或偏差这个问题常见于保存矢量图时使用了透明通道或高dpi设置。如果你发现PNG导出正常PDF导出后颜色断裂、边缘出现白线多半是反锯齿和alpha通道的问题。几个可行方案保存时关闭透明背景plt.savefig(out.pdf, transparentFalse)增加dpiplt.savefig(out.pdf, dpi300)使用pdf或svg输出时如果还出现白线试试plt.savefig(out.pdf, facecolorwhite)colorbar本身的显示问题还可能是因为shrink、aspect参数设置不当导致colorbar拉伸变形。变形本身不影响数据映射但会让数值刻度看起来不真实影响阅读体验。一般把aspect设8~20shrink设0.7~1.0之间绝大多数情况下都能得到适中比例。4.3 数据是离散类别但colorbar显示成了连续渐变这种问题的典型表现是类别图颜色像彩虹一样渐变的完全看不出是不同类。原因很简单——你把类别数据传给了连续型colorbar比如直接用了viridis而没用定性型色表。正确做法是使用离散色表并通过norm限制颜色边界import matplotlib.colors as mcolors labels np.array([0, 1, 2, 3, 4]) cmap ListedColormap([#1f77b4, #ff7f0e, #2ca02c, #d62728, #9467bd]) norm mcolors.BoundaryNorm(np.arange(-0.5, 5, 1), cmap.N) plt.scatter(x, y, clabels, cmapcmap, normnorm) plt.colorbar(ticksnp.arange(5))这里BoundaryNorm把边界设在[-0.5, 0.5, 1.5, ...]的位置确保每个整数标签对应一个色块中心。如果类别之间没有顺序关系但你还想用渐变色表我建议你停下来重新想一下同一个渐变色表本身就在暗示“量”的增减用在类别上哪怕颜色不同读者也会下意识地给不同类别排序。这个问题是设计层面的不是代码层面的换了色表也救不回来。4.4 colorbar标签显示科学计数法或者想改成百分比colorbar默认根据数值范围自动选择刻度格式。当数据跨度很大时会显示成1e6、1e-4这种样式有些场景是合理的但在汇报材料里并不友好。手动格式化colorbar刻度有两种常用方式方式一用固定格式import matplotlib.ticker as ticker cb plt.colorbar(im, axax) cb.ax.yaxis.set_major_formatter(ticker.FormatStrFormatter(%.1f))方式二显示成百分比cb plt.colorbar(im, axax) cb.ax.yaxis.set_major_formatter(ticker.PercentFormatter(xmax1.0))PercentFormatter会把刻度值乘以100并加百分号。如果你的数据本身就是0到1的小数xmax1.0即可。如果数据是0到100但你想显示带百分号就把xmax设成100。4.5 不同子图共用同一个colorbar怎么做才统一做多子图对比时最忌讳每个子图自动用自己的数值范围这样会导致颜色在不同子图间含义不一致读者根本没法横向对比。解决办法是给所有子图的绘图调用传相同的vmin和vmaximport numpy as np import matplotlib.pyplot as plt fig, axes plt.subplots(1, 3, figsize(12, 4), shareyTrue) data_list [data1, data2, data3] vmin min(d.min() for d in data_list) vmax max(d.max() for d in data_list) for ax, data in zip(axes, data_list): im ax.imshow(data, cmapviridis, vminvmin, vmaxvmax) fig.colorbar(im, axaxes, shrink0.8) plt.show()当所有子图共用同一个vmin和vmax后最后只要把最后一个im对象传给colorbar即可。如果你想更精细地控制也可以用fig.colorbar(im, axaxes.tolist(), locationright)明确告诉colorbar画在哪里。如果几张图的量纲差异实在太大共用范围会导致某张图颜色几乎不变这时候你需要思考的不是怎么调整colorbar而是是否应该用归一化数据、差分数据、或分开展示而不是强行共用色表。5. 几个实战中的配色策略建议前面讲了原理再补充一些我在实际项目中沉淀下来的策略。第一套策略是“默认优先”。只要你对可视化效果没有特别强烈的设计想法就先选viridis或者cividis。这两个色表在绝大多数环境PPT投影、手机屏幕、论文黑白打印下都表现稳定不容易翻车。等确实遇到需要突出中心值RdBu、需要类别色tab10、需要循环色twilight的场景再去换。第二套策略是“先定语义再定颜色”。比如画降水多数人直觉里蓝色代表强降水所以用蓝色系或蓝绿色系做顺序型而不是用红色系。画海拔绿色是低处、棕白色是高处用terrain类色表更符合常理。画涨跌国内习惯红涨绿跌那发散型色表 center0 时把红放在正方向绿放在负方向。语义比好看重要得多。第三套策略是“考虑灰度打印”。如果论文、报告可能需要黑白打印彩色图会变成灰色层次图这时选色表要看它的亮度变化是否均匀。viridis、cubehelix、cividis在转灰度后依然有比较清晰的梯度。相比之下很多高饱和度色表转灰度后会糊成一团。一个快速检查办法是plt.cm.get_cmap(viridis)拿到的颜色数组把所有RGB值转成灰度值看看排列后是不是单调变化。实际项目中我直接用matplotlib自带的色表可视化界面或者提前用ColorMaps的亮度曲线检查几分钟就能判断。第四套策略是“不迷信默认colorbar的位置”。colorbar默认在右侧但在一些横幅式宽图或分面图里把colorbar放在底部、或者放在某个特定子图旁会更协调。常见做法# 放在底部 fig.colorbar(im, axaxes, orientationhorizontal, fraction0.05, pad0.15) # 只挂在某一个子图旁 fig.colorbar(im, axaxes[2], shrink0.8, locationright)orientationhorizontal配合fraction和pad能让底部colorbar高度、边距可控不挤压上方的主体图像。location参数在较新的matplotlib里支持left、right、top、bottom灵活很多。还有一个小点colorbar的label不要写单位就完事最好把“含义”和“单位”都写清楚。比如“温度°C”就比“°C”更容易让读者理解。如果数据经过归一化或对数变换一定要在label里注明“log10尺度”或“归一化到0-1”。这是我被审稿人质疑过好几次后踩出来的教训细节决定professionalism。我自己在实际操作里的体会是colorbar这层颜色映射看着不起眼但它直接决定了读者从图里读到的第一个信息是不是对的。花10分钟想清楚数据语义、选对色表种类、设好vmin/vmax和norm比花一个小时去后期调色要有效得多。最后再分享一个小技巧如果你不确定某张图用哪个色表更合适就把同一个图用viridis、RdBu、cividis各出一个小图并排放到屏幕上距离屏幕两米看最顺眼的就是最适合你的。这个土办法帮我筛掉了不少“近距离看很好、远距离看很乱”的配色方案。