
做数据分析图表时真正让箱线图从能用变成好用的往往是填充颜色这个细节。默认的箱线图是空心的线条框放多组数据在一起时读者只能靠位置和标签去分辨谁是谁视线要在图上来回扫好几遍。这篇文章想讲的就是用 Python准确说是 matplotlib把箱线图的填充颜色按需自定义让图自带分层、自带重点信息一眼能看懂。适合正在学 Python 可视化的初学者也适合日常要做数据汇报、需要把图表做得更专业的职场人。全文没有绕圈子的概念直接给可运行的代码和背后逻辑。1. 为什么要折腾填充颜色默认箱线图的两个硬伤1.1 箱线图本身信息密度很高但默认样式拖了后腿箱线图是少数几种一张图塞进五个统计量的图表最小值、下四分位数、中位数、上四分位数、最大值外加异常值点。这意味着只要看一眼图就能大概判断一组数据的中心位置、离散程度、偏态方向和异常值情况。做实验对比、业务指标监控、成绩分析、AB 测试结果汇报它都是首选。但 matplotlib 的默认箱线图样式比较朴素空心线框黑色边框没有填充面。单组数据显示还没什么问题一旦出现三组、五组数据并排问题就来了——所有箱子长得一模一样只能靠横坐标标签去区分。人的视觉系统对形状的敏感度远不如对颜色的敏感度一堆同样的空心框摆在一起读图的人很难快速抓住哪一组是重点哪一组指标异常。这时候自定义填充颜色的价值就体现出来了。给不同数据组填充不同颜色等于给图表加了一层视觉编码读者不用看标签光靠颜色就能锁定目标。1.2 一个反直觉的事实箱线图的填充比柱状图麻烦如果你用过plt.bar()会发现指定颜色特别直接一个colorskyblue参数就搞定了。但箱线图不是这样。我第一次试着给箱线图加颜色的时候很自然地写了colorlightblue结果只改变了边框线的颜色箱子中间还是空的。原因在于matplotlib 的箱线图默认是由线条型 Artist 构成的矩形框不是带填充面的矩形。它更像统计图里的须图画的是轮廓不画的面。要让箱线图拥有可填充的面必须显式打开一个开关patch_artistTrue。patch在 matplotlib 里特指一类带填充区域的图形对象比如Rectangle、Polygon、Circle。只有这个参数为True时boxplot 内部的箱子才会从四根线拼成的框变成一个可以被填充颜色的矩形 patch你后面设置facecolor才会生效。提示patch_artistFalse是历史遗留的默认值和 matplotlib 最初把 boxplot 设计成纯线条风格有关。新版本里这个默认值一直没有改所以每画一次箱线图都要主动打开开关。2. 自定义填充的底层机制boxplot 返回的字典结构2.1 返回值bp到底是什么调用bp ax.boxplot(data, patch_artistTrue)之后返回的是一个 Python 字典里面按类别存了箱线图各个组成部分的 Artist 对象列表键名对应图形元素类型boxes箱子主体矩形 patchPatch 对象列表medians中位数线Line2D 对象列表whiskers须线从箱子到触须端点Line2D 对象列表caps触须末端的横线Line2D 对象列表fliers异常值点Line2D 对象列表要自定义填充颜色操作对象就是bp[boxes]。这是一个列表每个元素对应图上的一个箱子。比如三组数据bp[boxes]长度就是 3通过索引或 zip 循环可以逐个修改每个箱子的填充色。这个设计其实很好用。matplotlib 把每个图形元素都当作对象暴露给你你在绘图完成之后还能继续追改样式不需要重新画图。这比某些封装好的高级绘图库要灵活得多。2.2 指定颜色的几种方式在 matplotlib 里颜色可以用多种形式表示箱线图填充同样支持内置颜色名称lightblue、salmon、lightgreen单字母缩写r、g、b、c、m、y、k、w十六进制色值#4C72B0、#55A868RGB 或 RGBA 元组(0.3, 0.5, 0.7)、(0.3, 0.5, 0.7, 0.6)第四个分量是透明度灰度字符串0.2到0.9日常绘图我会优先用十六进制。内置颜色名称数量有限有些颜色在不同操作系统上渲染出来略有差异而十六进制是精确值发给同事、放进论文、投到期刊谁打开都一样。下面这段代码是最基础的框架一次性给三个箱子分别填充三种颜色import matplotlib.pyplot as plt import numpy as np # 构造三组示例数据 np.random.seed(42) data [ np.random.normal(60, 10, 100), np.random.normal(75, 12, 100), np.random.normal(50, 8, 100) ] fig, ax plt.subplots(figsize(8, 5)) bp ax.boxplot(data, patch_artistTrue, labels[班级A, 班级B, 班级C]) # 按顺序给三个箱子上色 colors [#4C72B0, #55A868, #C44E52] for patch, color in zip(bp[boxes], colors): patch.set_facecolor(color)跑完这段三个箱子会分别变成蓝色、绿色、红色。别急这只是开始。要让图真正好看边框、须线、中位数线、异常值点都得一起处理否则会出现彩色箱子配黑线条的割裂感。3. 实战一多组数据的完整配色方案3.1 从能上色到上得好看推荐一套整体配色颜色不是越花越好。同一张图里填充色承担的是分组标记功能应该彼此区分明显同时不刺眼。下面是几个被验证过比较舒服的搭配场景推荐色板特点论文配图#4C72B0#DD8452#55A868色相差距大灰度打印也能区分业务汇报#5B9BD5#ED7D31#A5A5A5商务感强不抢文字信息异常监控正常组#70AD47异常组#C00000红色高亮警示效果明显多组对比5 组以上tab20色板前 10 个颜色颜色多但饱和度偏高注意搭配对于三组数据的场景我个人常用#4C72B0蓝、#55A868绿、#C44E52红这套组合。蓝绿红三色在绝大多数显示设备上都能拉开差距色盲用户也能通过明度差异大致分辨。3.2 边框、须线、中位数线一起调才算完整填充色只是第一步。如果你只改了facecolor箱子的黑色边框、黑色的中位数线、黑色的须线会显得很突兀像一块彩色积木插在黑铁丝网里。我习惯把所有非填充元素统一成一个深灰或深蓝色让视觉焦点集中在填充色上。import matplotlib.pyplot as plt import numpy as np np.random.seed(42) data [ np.random.normal(60, 10, 100), np.random.normal(75, 12, 100), np.random.normal(50, 8, 100) ] fig, ax plt.subplots(figsize(8, 5)) bp ax.boxplot(data, patch_artistTrue, labels[班级A, 班级B, 班级C]) colors [#4C72B0, #55A868, #C44E52] edge_color #2F3542 # 深灰蓝和彩色填充都能搭配 line_width 1.5 for patch, color in zip(bp[boxes], colors): patch.set_facecolor(color) patch.set_edgecolor(edge_color) patch.set_linewidth(line_width) # 统一中位数线样式加粗突出中心位置 for median in bp[medians]: median.set_color(#2F3542) median.set_linewidth(2.0) # 须线、触须端点和异常值点统一颜色 for whisker in bp[whiskers]: whisker.set_color(edge_color) whisker.set_linewidth(line_width) for cap in bp[caps]: cap.set_color(edge_color) cap.set_linewidth(line_width) for flier in bp[fliers]: flier.set(markero, markersize5, markerfacecolornone, markeredgecoloredge_color) ax.set_ylabel(分数) plt.tight_layout() plt.show()这段代码里有个细节值得说明for median in bp[medians]遍历的是所有中位数线改成linewidth2.0后中位线比边框更粗视觉上能第一个抓住每组数据的中心在哪里读图效率会高很多。异常值点用空心圆圈markerfacecolornone避免点太多糊成一片。3.3 为什么要在循环里改不直接用 boxprops有人可能会问matplotlib 的 boxplot 里不是有boxprops参数吗直接传boxprops{facecolor: xxx}不就行了可以但要注意一个限制boxprops里的值是统一应用到所有箱子上的。如果你只需要所有箱子同一个颜色一句话就能搞定ax.boxplot(data, patch_artistTrue, boxprops{facecolor: #4C72B0, edgecolor: #2F3542})但如果你要不同箱子不同颜色boxprops是无能为力的必须走先画图再遍历修改的路线。这也是我在代码里采用zip(bp[boxes], colors)的原因。实际项目里绝大多数自定义填充需求恰恰是每组一个颜色所以这个循环模式是绕不开的核心知识点。还有个小技巧如果你嫌先画图再改样式太繁琐可以在boxprops里只统一设置边框把facecolor留到循环里单独处理。两种方式可以混用。4. 实战二动态着色——让异常组一眼被看见4.1 需求场景对比多组数据时突出最需要关注的一组颜色分组是基础玩法。更进一步的是动态着色不是提前硬编码颜色而是根据数据特征计算得出哪个组需要高亮然后把它涂成警示色其余组保持低调。这种图在业务监控和质量分析里非常实用。举个例子现在有 6 个车间的产品合格率数据你想快速找出中位数明显低于整体水平的车间。实现思路是先算出每个车间的中位数再算出全部数据的总体中位数某个车间的中位数低于总体中位数就用红色标记它其余车间用灰色系。4.2 实现代码颜色由数据算出来import matplotlib.pyplot as plt import numpy as np np.random.seed(7) # 生成6个车间的合格率数据模拟 groups [f车间{i} for i in range(1, 7)] data [] for i in range(6): base 88 - i * 2 # 车间编号越大合格率越低 data.append(np.random.normal(base, 4, 80)) # 计算每个组的中位数和全局中位数 group_medians [np.median(d) for d in data] global_median np.median(group_medians) fig, ax plt.subplots(figsize(10, 5)) bp ax.boxplot(data, patch_artistTrue, labelsgroups) for patch, median_val in zip(bp[boxes], group_medians): if median_val global_median: patch.set_facecolor(#C44E52) # 异常红色 else: patch.set_facecolor(#B0BEC5) # 正常灰蓝 # 统一边框线条让填充色成为唯一变量 for element in [whiskers, caps, medians]: for line in bp[element]: line.set_color(#37474F) for flier in bp[fliers]: flier.set(markero, markersize4, markerfacecolor#C44E52, markeredgecolornone, alpha0.5) ax.axhline(global_median, ls--, color#888888, lw1, labelf全局中位数: {global_median:.1f}%) ax.set_ylabel(合格率 (%)) ax.legend() plt.tight_layout() plt.show()运行结果里车间 4、5、6 会被涂成红色车间 1、2、3 是灰蓝色。再加一条全局中位数的虚线哪个车间拖后腿一眼便知。这种动态着色的核心逻辑是颜色不再是手工选的美术决定而是数据运算的产物。你会发现自己写的代码几乎不需要改换个数据集、换个判断阈值比如改成超过 75 分位才算异常图就能自动适配。4.3 用透明度做层次重点组饱和非重点组退后还有一种常见手法是用透明度控制视觉权重。把非重点组的填充色加上透明度比如patch.set_alpha(0.3)重点组保持alpha1.0。透明度低的箱子会退到背景里重点组自然浮出来。这在样本量很大的对比图里特别好用不需要大红大绿也能达到清晰的层级感。要注意的是set_alpha会影响整个 patch 的透明度包括边框。如果你只想让填充色变淡而边框保持清晰可以在设置透明度之后再单独调用一次set_edgecolor和set_linewidth或者直接用带 alpha 通道的 RGBA 颜色来填充rgba_color (0.722, 0.769, 0.812, 0.4) # 灰蓝40%不透明 patch.set_facecolor(rgba_color)这种方式只淡化了填充面不影响边框线效果更干净。5. 高频踩坑记录配色不生效、图例错乱、刻度漂移5.1 坑一忘了 patch_artistTruefacecolor 全部无效这是新手上路时最容易踩的坑。现象是代码里写了patch.set_facecolor(#4C72B0)运行不报错但图上的箱子就是没有颜色。为什么因为patch_artistFalse时bp[boxes]里的元素不是 Patch 对象而是 Line2D 线条对象。Line2D 没有set_facecolor方法所以设置被静默忽略你什么也看不到。排查方法很简单打印一下bp[boxes][0]的类型。如果是matplotlib.lines.Line2D证明patch_artist开关没打开如果是matplotlib.patches.PathPatch或Rectangle说明开关已打开就可以正常设置填充色了。5.2 坑二多个 boxplot 叠加调用时图例和刻度会互相打架做分组对比图时比如同一张图里画两个季度、同一组位置偏移很多人会写两次ax.boxplot()调用。问题来了第一次调用会把xticks设置为[1, 2, 3]之类的数值第二次调用同样会重新设置xticks直接覆盖第一次的刻度最终结果往往是横坐标标签错乱甚至只显示第二次的标签解决办法是在第一次及后续的boxplot调用里设置manage_ticksFalse然后自己在最后统一设置xticks和xticklabels。代码如下positions_1 [1, 2, 3] positions_2 [1.3, 2.3, 3.3] bp1 ax.boxplot(data_group1, positionspositions_1, widths0.28, patch_artistTrue, manage_ticksFalse) bp2 ax.boxplot(data_group2, positionspositions_2, widths0.28, patch_artistTrue, manage_ticksFalse) ax.set_xticks([1.15, 2.15, 3.15]) ax.set_xticklabels([班级A, 班级B, 班级C])图例也会遇到类似问题。matplotlib 3.4 之前的版本boxplot本身不支持label参数直接ax.legend()会得到一个空的或错误的图例。老项目里更稳妥的做法是手动创建图例代理对象import matplotlib.patches as mpatches legend_handles [ mpatches.Patch(color#4C72B0, label上学期), mpatches.Patch(color#55A868, label下学期) ] ax.legend(handleslegend_handles, locupper right)mpatches.Patch在这里只是个图例样品它不需要真的画在图里只需要展示颜色和标签的对应关系。这个技巧在自定义填充色之后几乎是必用的推荐背下来。5.3 坑三填充颜色和线条颜色对比度不足填了浅色但忘了改中位数线颜色结果浅蓝色箱子里一条浅灰色中位数线和底色几乎融为一体。这在浅色填充如#E8F0FE时特别明显。解决办法是养成配件颜色统一的习惯填充色负责大色块区分中位数线、边框、须线一律用深色#2F3542或#111111并且中位数线加粗到 1.8~2.0。这样不管填充色多浅核心统计信息都始终清晰可见。5.4 坑四Jupyter 里重复运行单元格图例和样式重叠在 Notebook 里反复执行同一个绘图单元格如果每次都调用ax plt.subplot()不会有问题但如果复用了同一个ax第二次运行会覆盖式叠加箱线图样式会变得混乱。建议每个单元格前都重新创建fig, ax plt.subplots()或者运行前先ax.clear()。这不是箱线图特有的坑但在需要反复调整填充颜色的时候特别容易遇到——因为你改颜色、重跑图结果发现箱子上叠了阴影。6. 进阶技巧透明度叠加、色板统一与 Seaborn 协同6.1 分组箱线图与透明度叠加当数据类别较多时例如三组分类每组 5 个类别颜色会不够用。一种处理方案是维度一用填充颜色区分维度二用透明度区分。比如电商分析场景中横轴是 5 个商品类别每个类别下有两个渠道线上下单、线下门店你想用蓝色系表示线上、深浅区分不同类别或者反过来用颜色的深浅表示类别、用色相表示渠道。给箱子设置透明度的代码很简单# 深蓝 patch.set_facecolor((0.298, 0.447, 0.690)) # 浅蓝同一色相较低透明度 patch.set_facecolor((0.298, 0.447, 0.690, 0.4))用带 alpha 通道的 RGBA 而不是set_alpha可以在保持边框色不变的前提下单独调整填充浓度层次表现更细腻。6.2 自定义色板轮换免去逐个手写颜色如果数据组数很多不想每画一组都手写一个颜色列表可以用 matplotlib 自带的色板循环import matplotlib.cm as cm import matplotlib.colors as mcolors # 从 colormap 中采样 N 个颜色 def generate_colors(n, cmap_nameviridis): cmap cm.get_cmap(cmap_name) return [cmap(i / max(n - 1, 1)) for i in range(n)] colors generate_colors(6)viridis色板从深紫到明黄渐变色相跨度大相邻两组也能拉开差距。还有Set2matplotlib 自带离散色板、tab10、Pastel1等。用这种方式生成的 colors 列表直接替换前面代码里的colors就行箱线图的填充逻辑完全不变。6.3 和 Seaborn 配合的细节Seaborn 的sns.boxplot()有palette参数一句话就能给箱子自动上色import seaborn as sns sns.boxplot(datadf, xgroup, yvalue, paletteSet2)它的原理仍是调用 matplotlib 的 boxplot然后按调色板依次修改facecolor。如果你只是要快出一张带颜色的箱线图Seaborn 无疑是最省事的但如果你需要精确指定某组颜色、动态着色、或者精细控制中位数线的粗细最终还得落回 matplotlib 的这套patch_artistbp[boxes]方案。我的习惯是快速探索数据用 Seaborn做正式汇报或论文图用 matplotlib 手写配色。两者切换并不冲突理解 matplotlib 的底层逻辑后Seaborn 的限制反而看得更清楚。6.4 小技巧保存图片时注意背景色最后分享一个容易被忽略的点带自定义填充色的图保存成透明背景 PNG 时如果填充色本身带透明度透明区域会和背景混在一起在某些软件里打开后会显得颜色偏淡。做正式交付时建议用plt.savefig(boxplot.png, dpi300, bbox_inchestight)保存为白底图不要用transparentTrue除非你确定用途是叠放到深色 PPT 模板里。我在实际做数据汇报时的习惯是先用默认参数跑一遍确认数据没异常再套颜色体系、调线条粗细、加图例最后统一保存成一份白底高分辨率 PNG。自定义填充颜色这件事做一次就形成了自己的模板后面对接任何数据集都是复制粘贴再微调的事。