用Python绘制自定义填充颜色的箱线图:matplotlib与seaborn实战 用Python画箱线图这件事很多教程两行代码就带过去了。但等你真把数据丢进去看到默认输出的那几个空心线框框多半会愣一下这跟我脑子里那张能直接放进报告里的图差得有点远。尤其是做分组对比、给客户出图表、或者在同一张大图上展示多组数据的时候带自定义填充颜色的箱线图阅读效率完全不是默认样式能比的。这篇文章就围绕“Python绘制带自定义填充颜色的箱线图”这个需求展开讲清楚如何用matplotlib和seaborn把每个箱子的填充颜色做成自己想要的样子顺手把边框、须线、异常点这些配套细节也一起收拾干净。适合刚接触Python数据可视化的人也适合已经会画图但想提升图表表达力的同学内容偏实操看完可以直接抄作业。1. 箱线图到底在画什么先理解再动手1.1 一张箱线图里藏了哪些统计量很多人在调颜色之前其实没完全搞明白箱线图上的每个元素对应什么数据含义。不理解这一点后面设置颜色时就会很被动——你想改“箱子”的颜色但代码里还能碰到“须线”“中位数线”“异常点”这些对象如果不清楚它们各自是谁样式就会改得七零八落。箱线图的经典构成是这样箱子的下边界是下四分位数Q1也就是25%分位数上边界是上四分位数Q3也就是75%分位数箱子中间的横线是中位数Q250%分位数。箱子本身的高度代表四分位距IQR这个数值是Q3减去Q1反映中间一半数据的离散程度。从箱子两端延伸出去的“须线”默认延伸到1.5倍IQR范围内的最远数据点超过这个范围的点会被单独标记成“异常点”fliers通常画成散点样式。所以一张箱线图其实是在用五个统计量概括一组数据的分布形态最小值边界、Q1、中位数、Q3、最大值边界外加异常点信息。明白了这个构成再看今天的核心主题就清楚了我们说的“自定义填充颜色”本质上就是给“箱子”这个矩形区域上色也就是Q1到Q3之间的那个四边形区域。须线、中位线、异常点这些是配套元素颜色上可以跟箱体协调也可以刻意做反差这个后面会展开。1.2 为什么填充颜色会成为刚需有人可能会问默认的黑白空心箱体也能看出中位数和离散程度为什么非得上颜色我自己的体会是颜色在数据可视化里承担的不只是“好看”这个功能更重要的是“编码信息”。比如同一张图上放了四个实验组的箱线图你用红色标出异常组、蓝色标出正常组读者半秒钟就能锁定问题组在哪里如果全是黑白框大家还得逐个去比对中位数位置和箱子高度。再比如做业务汇报图表配色跟公司VI统一或者跟PPT主题一致专业感是完全不同的。学术论文里也常见这种需求——审稿人不会因为你图好看就录用但一张配色混乱、分不清组别的图一定会被挑毛病。所以给箱体填充颜色本质上是让数据对比变得更直观为读者降低解读成本而不是单纯装饰。另外还有一个非常现实的场景当多组数据的箱体在图上靠得很近时如果不填色箱体边界线容易互相纠缠视觉上根本分不清哪个箱子属于哪一组。填充颜色相当于给每个箱子一个“底牌”一眼就能完成分组归属的判断。2. 绘图工具选型与准备工作2.1 三个主流库的定位与适用场景Python生态里画箱线图的方案不少但主流就三个matplotlib、seaborn、plotly。三者不是替代关系更多是“不同场景选不同工具”的关系。工具优点缺点适合场景matplotlib控制粒度最细所有元素都能手动改API偏底层代码量相对大需要精细定制、论文级图表seaborn封装简洁一行出图内置配色多定制灵活度比matplotlib稍弱快速探索数据、标准统计图表plotly交互式图表可缩放悬停静态导出样式稍重依赖较多Web展示、需要交互的场景我的建议很直接如果你要的是“精确控制每一个像素级细节”请用matplotlib它是seaborn的底层学会了它seaborn出的图你也能二次修改如果你要的是“快速出一张能看的统计图”用seaborn更省事。plotly适合做线上仪表盘或需要用户交互的页面但做报告里的静态图matplotlib和seaborn完全够用。本文两个路线都会覆盖到你先学matplotlib这条主线seaborn作为效率方案补充。2.2 环境准备与一行命令装好依赖在动手之前先把环境确认好。Python版本建议3.8以上太老的版本在部分API行为上会有差异。安装依赖用pip即可我通常这样装pip install matplotlib seaborn numpy如果你用的是conda环境也可以走conda渠道conda install matplotlib seaborn numpy两个渠道选一个就行不要混着装否则偶尔会出现同一个库被装了两份、互相覆盖的混乱情况。装好之后在Python里执行一下import matplotlib.pyplot as plt不报错就说明环境没问题。顺便说一句我见过不少人在Jupyter Notebook里画图但忘记加一行魔法指令导致图不显示这里先帮你排掉这个雷%matplotlib inline在Jupyter里需要加上这行matplotlib的图才能直接内嵌显示。如果你用PyCharm或者VS Code直接跑脚本则不需要。2.3 中文字体与坐标轴的预处理国内用户画图第一个容易翻车的地方是中文字体。matplotlib的默认字体里不含中文字形如果你直接在图里写“实验组A”这种标签画出来的图会显示成一个个小方块。解决方法是在画图前统一设置字体参数import matplotlib.pyplot as plt plt.rcParams[font.sans-serif] [SimHei, Microsoft YaHei, PingFang SC] plt.rcParams[axes.unicode_minus] False第一行指定无衬线字体列表matplotlib会按顺序找第一个可用的中文字体第二行是修复负号显示问题——默认字体下坐标轴的负号会变成方块。这两个设置属于“开局前必埋的伏笔”先写上能省掉后面90%的乱码问题。如果你用的系统里没有这些字体可以换成你本机实际安装的字体名比如Noto Sans CJK SC。3. matplotlib路线从默认样式到自定义填充色3.1 最简版本先让箱体变成可填充的matplotlib画箱线图的核心API是ax.boxplot()。大多数人第一次画出来的是空心箱子原因在于默认情况下boxplot返回的“箱子”是线条对象Line2D线条只能设置颜色和粗细没法“填充”内部区域。要让箱子变成可以填充的图形块必须打开一个关键开关patch_artistTrue。import matplotlib.pyplot as plt import numpy as np np.random.seed(42) data [np.random.normal(60, 10, 80) for _ in range(4)] fig, ax plt.subplots(figsize(10, 6)) bp ax.boxplot(data, patch_artistTrue, labels[A组, B组, C组, D组]) plt.show()patch_artist翻译过来就是“用图块艺术化”设置为True之后bp[boxes]这个列表里装的就不再是Line2D而是matplotlib.patches.Patch对象只有Patch对象才有set_facecolor()、set_edgecolor()这些跟填充相关的接口。这一步是整个自定义填色的地基很多人填色失败多数都是因为没开这个开关。3.2 给每个箱子分配不同的填充颜色现在箱子已经“可填充”了给它上颜色就顺理成章。boxplot()返回的bp是一个字典结构其中bp[boxes]是每个箱子的艺术对象列表顺序跟你传入数据的顺序一致。我们用一个颜色列表跟它做zip操作逐个设置填充色colors [#4C72B0, #DD8452, #55A868, #C44E52] for patch, color in zip(bp[boxes], colors): patch.set_facecolor(color) patch.set_edgecolor(#2B2B2B) patch.set_linewidth(1.2)这里的颜色用了十六进制色值Hex这是最常用的颜色表达方式。#4C72B0这种格式前两位是红色通道中间两位是绿色通道后两位是蓝色通道数值范围从00到FF。你也可以直接用matplotlib内置的颜色名称比如steelblue、tomato或者用RGB元组(0.3, 0.45, 0.69)。我个人的习惯是统一用Hex因为从设计软件或者公司VI规范里拿到的颜色值基本都是Hex格式直接复制就能用不用换算。还有一个实用参数是透明度。如果你希望箱体颜色半透明露出底下的网格线可以这样设置patch.set_facecolor(#4C72B0) patch.set_alpha(0.6)透明度的取值范围是0到10完全透明1完全不透明。在需要叠加多组箱体的时候适当降低透明度能避免后画的那个箱子完全遮住前面的。3.3 配套细节边框、异常点、中位线的整体协调填充色只是第一步一张图要好看必须让其他元素也协调起来。boxplot()的返回字典里除了boxes还有whiskers须线、caps须线末端的横线、medians中位数线、fliers异常点它们各自都是列表形式可以统一或逐个设置样式。下面的代码是我做分组对比图时比较常用的一套“组合拳”for whisker in bp[whiskers]: whisker.set_color(#8B8B8B) whisker.set_linestyle(--) whisker.set_linewidth(1.0) for cap in bp[caps]: cap.set_color(#2B2B2B) cap.set_linewidth(1.5) for median in bp[medians]: median.set_color(white) median.set_linewidth(2.0) for flier in bp[fliers]: flier.set_marker(o) flier.set_markerfacecolor(none) flier.set_markeredgecolor(#2B2B2B) flier.set_markersize(5)这里的设计逻辑是须线用灰色虚线让它退到次要视觉层级中位数线用白色加粗在彩色箱体上非常显眼读者第一眼就能抓到分布中心异常点用空心圆点既不会被误认为正常数据也不会喧宾夺主。白色中位线在深色箱体上特别清晰但如果你的箱体本身是浅黄色或者白色那中位线最好改成深色比如#2B2B2B否则会看不见。这是配色对比度的问题后面第5章还会细说。完整的可运行版本我贴一下你复制就能用import matplotlib.pyplot as plt import numpy as np plt.rcParams[font.sans-serif] [SimHei, Microsoft YaHei, PingFang SC] plt.rcParams[axes.unicode_minus] False np.random.seed(42) data [np.random.normal(60, 10, 80) for _ in range(4)] colors [#4C72B0, #DD8452, #55A868, #C44E52] labels [对照组, 方案A, 方案B, 方案C] fig, ax plt.subplots(figsize(10, 6)) bp ax.boxplot(data, patch_artistTrue, labelslabels) for patch, color in zip(bp[boxes], colors): patch.set_facecolor(color) patch.set_edgecolor(#2B2B2B) patch.set_linewidth(1.2) for whisker in bp[whiskers]: whisker.set_color(#8B8B8B) whisker.set_linestyle(--) whisker.set_linewidth(1.0) for cap in bp[caps]: cap.set_color(#2B2B2B) cap.set_linewidth(1.5) for median in bp[medians]: median.set_color(white) median.set_linewidth(2.0) for flier in bp[fliers]: flier.set_marker(o) flier.set_markerfacecolor(none) flier.set_markeredgecolor(#2B2B2B) flier.set_markersize(5) ax.set_ylabel(指标值) ax.set_title(四组方案箱线图对比) ax.grid(axisy, linestyle--, alpha0.4) plt.tight_layout() plt.show()这版代码画出来的图基本可以直接放进周报里。网格线我刻意只开横向的axisy这样不会跟竖直方向的箱体冲突视觉上更干净。4. seaborn路线更省事的配色方案4.1 palette参数实现一图多色如果你不想手动写循环去设置每个箱子的颜色seaborn是很顺手的替代方案。它把boxplot封装成了高层API一个palette参数就能完成整体配色import seaborn as sns import matplotlib.pyplot as plt tips sns.load_dataset(tips) plt.figure(figsize(9, 6)) sns.boxplot(datatips, xday, ytotal_bill, palette[#4C72B0, #DD8452, #55A868, #C44E52]) plt.title(不同星期的小费金额分布) plt.show()这里palette接收一个颜色列表seaborn会按x轴类别的顺序依次分配颜色。如果你不给具体的颜色值也可以直接传一个内置调色板的名字比如paletteSet2、palettehusl、palettecolorblind。其中colorblind是专门针对色觉障碍人群设计的配色表格、报告场景里都挺稳妥。seaborn的好处是省去了patch_artistTrue这一步因为它的箱体默认就是填充样式内部会自动调用matplotlib的底层机制。你拿到axes对象之后同样可以继续用matplotlib的API做细微调整——seaborn并没有把你锁死。4.2 hue分组与细粒度颜色覆盖再往前走一步如果你的数据里还有一个分类变量需要同时展示比如除了周几之外还要区分性别那就得用到hue参数sns.boxplot(datatips, xday, ytotal_bill, huesex, paletteSet2)这种情况下palette的作用对象就不再是x轴类别而是hue对应的类别。比如huesex有男、女两个水平paletteSet2里有8个颜色seaborn会取前两个分配出去然后在下一个x轴类别里重复同样的循环。如果你对颜色的要求非常具体比如必须确保“男性”固定是蓝色、“女性”固定是橙色那就别用列表改用字典来映射palette_dict {Male: #4C72B0, Female: #DD8452} sns.boxplot(datatips, xday, ytotal_bill, huesex, palettepalette_dict)用字典的好处是颜色跟类别名称绑定不管x轴怎么变、数据顺序怎么变颜色永远稳定对应不会出现“今天红色是男性、明天红色变成女性”这种低级错误。我在跑批数据的时候特别依赖这个特性——因为底层数据每天刷新分类顺序未必完全一致颜色错位会让读图的人产生严重误判。5. 真实场景中的配色经验与避坑指南5.1 配色不只是美观颜色编码背后的信息逻辑这一节想聊点跟审美无关、但比审美更重要的东西。自定义填充颜色本质上是在用颜色做“信息编码”。既然是编码就得遵守一些基本规则否则配色越丰富图反而越难读。第一同一张图里不同分组应该用不同色相的颜色而不是同一个颜色的深浅变化。色相就是人眼能直接分辨的“红色、蓝色、绿色”这种差别深浅则是同一个颜色从浅到浓的变化。箱线图的分组之间是平行关系没有高低层级之分用深浅区分会让读者误以为某组更“弱”或更“低级”。除非你的分组本身有等级含义比如“低中高风险”那用同一个色相的渐变更合理。第二要考虑色觉障碍人群。红绿配色是最常见的雷区因为红绿色盲人群占比不低。如果图要公开发布优先选colorblind这类安全调色板或者至少避开“红配绿”这种组合改成“红配蓝”“橙配蓝”这类对比。第三颜色的语义要稳定。比如你约定“红色异常组”那么整张图里红色只能出现一次而且只能代表异常组。如果背景里还有个红色的标题或者注释读者就可能误读。我在实际项目里吃过这个亏图例是红色代表警告组结果PPT模板里的标题飘带也是红色客户当场把警告组的数量看错成了标题装饰。后来我学乖了所有报告配图统一走“颜色清单”制度一图一义。5.2 颜色作用对象不同patch_artist这个开关的坑回到技术细节。我在帮同事排查代码的时候发现“填色不生效”这个问题的出现频率远超想象而根因几乎都是同一个没有开patch_artistTrue。前面提到过默认情况下bp[boxes]里是Line2D对象你对一个Line2D调用set_facecolor()它要么没有效果要么在特定版本里直接报错。这是一个静默失败型的问题——代码不报错你甚至以为自己设置成功了但图上看就是没有填充色。另一个容易踩的点是bp[boxes]是一个列表不是单个对象。有些初学者会写bp[boxes].set_facecolor(red)然后报AttributeError。因为列表没有set_facecolor方法你得遍历它或者用zip跟颜色列表配对。写代码时要记住matplotlib返回的字典里几乎所有键对应的都是列表因为一张图上可能有多个箱子、多条须线、多个异常点。之前也提过set_alpha()这个接口它作用于整个Patch对象。如果你既想箱体半透明又想边框不透明那得拆开操作透明度属性是整体生效的没法只对填充部分生效。遇到这种需求我的做法是放弃alpha改用“括号颜色”来模拟——把边框设置成深色填充色用本身较浅的颜色效果接近半透明且更可控。5.3 输出与展示环节的常见坑图在屏幕上画好了真正的考验在导出环节。plt.show()显示的窗口分辨率通常不高直接截图放到报告里会发虚。正确做法是用savefig导出并且显式指定dpifig.savefig(boxplot_result.png, dpi300, bbox_inchestight)dpi300适合打印和PPT插入bbox_inchestight会自动裁剪掉图周围的空白边距避免坐标轴标签被截断。如果你要放到深色背景的PPT里可以加一个transparentTrue参数让图的背景变成透明这样跟PPT的深色背景无缝衔接。还有一个我自己反复踩的坑是中文标签导出问题。在屏幕上预览正常但导出成PDF或SVG之后中文全部变成方块。这是因为PDF和SVG在渲染字体时走了不同的字体子系统。解决办法是保存前把字体嵌入方式调整一下或者干脆导出PNG格式的图。PNG是位图所见即所得最省心。如果是论文投稿要求矢量图那我建议先确认系统里装了完整的中文字体再导出PDF。6. 常见问题排查实录6.1 填色不生效的三种原因我把平时遇到最多的填色问题整理成了一张速查表方便你对照排查现象可能原因解决办法箱子还是空心没有任何填充色没开patch_artistTrue在boxplot()调用中加上该参数所有箱子颜色一样或者颜色顺序不对颜色列表与数据组数不匹配检查colors的长度是否等于数据组数用zip逐一对齐填色成功但被网格线或其他图形盖住绘制顺序或zorder问题先画网格再画箱线图或给箱体设置zorder3第三种情况多说一句如果你用ax.grid(True)把网格开在默认层级又先画了箱体网格线可能覆盖在箱体上面看起来就像颜色变淡了。解决方式很简单把ax.grid(axisy, zorder0)放在boxplot()之前执行或者给箱体补一个zorder3让数据图形始终在网格上面。6.2 图例与颜色对不上用ax.boxplot()画图时如果你不额外处理它是不会自动生成图例的。很多人在图上自己画了一堆带颜色的箱体然后想加图例却不知道从哪里入手。我的做法是用matplotlib.patches.Patch手动构造图例句柄from matplotlib.patches import Patch legend_handles [ Patch(facecolorcolors[0], label对照组), Patch(facecolorcolors[1], label方案A), Patch(facecolorcolors[2], label方案B), Patch(facecolorcolors[3], label方案C), ] ax.legend(handleslegend_handles, locupper right)Patch是一个“假的”图形块它不参与实际数据绘制只负责在图例里展示颜色和标签。这样构造出来的图例颜色和箱体填充色完全一致不会出现自动图例那种“线型对不上”“颜色偏了”的错位。关键是图例里的颜色要和箱体颜色用同一个colors列表别手动复制粘贴——手动复制最容易出错改一个忘一个。6.3 多子图场景下的颜色错位最后一个是多子图场景。比如你用plt.subplots(1, 2)画左右两张箱线图左边是四个季度的数据右边是四个产品线的数据。如果不注意很容易写出这样的代码两张图各自用一套颜色逻辑结果左边A产品是蓝色右边A产品变成了橙色。我常用的做法是先定义一份全局颜色映射比如用字典把“类别名”绑定到固定颜色color_map { Q1: #4C72B0, Q2: #DD8452, Q3: #55A868, Q4: #C44E52 }然后不管画哪个子图都从同一个color_map里按名字取颜色。这样能保证跨子图的颜色语义一致。尤其是在自动化生成多张报表时这个习惯能帮你减少一大部分人工核对工作。多子图还有一个注意点boxplot()默认情况下每个子图的坐标轴范围是各自独立的。如果你想让两张图在同一个数值尺度下可比记得用ax.set_ylim()手动统一范围否则左边图的中位数位置和右边图之间没有可比性视觉上会误导读者。最后分享一点个人的实际体会。我做数据报表这几年发现“把图做得好看”这件事从来不是锦上添花而是信息传达效率的一部分。一张带自定义填充颜色的箱线图颜色选对了读者扫一眼就能得出结论颜色选错了再扎实的统计结果也会被埋没。我自己每次画完图都会做一个简单的自检遮住图例看能不能仅凭箱体颜色快速说出每个分组是谁。如果做不到说明颜色编码还不够直观需要重新调整。这个习惯帮我避免了不少次“图出了但没人看懂”的尴尬。如果你也在做类似的可视化工作不妨试试这个自检方法很便宜但很有效。