多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

Matplotlib柱状图全攻略:垂直、水平、分组、堆叠实践

Matplotlib柱状图全攻略:垂直、水平、分组、堆叠实践 做了多年的数据可视化我越来越觉得 Matplotlib 的柱状图被很多人低估了。一提到类别对比大家脱口而出就是垂直柱状图但真到了业务场景里——品牌名太长、多系列并排、想同时看总量和构成基础画法根本不够用。这篇攻略我会把垂直、水平、分组、堆叠四类柱状图一次讲透从场景选型到参数细节再到我踩过的坑全部按实操顺序整理出来让你拿到任何类别对比需求都能快速画出专业又清晰的图。这四类图是 Matplotlib 柱状图体系的核心骨架。垂直柱状图适合类别少、标签短的常规对比水平柱状图专门解决长标签和大量类别的排列问题分组柱状图处理多系列并列对比堆叠柱状图则能同时呈现总量和内部构成。理解它们的适用边界比记住上百个 API 参数重要得多。建议搭配官方示例一起看但重点是搞清楚每个参数为什么这么设。1. 先想清楚柱状图要表达什么1.1 柱状图的底层逻辑和使用边界柱状图的本质是用矩形长度编码数值大小人类对长度差异的感知远比对面积、角度更敏感所以它在类别对比场景中几乎是万能选项。但要明确一点柱状图的横轴是离散类别不是连续数值轴这也是它和折线图的核心区别。实际工作中90% 的可视化问题都出在“没想清楚要表达什么”就急着绘图。拿到数据后先问自己三个问题类别数量有多少标签长度如何要对比的是单一指标还是多维指标这决定了该用哪种柱状图。我个人习惯在动手前画一张草稿哪怕最简单的四个柱子的草图也有助于理清思路。柱状图适合类别数量在 2 到 20 个之间的场景。少于 2 个直接写数字即可多于 20 个柱子时再宽的画布也会让每根柱子显得纤细且难以区分这时更适合用箱线图或热力图。这是个硬性边界碰到类别特别多的需求最好先做数据聚合或筛选。1.2 四类柱状图的选型决策法我总结出一个简易的选型流程。单一系列、类别名短、类别少于 10 个时优先用垂直柱状图。类别名较长超过 6 个中文字符或类别数量在 10 到 20 个之间水平柱状图更合适因为水平方向的画布宽度可以容纳长标签而且柱子由上到下排列符合阅读习惯。多个系列要并列对比时首选分组柱状图每个类别下用不同颜色的柱子并排便于横向比较系列差异。想同时展示总量和构成或者突出某一部分在整体中的占比用堆叠柱状图但注意堆叠不适合精确比较各系列数值因为内部段不是从零开始的。另外如果系列数量超过 4 个分组柱状图会很拥挤堆叠图也不容易看清各段变化这属于多维度对比应该考虑使用分面图或折线图。选型本身就是可视化最重要的前置工作图选对了后面只需调样式就能出效果。2. 垂直柱状图最基础的类别对比方案2.1 从零开始绘制一幅标准垂直柱状图垂直柱状图是入门基础几乎所有 Matplotlib 用户的第一幅图都是它。最简单的实现代码非常直白import matplotlib.pyplot as plt import numpy as np categories [产品A, 产品B, 产品C, 产品D, 产品E] sales [120, 235, 180, 290, 160] fig, ax plt.subplots(figsize(8, 5)) ax.bar(categories, sales, color#4C72B0, edgecolorwhite, linewidth0.8) ax.set_title(各产品销售情况, fontsize14, pad12) ax.set_xlabel(产品名称, fontsize11) ax.set_ylabel(销量件, fontsize11) ax.grid(axisy, alpha0.3, linestyle--) plt.tight_layout() plt.show()这里有一个关键概念值得展开axes 对象。很多初学者直接使用plt.bar()但一旦进入分组、堆叠或自定义坐标轴阶段直接操作 Axes 对象通过fig, ax plt.subplots()获得会灵活得多。它就像一张画布条形、文字、图例都是在这张画布上叠加而不是直接画在整个窗口上。颜色设置上color参数接受单个颜色值也接受颜色列表。如果希望每根柱子颜色不同可以传入列表color[#4C72B0, #DD8452, #55A868, #C44E52, #8172B3]。配色是柱状图的脸面我建议优先参考 Matplotlib 自带的tab10、tab20色系它们经过专业调色不会出现饱和度失衡的问题。或者你直接使用plt.colormaps[viridis]这类渐变色映射。2.2 数据标签、网格与坐标轴细节处理光有柱子还不够一张合格的柱状图必须在柱顶标注数值否则读者要对着坐标轴猜具体大小这在汇报场景中显得不够专业。核心思路是遍历每个柱子Bar Container取它的位置和高度在对应坐标处添加文字bars ax.bar(categories, sales, color#4C72B0, edgecolorwhite) for bar in bars: height bar.get_height() ax.text(bar.get_x() bar.get_width()/2, height 5, f{height}, hacenter, vabottom, fontsize10)bar.get_x()返回柱子左侧的 x 坐标get_width()是柱子宽度两者相加再除以 2 得到柱子的中心横坐标这样才能让标签完全居中。hacenter是水平居中vabottom是垂直对齐到柱子顶端并稍微偏移偏移量由height 5控制这个值需要根据数据量级灵活调整数值过小标签会贴住柱顶过大则显得疏离。网格线的设置也值得讲究。默认的ax.grid(True)会在水平和垂直方向都画线视觉上比较嘈杂。我通常只保留 y 方向网格ax.grid(axisy, alpha0.3, linestyle--)让 x 方向保持干净有助于以数值轴为参考读取高度差又不会干扰类别的识别。坐标轴范围方面柱状图默认从 y0 开始这是正确且必须的。柱状图的长度编码数值大小如果截断 y 轴柱子的视觉比例就会失真这在数据可视化领域是一条原则性底线。除非你的场景确实需要从某个非零值开始对比差异否则保持从零开始。3. 水平柱状图长标签与排名场景的解法3.1 什么时候该放弃垂直改用水平很多需求在垂直柱状图上表现不佳但只要把图转 90 度问题立刻消失。典型场景有两类。一是类别名称过长。如果类别名是完整的公司名例如东北区域营销中心把它放在垂直柱状图的 x 轴上要么斜着摆放显得凌乱要么自动截断导致信息丢失。水平柱状图将标签放在 y 轴左侧可随意换行长度空间充裕。二是类别数量多且带有序次。当你有 15 个省份需要按照指标排名时垂直柱状图的可读性非常差而水平条形图从上到下排列配合从大到小的数值排序效果就像一份排行榜读者可以很自然地按顺序扫描。这种视觉流向符合人的阅读习惯从上往下逐条比较。在 Python 中实现水平柱状图几乎零成本只要把ax.bar换成ax.barh。y 轴不再是类别名而是各类别的数值高低维度x 轴变成数值轴。categories [东北区域营销中心, 华北区域营销中心, 华东区域营销中心, 华南区域营销中心, 西南区域营销中心, 西北区域营销中心] values [86, 75, 92, 78, 90, 68] fig, ax plt.subplots(figsize(8, 5)) ax.barh(categories, values, color#55A868, edgecolorwhite) for bar in ax.patches: width bar.get_width() ax.text(width 1, bar.get_y() bar.get_height()/2, f{width}, haleft, vacenter, fontsize10) ax.set_xlabel(业绩得分) ax.set_title(各区域年度业绩评分, fontsize14, pad12) ax.set_xlim(0, 100) plt.tight_layout() plt.show()需要特别提醒的是bar.get_x()与bar.get_y()的语义在 bar 和 barh 之间的差异。ax.bar产生的 bar 对象get_x()是柱子左下角的横坐标get_width()是横向宽度而ax.barh产生的 bar 对象get_y()才是类别的纵坐标get_width()变成了水平方向的数值长度。这里容易混淆导致数据标签位置错乱。3.2 排名场景中如何正确排序水平柱状图最常见的落地场景是排名。很多人画商会排名、销售排行榜直接把 Excel 里的原始顺序拿过来画结果是柱子高低起伏毫无规律阅读体验差。正确做法是先排序再绘图。实际上对一个 NumPy 数组做排序是非常简单的操作用np.argsort或者列表的sorted方法即可。以列表为例categories [A, B, C, D, E, F] values [86, 75, 92, 78, 90, 68] sorted_pairs sorted(zip(values, categories), reverseTrue) sorted_values [v for v, _ in sorted_pairs] sorted_categories [c for _, c in sorted_pairs]排完序后柱子从长到短依次排列一眼就能看出第一名是谁。这里要注意zip(values, categories)得到的是元组列表排序时默认先按第一个元素排序所以把 values 放在前面。如果想要从短到长排列reverseTrue可以控制升降序。排名图惯例是从高到低所以降序是主流。排序后还要考虑 y 轴方向的问题。默认ax.barh会把第一个类别放在最上面这正好符合降序排行榜的视觉习惯不需要额外调整。但如果你的数据本身是时间顺序或固定顺序那就不应该排序保持原始顺序即可。排序是有意识的数据叙事不排序也有它的合理性关键是明确自己想让读者看到什么。4. 分组柱状图多系列对比不是简单的多画几个柱子4.1 分组柱状图的原理与横坐标定位分组柱状图的本质是在同一个类别下用不同颜色的柱子并排列出多个系列的数值。但 Matplotlib 的bar方法并不会自动为你分组它只是在指定的 x 坐标上画柱子。要实现分组效果你必须手动计算每个柱子应该在哪个精确的 x 坐标位置。假设两个系列每个类别的宽度为 1柱子宽度设为 0.8那么第 1 根柱子的中心位置在类别坐标 - 0.2第二根柱子在类别坐标 0.2。这样两根柱子之间的间距是 0.4既不会重叠又能明显看出它们属于同一个类别。import numpy as np categories [2021, 2022, 2023, 2024] series1 [120, 135, 145, 160] series2 [80, 110, 130, 150] x np.arange(len(categories)) width 0.35 fig, ax plt.subplots(figsize(8, 5)) bars1 ax.bar(x - width/2, series1, width, label产品A, color#4C72B0) bars2 ax.bar(x width/2, series2, width, label产品B, color#DD8452) ax.set_xticks(x) ax.set_xticklabels(categories) ax.set_ylabel(销量件) ax.set_title(近四年产品销量对比, fontsize14, pad12) ax.legend() ax.grid(axisy, alpha0.3, linestyle--) for bar in list(bars1) list(bars2): height bar.get_height() ax.text(bar.get_x() bar.get_width()/2, height 3, f{height}, hacenter, vabottom, fontsize9) plt.tight_layout() plt.show()这里x - width/2和x width/2是核心。np.arange(len(categories))生成一个数组 [0, 1, 2, 3]代表类别的基本坐标width是柱子宽度每组两个系列时第一个系列偏移-width/2第二个系列偏移width/2视觉上两个柱子以类别坐标为中心完美对称。三组及以上的系列也同理偏移量从-(n-1)*width/2到(n-1)*width/2均匀铺开。4.2 三系列及以上场景的通用公式当系列数增加到 3 个或 4 个时手动写x - width/2、x width/2就会变得繁琐且易错。你应该在代码里用循环一次生成。series_data [series1, series2, series3] series_labels [产品A, 产品B, 产品C] colors [#4C72B0, #DD8452, #55A868] n_series len(series_data) width 0.25 for i, (data, label, color) in enumerate(zip(series_data, series_labels, colors)): offset (i - n_series/2 0.5) * width bars ax.bar(x offset, data, width, labellabel, colorcolor)这个offset公式值得拆解。数组长度为 3 时n_series/2是 1.50.5 - 1.5 -1.0、1.5 - 1.5 0.0、2.5 - 1.5 1.0三根柱子的偏移量是 -1.0、0.0、1.0 倍宽度正好均匀覆盖。数组长度为 4 时偏移量为 -1.5、-0.5、0.5、1.5 倍宽度。这个公式的本质是将系列索引映射到 [-n/2, n/2] 的对称区间保证所有柱子以类别坐标为中心。分组柱状图最多建议 4 个系列。系列过多时柱子会变得非常窄标签挤成一团视觉上每一组的柱子也难以快速对应到图例。遇到这种情况我会考虑分面或者改用其他图表类型。另外分组柱状图的柱子宽度和组间距需要精心调优。宽度建议在 0.2 到 0.4 之间具体取决于系列数系列多就窄一点系列少可以宽一点。组间距由类别坐标x的步长决定默认为 1主要看整体图面是否舒适。5. 堆叠柱状图总量与构成的并行展示5.1 堆叠柱状图的实现思路与标签处理堆叠柱状图是把多个系列的柱子放在同一类别上一个叠一个柱子总高度等于各系列数值之和。它适合回答总量趋势 内部构成变化这两类问题。比如你想观察某产品线 4 年的总体营收变化同时看哪部分业务贡献最多堆叠图就非常直观。实现堆叠的核心是bottom参数。第一个系列正常绘制第二个系列的bottom设为第一个系列的值第三个系列的bottom设为前两个系列的和以此类推。bottom意思是从这个高度开始往上画。categories [2021, 2022, 2023, 2024] region_a [50, 60, 70, 80] region_b [40, 45, 50, 55] region_c [30, 35, 40, 42] fig, ax plt.subplots(figsize(8, 5)) ax.bar(categories, region_a, label区域A, color#4C72B0) ax.bar(categories, region_b, bottomregion_a, label区域B, color#DD8452) ax.bar(categories, region_c, bottomnp.array(region_a) np.array(region_b), label区域C, color#55A868) ax.set_ylabel(营收万元) ax.set_title(各区域年度营收贡献, fontsize14, pad12) ax.legend() ax.grid(axisy, alpha0.3, linestyle--) plt.show()这组代码里隐藏着一个常见坑当categories是字符串列表时bottom参数也可以直接传列表代码能正常运行。但如果你要对数值做加法——比如第三个系列的bottom是region_a region_b——就会碰壁因为两个列表直接相加在 Python 里是拼接而不是元素相加。这是新手最容易踩的雷正确写法是np.array(region_a) np.array(region_b)。如果你不想引入 NumPy也可以用列表推导式[a b for a, b in zip(region_a, region_b)]。为堆叠柱状图添加数据标签比普通柱状图复杂一些。柱顶标签要标注的是累计高度而不是每个段本身的数值否则累计高度的坐标就没了基准。每个类别下不同段落的中间位置计算公式是底部 段高度 / 2这样标签才会落在段落中央。我通常会为每个段落添加带白色背景的文本避免重叠影响可读性——这在段数较多时尤为重要。5.2 百分比堆叠柱状图把绝对数值变成占比堆叠柱状图虽然能看出构成变化但如果你更关心的是占比变化而不是绝对数值建议使用百分比堆叠图。做法是在绘图前先对每行数据进行归一化让每个类别的各系列之和等于 100。data np.array([region_a, region_b, region_c]) percent_data data / data.sum(axis0) * 100这里的data.sum(axis0)得到每一列的总和即每个年份的营收合计然后逐元素相除得到每个系列在对应年份的占比。接下来用这个归一化后的数据绘制堆叠图y 轴就变成了百分比单位。x np.arange(len(categories)) colors [#4C72B0, #DD8452, #55A868] bottom np.zeros(len(categories)) for i, (series, label, color) in enumerate(zip(percent_data, labels, colors)): ax.bar(x, series, bottombottom, labellabel, colorcolor) for j, value in enumerate(series): ax.text(x[j], bottom[j] value/2, f{value:.0f}%, hacenter, vacenter, fontsize9, colorwhite) bottom series变量bottom的更新是最关键的一步。每画完一个系列就用bottom series把累计底部高度向前推进这样下一个系列能准确地画在已有部分的上面。这种滚动累加的方式比手动计算每个系列的bottom高效得多也是官方示例中推荐做法。百分比堆叠图中每个段中心位置的标签就是百分比直接标在段中间既准确又美观。需要提醒的是堆叠图不适合做精确的数值比较。人眼很难判断一个段在非零基线以上的精确高度所以如果你需要精确对比区域 A 四年的变化堆叠图不是好选择分组柱状图或折线图更合适。堆叠图的价值在于宏观趋势与构成变化这一点在向业务方解释时要明确沟通。6. 常见问题与排查技巧实录6.1 柱子太窄、标签重叠、图例错乱我在实际项目中积累了一套排查清单先处理视觉问题。柱子太窄多半是画布尺寸和柱子宽度不匹配。-figsize设为(4, 3)可为只有 5 个类别的图预留更宽的画布width也需要相应调整例如 6 个类别建议width0.510 个类别建议width0.7避免柱子之间都是留白。标签重叠先检查 x 轴标签是否需要旋转。类别名超过 5 个字时plt.xticks(rotation45, haright)会有效。对于柱顶数值标签重叠通常是字号太大或偏移量不足把fontsize降到 9或增大文本与柱顶的距离就能缓解。图例错乱最常见原因是label参数没有写在ax.bar()里而是写在了循环外面或者重复传了label导致图例重复。确保每个绘制系列只传一次label并调用ax.legend()。如果你用循环绘制多个系列label自然也要在循环里。6.2 渐变配色与动画导出等进阶需求渐变配色Matplotlib 本身支持渐变填充在柱状图里一般不太会出现批量使用的场景单一序列加渐变更多是为了装饰。办法是使用Imshow或者自定义color的色带但最简单的做法是使用 LinearSegmentedColormap 生成颜色列表后按照索引取色依次赋给各柱子。另一个常见需求是把柱状图保存成动图GIF本质是用matplotlib.animation.FuncAnimation逐帧调用更新函数然后用PillowWriter写入 GIF。这种动图适合演示柱子逐步增长的动态过程但不建议在正式报告中使用静态图更容易让读者聚焦数据。为柱状图添加交互当你在 Jupyter 中使用%matplotlib notebook模式时可以拦截鼠标事件在鼠标悬停位置显示对应的数据。一个常见的实现是利用mpl_connect绑定事件读取鼠标坐标用ax.get_xticks()反查最近类别再高亮对应的柱子。这个功能的代码实现有一定复杂度需要处理好坐标转换和事件循环。6.3 双 Y 轴柱状图重叠问题很多人在做双 y 轴图表时柱状图会完全重叠导致后画的系列盖住先画的系列。原因很简单在同一个坐标轴上重复调用bar()而且两个系列的数据量级相同时它们的 x 坐标完全相同。解决方案有三种调整透明度、偏移 x 坐标、改用twinx()后进行位置偏移。如果你的需求是同一个 x 类别下有两个柱子、各自使用不同的 y 轴那么就需要把其中一个柱子的 x 坐标整体偏移 -0.2另一个偏移 0.2同时设置一个width0.35左右的宽度让它们错开。最反直觉的是twinx()创建的新坐标轴它的默认数据映射是独立的但它并不会自动为你偏移柱子的 x 位置所以本质上还是手动偏移的问题。这个坑在 Origin 和 Excel 中习惯了的用户换到 Matplotlib 时经常踩。我实际处理双轴柱状图时更推荐的做法是双轴的情况下将其中一个数据系列改成折线而不是两个都是柱子这样视觉上更清晰也避免双轴柱子的宽度和位置错位。如果两个量纲单位完全不同比如销售额和增长率柱状图和折线图的组合其实是最合理的方案。6.4 中文显示与系统字体问题的排查Matplotlib 默认字体在多数系统上不支持中文绘制中文标签时会出现方块。这是每个中文用户的必经坑。解决办法分三步检查系统中文字体、配置 rcParams、重载字体缓存。最简单可靠的方式是plt.rcParams[font.sans-serif] [SimHei] # Windows # 或 [PingFang SC] # macOS # 或 [Noto Sans CJK SC] # Linux plt.rcParams[axes.unicode_minus] False # 解决负号显示为方块字体配置是全局性的前缀每次绘制前设置一次即可。如果你不想改动全局配置也可以在每个Axes对象上用fontproperties参数指定字体文件路径。这个方案在无 GUI 的服务端环境中特别有用你可以直接指定单个字体文件的路径不依赖系统是否注册。7. 实用技巧补充与选型参考7.1 配色策略与误差棒设置柱状图的配色我的经验是“先选一套再统一”。不要每根柱子单独定义颜色优先用tab10或Set2这类专业色环。在团队内部建议固化一个 color 列表命名为 COMPANY_COLORS放工具模块里保证所有图表风格统一。误差棒在柱状图上也很常用只要在ax.bar中加入yerr参数并传入一个误差列表即可yerr [3, 5, 4, 6] ax.bar(categories, sales, yerryerr, capsize3)这里capsize控制误差棒末端横线的大小。误差棒的目的是表达数据不确定性如果你的数据来自多次实验结果务必加上它。这也是柱状图区别于 Excel 默认图表的重要能力。7.2 排列组合的决策速查垂直柱状图适用类别少、标签短、单一或少量系列水平柱状图适用标签长、类别多、排名场景分组柱状图适用多系列2-4 个精确比较堆叠柱状图适用总量 构成展示。实际使用中可以重叠使用。比如水平 分组多系列长标签排名堆叠 水平多系列长标签构成分析这些都是我日常操作中的真实需求。关键是先明确你的业务问题再套用基础模板最后调整细节。不要为了花哨而使用复杂图表能用简单图表表达清楚的地方绝不加戏。7.3 导出设置与图像清晰度的最后一步图像导出是大多数人忽略的收尾工作。保存图片时使用dpi参数控制分辨率fig.savefig(sales_bar.png, dpi300, bbox_inchestight)dpi300是打印级别的清晰度bbox_inchestight可以自动裁剪空白边缘这是最重要的两个参数。如果是要放到网页中dpi 150 即可体积更小加载更快。transparentTrue参数可以做透明背景适合放到深色 PPT 中。SVG 是矢量格式适合做印刷和大尺寸缩放Word 或 PPT 中插入 SVG 也很方便。提示如果图片导出后中文显示为方块记得检查字体 rcParams 是否在保存前正确设置。另外图例位置locupper right在高分辨率下不会自动避让数据必要时手动用loc(1.02, 0.8)这类元组位置微调。我个人在项目里最常用的组合是figsize(8,5)配dpi150保证屏幕展示和打印都够用。图例、数据标签、网格线这三样都会占用画布空间tight_layout()能有效缓解布局问题。这是我整理 Matplotlib 柱状图完整攻略的核心思路。垂直、水平、分组、堆叠四种基础形态其实已经覆盖了我日常工作中 90% 的类别对比需求。最后分享一个小技巧当你在颜色、布局上反复调试时可以先把plt.show()换成plt.savefig(preview.png)在 Jupyter 里快速迭代调参速度会翻倍。这篇内容是我在多轮实战中总结出来的方法最朴素也最经得起反复使用。
返回列表