ARTICLE DETAIL

资讯详情

深耕商务建站与企业官网运营的一线实战洞察。

matplotlib箱线图自定义填充颜色:从原理到实战

matplotlib箱线图自定义填充颜色:从原理到实战 做数据分析这一行箱线图是逃不掉的基础图。我最早用它的时候matplotlib 默认画出来的箱体就是个白色带黑边的矩形放在报告里总觉得单调尤其是要对比三五个业务分组时光靠位置和形状去区分眼睛看久了容易花。后来开始研究怎么给箱线图自定义填充颜色才发现这里面的门道比我预想的多默认情况下 matplotlib 根本没打算让你直接填色需要先打开一个叫patch_artist的开关再配合boxprops参数才能控制箱体的facecolor。如果你已经在用 Python 做数据分析和可视化想画一张既专业又有辨识度的彩色箱线图这篇文章就是围绕这个目标展开的。我会从箱线图的基本原理讲起再用可复现的代码一步步演示如何实现自定义填充颜色、按分组上色、结合 seaborn 快速配色最后把我在实际项目中踩过的坑和排查方法一并整理出来。1. 箱线图到底在展示什么想给箱线图换颜色先得搞清楚你画的这个图是什么。箱线图不是简单的“一个盒子加两根须”它把一组连续型数值数据的分布情况浓缩成了几个关键统计量。理解这些统计量你才能在上色之后依然读出图表里的信息。1.1 五个统计量构成一份标准的箱线图会呈现五个关键位置通常称为五数概括。箱体上边缘和下边缘分别是上四分位数Q3和下四分位数Q1箱体中间那条线是中位数Q2从箱体延伸出去的两条须线则指向非异常范围内的最大值和最小值须线之外单独画的点就是被识别为离群值的数据点。我用一个具体数字举例。假设某门店一周的日销售额是 80、85、88、90、92、95、200 这 7 个数据排序后中位数是 90Q1 是 85Q3 是 95IQR 等于 10。按照常见的 1.5 倍 IQR 规则下边界是 85 - 15 70上边界是 95 15 110那么 200 就会被标成异常点须线最右端只画到 95。箱体的高度也就是 Q1 到 Q3 这段区间集中了中间 50% 的数据这比单纯报一个平均值更能反映数据是否稳定。实际工作中我经常用箱线图做三件事第一快速判断一组数据是左偏还是右偏第二比较不同分组之间的中位数和离散程度第三找出异常值并跟进原因。颜色本身不改变这些统计结果但合理的配色能让图表更快被读懂尤其是当你要在汇报时同时展示七八个分组时。1.2 为什么需要给箱线上色很多人一开始只画单组箱线图用默认颜色没问题。但一旦涉及多组对比比如不同地区、不同产品线、不同月份光靠黑白线条就很难一眼看出哪个组的波动更大。给箱体填充不同的颜色本质上是把“分组”这个额外维度用视觉通道表达出来观众不用花时间对比每根须线的位置直接看颜色就能锁定关注对象。还有个容易被忽视的使用场景品牌一致性。给外部客户做交付报告时如果整份报告的主色是某一种品牌蓝那么图表却还是 matplotlib 默认的白底黑边视觉上会非常割裂。自定义填充颜色后箱体颜色能和报告主视觉保持一致整体专业感立刻提升。另外颜色还能用于突出“异常组”。我在一次质量分析中把合格率偏低的产线标成红色其余产线用灰色汇报时所有人的注意力第一时间就集中到了红色箱体上。当然使用颜色也要克制如果每组的颜色都特别鲜艳图就会变得刺眼信息层次反而被削弱。后面我会专门讲配色经验这里先明确一个结论上色不是随便挑几个好看的颜色而是为了让数据对比更清晰、报告更统一、重点更突出。2. 环境准备与数据样例开始画图之前先把运行环境准备好。这里我用的是最常见的 Python 数据科学组合matplotlib 负责绘图numpy 负责生成模拟数据seaborn 作为可选的美化工具。如果你用的是 Anaconda 或者 VS Code 配好了 Python 环境下面这几行命令基本不会遇到问题。2.1 环境准备推荐使用 Python 3.8 以上版本matplotlib 用 3.5 以上版本因为低版本在boxplot的参数细节上有些小差异比如某些返回对象结构不一样。安装很简单pip install matplotlib numpy seaborn如果网络环境比较慢也可以指定国内镜像源安装。装好之后在代码开头统一导入import matplotlib.pyplot as plt import numpy as np import seaborn as snsseaborn 这个库其实不是必须的它是在 matplotlib 之上做了一层封装让我能用更少的代码画出样式更精致的图。但你如果只为了实现“自定义填充颜色”只用 matplotlib 也可以后面我会两种方式都演示。2.2 构造演示数据直接拿真实数据不方便复现所以我用 numpy 生成三组模拟数据模拟三个门店的日销售额情况。为了每次运行结果一致我设置了随机种子。rng np.random.default_rng(42) store_a rng.normal(loc90, scale8, size120) store_b rng.normal(loc85, scale15, size120) store_c rng.normal(loc95, scale5, size120) # 手动加入几个离群点模拟真实业务中的突发情况 store_b np.append(store_b, [150, 160]) store_c np.append(store_c, [60]) data [store_a, store_b, store_c] labels [门店A, 门店B, 门店C]这三组数据的设计思路是门店A 平均水平中等、波动适中门店B 平均水平略低但波动很大还带两个高值异常点门店C 平均水平最高、最稳定但我也加了一个偏低异常值。这样画出来之后每组箱体之间能明显看出差异颜色对比的效果也更直观。3. 用 matplotlib 实现自定义填充颜色matplotlib 的boxplot函数是绘制箱线图的核心入口。这一节我会从最朴素的画法开始一步步把颜色加进去同时解释每个参数背后的原理。3.1 最基础的箱线图先画一个完全默认的箱线图看看 matplotlib 默认给了我们什么。plt.figure(figsize(8, 6)) bp plt.boxplot(data, tick_labelslabels, patch_artistFalse) plt.title(默认样式的箱线图) plt.ylabel(销售额) plt.grid(axisy, linestyle--, alpha0.6) plt.show()运行这段代码你会看到三个白底黑边的箱体。很多人第一次看到patch_artistFalse会很疑惑为什么默认是 False这就涉及 matplotlib 的内部绘图机制了。在默认情况下boxplot里的箱体是用线条对象Line2D画出来的它只有边框颜色没有真正意义上的填充区域所以无论你怎么设置facecolor都不会生效。只有把patch_artist改成True箱体才会被创建成可填充的Patch对象这时候你才有资格去设置填充颜色。我最初就是没搞懂这一点直接传了boxprops{facecolor: red}结果运行完什么变化都没有还以为是自己参数写错了。后来翻源码才发现patch_artist这个开关是前提条件。你记住一句话就行不给patch_artistTrue一切facecolor都是白给。3.2 用 patch_artist 打开填充开关把patch_artist打开之后箱体从线条对象变成了可填充图形对象boxprops里的facecolor就能真正控制箱体内部的颜色了。最简单的写法如下plt.figure(figsize(8, 6)) bp plt.boxplot( data, tick_labelslabels, patch_artistTrue, # 关键开关 boxpropsdict( facecolor#4C72B0, # 箱体填充颜色 edgecolorblack, # 边框颜色 linewidth1.5 ) ) plt.grid(axisy, linestyle--, alpha0.6) plt.show()这次三个箱体都变成了蓝色边框是黑色。这里颜色我用了十六进制字符串#4C72B0它是 seaborn 常用配色中的一种蓝色偏沉稳适合商业场景。你也可以用内置颜色名称比如red、steelblue、skyblue或者 RGB 三元组(0.3, 0.45, 0.69)。有一点要提醒如果你在同一个boxprops里既设置了facecolor又设置了colorcolor会覆盖边框、须线等多个元素的颜色有时候会出现边框也跟着变蓝的情况。所以更稳妥的做法是分开控制facecolor管填充edgecolor管边框。3.3 boxprops 参数详解想要画出好看且信息完整的箱线图只调facecolor是不够的。boxprops作为一个字典可以接收大量线条和填充相关属性我按实际使用频率整理了一张表格参数作用常用取值示例facecolor箱体内部填充色#4C72B0,(0.2, 0.4, 0.6)edgecolor箱体边框颜色black,#333333linewidth边框线粗细1.2,2linestyle边框线型-,--alpha整体透明度0 到 10.6,0.85hatch填充纹理打印时防颜色丢失/,o,xxzorder图层层级控制谁盖住谁2,3hatch这个参数很多人不知道但我强烈建议你做打印材料时考虑它。因为有些打印机默认是黑白模式或者对方会把报告打印出来看这时候仅靠颜色区分分组就没用了。加上hatch/后箱体内部会有斜线纹理黑白打印也能清晰看出是不同分组。透明度alpha也是一个需要谨慎设置的参数。理论上透明色能减轻视觉压迫感但如果alpha太低比如 0.2箱体颜色会变得很浅叠加在格子背景上可能看不太清。我自己的习惯是商业正式图表降到 0.7 左右既保留颜色质感又不至于太刺眼。4. 按分组给不同箱体上色上面演示的是所有箱体统一用一种颜色。但实际分析中更多情况是每个分组需要各自的颜色这样可以在同一张图里清晰地对照多个门店、多个时段。这一节我会介绍两种方式手动指定颜色列表以及用 colormap 自动映射。4.1 手动指定多组颜色当分组数量不多时3 到 6 组手动指定颜色是最直接可控的。核心思路是先把boxplot返回的结果保存下来然后遍历每个箱体逐个设置set_facecolor。boxplot的返回值是一个字典里面的boxes键对应所有箱体对象的列表。colors [#4C72B0, #DD8452, #55A868] plt.figure(figsize(8, 6)) bp plt.boxplot( data, tick_labelslabels, patch_artistTrue, medianpropsdict(colorblack, linewidth2) ) for patch, color in zip(bp[boxes], colors): patch.set_facecolor(color) patch.set_edgecolor(#333333) patch.set_linewidth(1.2) plt.title(每个分组使用不同填充颜色) plt.ylabel(销售额) plt.grid(axisy, linestyle--, alpha0.6) plt.show()这里我用zip把箱体对象和颜色列表一一对应顺序不会错。为什么不用索引for i in range(len(bp[boxes]))因为一旦你后面增删了分组数据索引写法很容易越界或者错位而zip会安全地按最短列表停止至少不会报错这个习惯我在很多循环场景里都保持下来了。颜色列表的选择上要注意组与组之间的亮度差。比如三组分别用深蓝、橙、绿对比度足够但如果你选了浅蓝、浅灰、浅粉它们之间的界限会比较模糊图例一多就分不清了。4.2 使用 colormap 自动映射如果分组数量很多比如 8 个、10 个甚至更多手动写颜色列表就很痛苦也容易让颜色之间缺乏递进感。这时候可以用 matplotlib 自带的 colormap 来生成一组颜色。colormap 是一系列连续变化的颜色常见的有viridis、plasma、coolwarm等。import matplotlib.colors as mcolors n_groups len(data) cmap plt.get_cmap(viridis) norm mcolors.Normalize(vmin0, vmaxn_groups - 1) colors [cmap(norm(i)) for i in range(n_groups)] plt.figure(figsize(8, 6)) bp plt.boxplot( data, tick_labelslabels, patch_artistTrue, medianpropsdict(colorwhite, linewidth2) ) for patch, color in zip(bp[boxes], colors): patch.set_facecolor(color) patch.set_edgecolor(#333333) plt.title(使用 viridis 自动映射颜色) plt.ylabel(销售额) plt.grid(axisy, linestyle--, alpha0.6) plt.show()这段代码的思路是先用Normalize把组序号 0 到 2 映射到 0 到 1 的范围再用cmap(norm(i))取出对应颜色。这样就算你有 20 组数据也不需要手写 20 个颜色值。用 colormap 时有个细节viridis这类感知均匀的色图颜色从深紫到明黄过渡虽然好看但它偏向表达“从小到大”的数值等级。如果你的分组本身没有大小顺序只是并列关系用连续的 colormap 反而可能让读者误以为组 A 比组 C 数值更大。遇到这种并列分组我建议还是回到 4.1 的离散颜色方案或者选择色相跨度大一些的定性色图比如tab10。4.3 同时自定义中位数线、异常点颜色箱线图的信息不只是箱体中位数线、异常点、须线都在传递信息。给箱体上色以后千万注意线条颜色和箱体填充色之间的关系。如果中位数线和箱体颜色同为深色中位数可能就看不清了整个箱体的高度信息会丢失。我常用的做法是把中位数线设置成与箱体对比明显的颜色。颜色较浅的箱体配黑色中位数线颜色较深的箱体配白色中位数线。同时异常点一般用醒目的颜色标出因为异常值往往是业务上需要重点关注的数据。下面这段代码是一次性调整所有元素的示例plt.figure(figsize(9, 6)) bp plt.boxplot( data, tick_labelslabels, patch_artistTrue, boxpropsdict(facecolor#4C72B0, edgecolor#2A3F5F, linewidth1.2), medianpropsdict(colorwhite, linewidth2.5), whiskerpropsdict(color#2A3F5F, linewidth1.5, linestyle-), cappropsdict(color#2A3F5F, linewidth1.5), flierpropsdict(markero, markerfacecolor#C44E52, markeredgecolor#C44E52, markersize6) ) plt.grid(axisy, linestyle--, alpha0.6) plt.show()各参数的作用如下medianprops中位数线的样式我建议加大线宽让中位数位置更突出。whiskerprops须线的样式通常和箱体边框保持一致颜色。capprops须线末端的横线用来标记非异常范围内的最小值与最大值。flierprops异常点的样式包括点形状、填充色、边框色、尺寸。这一套设置下来图表的层次就非常清楚了白色中位数线在蓝色箱体上很明显红色异常点不会被误认为正常数据。颜色之间的对比度不仅仅是审美问题更是信息传递问题。5. 用 seaborn 一行实现带填充色的箱线图如果你不想花太多时间调 matplotlib 的参数seaborn 是一个很好的替代方案。它上层封装了boxplot对输入数据的格式更友好尤其是当你手上已经是一个 DataFrame 时一行代码就能画出带填充颜色的箱线图。5.1 palette 参数介绍假设数据已经整理成了长格式的 DataFrame包含一个分组列和一个数值列。下面构造一份import pandas as pd df pd.DataFrame({ 门店: np.repeat(labels, 120), 销售额: np.concatenate([store_a[:120], store_b[:120], store_c[:120]]) }) sns.boxplot(datadf, x门店, y销售额, paletteSet2) plt.title(seaborn 默认 palette 配色箱线图) plt.show()palette参数可以直接传字符串比如Set2、husl、pastel也可以传一个颜色列表顺序对应分组顺序最灵活的是传一个字典键是分组名称值是颜色。color_map { 门店A: #4C72B0, 门店B: #DD8452, 门店C: #55A868 } sns.boxplot(datadf, x门店, y销售额, palettecolor_map) plt.title(seaborn 使用颜色字典精确控制每个分组) plt.show()用字典最大的好处是即使 DataFrame 里分组的顺序发生变化颜色和分组之间的绑定关系也不会错乱。这在数据源字段顺序不稳定时非常有用。seaborn 默认的箱体边框是白色这在深色背景图或者白色背景上都很干净但如果你觉得白色边框太轻可以在boxplot中加上linewidth2让边框更明显。5.2 与 matplotlib 混排时的回填seaborn 虽然方便但它返回的是一个 matplotlib 的Axes对象所以你在它之后依然可以用 plt 系列函数去微调标题、坐标轴、网格线。也可以利用返回的Axes拿到花了几分钟才调好的图形细节。有一个常见需求是先用 seaborn 画箱线图再往同一张图上叠加其他统计信息比如把每个组的均值点画上去。代码可以这样写ax sns.boxplot(datadf, x门店, y销售额, paletteSet2) means df.groupby(门店)[销售额].mean() for idx, (store, mean_val) in enumerate(means.items()): ax.scatter(idx, mean_val, markerD, s60, colorblack, zorder3) plt.title(箱线图上叠加均值点) plt.show()这里的核心思路是seaborn 和 matplotlib 本来就共用底层对象不需要在两种绘图风格之间做切换你可以把ax.scatter看成是对同一个坐标画布追加元素。我个人偏爱这种“先用 seaborn 快速出图再用 matplotlib 精细微调”的组合方式效率高也保留了足够的灵活性。6. 常见问题与排查实录这部分是实打实的经验复盘。我见过身边不少同事在给箱线图上色时踩坑有些问题排查起来很费时间但其实原因就那么几个。我按常见程度从高到低列出来并附上排查思路。6.1 设置了 facecolor 却不变色这是最经典的问题我在前面已经提到过。现象是boxprops{facecolor: red}写进去了图还是白底的。原因几乎都是没有设置patch_artistTrue。排查步骤很简单第一确认boxplot调用里有没有patch_artistTrue第二确认颜色是写在boxprops里而不是直接写在boxplot的最外层第三检查颜色值本身是否合法比如十六进制字符串有没有写错RGB 三元组是否在 0 到 1 范围内。如果你用bp[boxes][0].get_facecolor()拿到的是白色或透明色的 RGBA 值那说明问题还是出在patch_artist上。这个函数是我调试时很常用的“体检工具”。6.2 填充色盖住了中位数线设置成深色填充后中位数线也设置成深色比如黑色结果中位数线和箱体边缘融为一体看不出中位数在哪。严格说不是盖住而是对比度不够。解决方式有两种一是把中位数线改成白色或浅色线宽加大到 2.5这适合深色箱体二是降低箱体填充色的透明度比如facecolor(0.2, 0.4, 0.6, 0.5)让深色背景上的深色线条透出来。前一种方式更干净我推荐优先调整medianprops。还有一个容易被忽略的地方当你用patch.set_facecolor(color)循环上色时medianprops必须在boxplot初始化时就传好因为中位数线的对象已经生成之后你依然可以通过bp[medians]来改但直接在初始化时设置最省事。6.3 颜色列表与分组数量不匹配手动传颜色列表时最常见的问题是颜色数量少于分组数量。用zip(bp[boxes], colors)时不会报错但最后几组箱体会保持默认颜色或上次循环留下的颜色很容易看不出问题。如果颜色数量多于分组数量多出来的颜色静静躺在列表里没人用同样不报错。这两种情况都属于“灾难性失败的很安静”的类型让人防不胜防。我的排查习惯是在对每个箱体上色后马上用print(len(bp[boxes]), len(colors))确认两边数量一致。另外尽量不要在图例里手写颜色而是从修改后的图形对象中提取真实的颜色来生成图例句柄这样图例和实际图形永远对得上。6.4 保存图片后颜色发灰有时候屏幕显示颜色很鲜艳保存成 PNG 后却发灰变暗问题通常出在配色方案上。某些颜色在纯白背景下显示正常但系统会自动进行色彩管理加上字体渲染宽度不同整体观感会变化。这类问题我一般从两个角度解决。第一保存时提高分辨率用dpi200或 300能让颜色边缘更平滑整体画面更细腻。第二如果想保持透明背景用plt.savefig(boxplot.png, transparentTrue, dpi300)但要注意透明背景经过某些看图软件展示时会反白慎用于正式交付场景。保存矢量图用plt.savefig(boxplot.svg)颜色精度极高放到 PPT 或设计软件里也很稳。还有一个容易被忽略的问题是屏幕色域差异。同一份红色在你的显示器上看着发暗在别人笔记本上可能偏亮。所以正式报告里我通常会在颜色旁边加文本标签或图例文字保证即使色彩有轻微偏差分组信息也不会丢。7. 几个实际项目里的配色经验技术细节聊完了最后分享一些我真实项目里积累的配色原则。这些经验不一定出现在官方文档里但对最终成图质量影响很大。先看“品牌色优先”。如果这份图要进公司汇报 PPT我建议直接从公司 VI 规范里取主色和辅助色而不是临时去调色板里选。这样整份报告的颜色系统是统一的领导看完不会觉得图表是“外包风格”。具体操作时我会把品牌色整理成颜色列表专门放在一个配置模块里所有项目统一调用。再说“颜色数量控制”。一张箱线图里颜色数量我尽量控制在 3 到 6 个。超过 7 个时即使颜色有差异人眼也很难快速区分不如改用其他视觉通道比如箱体填充纹理、边框线型。分组特别多时我更倾向于拆成小图或者先用序号排序再用颜色强调其中一组。“颜色要有语义”。如果你在画的是业务监控数据尽量让颜色本身包含含义红色代表异常绿色代表正常蓝色代表中性。这样读者不需要看图例单凭生活常识就能快速判断。比如我在分析门店销售额时把异常波动的门店用红色箱体标出其余用灰色信息传达效率非常高。还要记得“打印与色盲友好”。前面提到的hatch参数我在做纸质汇报材料时一定会加因为黑白打印无法显示色彩区别。对于色盲人群红绿搭配是最容易被混淆的所以我会改用蓝橙搭配或者同时用纹理区分。一次给客户做交付对方明确指出小组内部有红绿色盲同事从那以后我的默认配色方案就从红绿改成了蓝橙。最后一个建议给箱线图加图例时不要直接拿颜色名称当分组名称。我通常会把图例和颜色关系用一个小表格放在图表下方或者直接在箱体上方标注文字。颜色作为视觉辅助非常好用但只有配合清晰的文字说明图表才能真正自解释。我个人在实际操作中的体会是自定义填充颜色的价值不在于炫技而在于让数据图表更快地说明问题。当你面对一张多组对比的箱线图颜色能让观众在 10 秒内锁定哪组波动最大、哪组中位数最高这就是这项技能最大的回报。下次你再画箱线图不妨先把默认的白色箱体换成有语义的颜色再看一眼图表是否立刻变得更容易读了。
返回列表
PREV
查看更多资讯
NEXT
返回资讯列表