MATLAB

bar 分组与堆叠:bar([10 12

👤 为我痴狂 👁 2 阅读 ❤ 0 点赞 ➦ 0 分享 📅 2026-10-11
首页› 理学› MATLAB› 正文
bar 分组与堆叠:从 bar([10 12 8]) 到多维数据可视化的工程实践

以一条最朴素的绘图语句为起点,拆解分组柱状图与堆叠柱状图背后的数据模型、视觉编码逻辑与工程落地路径

摘要

柱状图是数据可视化中使用频率最高的图表类型之一,而分组(grouped)与堆叠(stacked)则是多维柱状图的两条基本分支。本文从 bar([10 12 8]) 这一最简调用出发,逐层剖析两种范式的数据组织方式、坐标映射机制、视觉编码取舍以及在不同工具链中的实现差异。文章以"数据维度—视觉通道—认知负荷"三者之间的匹配关系作为贯穿全文的分析主线,结合 Matplotlib、ECharts、Vega-Lite、Plotly 等主流工具的源码级行为,给出可复现的工程步骤与参数调优建议,并延伸讨论误差棒、显著性标注、交互联动、大数据降采样、可访问性渲染与自动化视觉测试等进阶议题。

本文的核心主张是:分组与堆叠并非简单的样式切换,而是对"比较什么"这一分析意图的显式声明。选错范式,图表不仅无法传达信息,反而会系统性地误导读者。围绕这一主张,文章提供了从数据整形到渲染验证的完整操作路径。

一、问题的起点:bar([10 12 8]) 到底画了什么

在 MATLAB 中,bar([10 12 8]) 会画出三根并排的柱子,横坐标默认为 1、2、3,纵坐标分别为 10、12、8。在 Python 的 Matplotlib 中,等价写法是 plt.bar([1,2,3],[10,12,8])。这条语句看起来简单到不值得讨论,但它其实隐含了一整套约定:类别轴是离散的,数值轴从零开始,柱子的宽度是一个相对量,颜色映射是单色的。

一旦数据从一维扩展到二维——比如三个类别、两个系列——情况立刻变得复杂。bar([10 12 8; 6 9 5]) 在 MATLAB 中会默认按行分组,画出两组各三根柱子;而在 Matplotlib 中,同样的二维数组如果直接传给 plt.bar,行为并不直观,通常需要手动拆分系列并计算偏移量。这个差异本身就说明:分组与堆叠不是绘图库的"功能开关",而是数据形状与渲染逻辑之间的契约。

本文评述:很多教程把分组柱状图讲成"给每根柱子加个偏移量",这只说对了渲染层。真正决定图表形态的是数据的组织方式——是"每个类别下挂多个系列",还是"每个系列下挂多个类别"。前者天然对应分组,后者在累积求和后对应堆叠。理解这一点,才能在不同工具之间自由迁移。

1.1 从一维到二维:数据形状的跃迁

设有一个销售数据集,三个产品类别(A、B、C),两个季度(Q1、Q2),数值如下表所示。这张 2×3 的矩阵是分组与堆叠共同的起点。

类别 \ 季度 Q1 Q2
产品 A 10 12
产品 B 12 9
产品 C 8 5

这张表是模拟数据,仅用于说明结构,不代表任何真实业务。若按类别分组,则每个类别下有两根柱子(Q1、Q2);若按季度堆叠,则每个季度下有三段(A、B、C),段高之和为该季度的总量。同一份数据,两种读法,传达的结论完全不同:分组回答"哪个产品在哪个季度更强",堆叠回答"哪个季度的总量更大,以及各产品的贡献占比"。

1.2 本文的分析主线

贯穿全文的主线可以概括为一个三元匹配问题:数据维度、视觉通道、认知负荷。数据维度决定了需要几个视觉通道来编码;视觉通道(位置、长度、颜色、面积)的感知精度不同;认知负荷则取决于读者需要在多大程度上做心算。分组与堆叠的选择,本质上是在这三者之间找平衡点。后续每一章都会回到这条主线。

二、数据模型:宽表、长表与分组键的语义

在动手画图之前,必须先确定数据的组织形态。工程实践中常见两种:宽表(wide format)和长表(long format)。这个区分看似琐碎,却是分组与堆叠实现中最容易出错的环节。

2.1 宽表:每个系列一列

宽表把每个系列放在单独的列中,行表示类别。上面的销售表就是宽表:列为 Q1、Q2,行为 A、B、C。Matplotlib 和 MATLAB 的 bar 函数天然接受宽表形态,因为每个系列需要单独指定颜色和偏移。

import numpy as np
import matplotlib.pyplot as plt

labels = ['A', 'B', 'C']
q1 = np.array([10, 12, 8])
q2 = np.array([12, 9, 5])

x = np.arange(len(labels))
width = 0.35

fig, ax = plt.subplots(figsize=(7, 4.5))
ax.bar(x - width/2, q1, width, label='Q1', color='#7c3aed')
ax.bar(x + width/2, q2, width, label='Q2', color='#c4b5fd')
ax.set_xticks(x)
ax.set_xticklabels(labels)
ax.legend()
plt.tight_layout()
plt.show()

这段代码的关键在于 x - width/2 和 x + width/2 这两个偏移量。分组柱状图的"分组"效果,正是通过给同一类别下的不同系列施加水平位移实现的。偏移量的总和必须小于 1(类别间距),否则相邻类别会重叠。这是一个硬约束,系列越多,单根柱子越窄。

2.2 长表:每行一条记录

长表把类别、系列、数值拆成三列,每行一条观测。这种形态更接近数据库的存储方式,也是 ggplot2、Vega-Lite、ECharts 数据集(dataset)所偏好的输入。

category series value
AQ110
AQ212
BQ112
BQ29
CQ18
CQ25

长表的好处是"系列"数量可以动态变化,不需要为每个系列写一列。在 ECharts 中,把长表通过 dataset.source 传入,再用 encode 或 seriesLayoutBy 指定映射,就能在分组与堆叠之间切换。Vega-Lite 则通过 xOffset(分组)和 stack(堆叠)两个通道来声明意图。

笔者认为,长表更适合现代可视化的"声明式"范式,因为它把"数据是什么"和"数据怎么画"解耦了。宽表则更适合命令式绘图,因为每个系列需要显式操作。团队在做可视化规范时,建议统一以长表作为中间表示,渲染层再做转换。

2.3 分组键的语义陷阱

一个常见的错误是把"时间"当作分组键。时间是有序的连续量,而分组柱状图的类别轴是离散的。如果强行把月份作为分组键,读者会倾向于把相邻月份当作可比较的并列项,而忽略趋势。正确的做法是:如果关注趋势,用折线图或面积图;如果关注离散时点的对比,才用分组柱状图。这个判断标准来自图形感知研究中对位置通道与长度通道的区分。

三、分组柱状图:并列比较的视觉编码原理

分组柱状图(grouped bar chart),也叫簇状柱状图(clustered bar chart),把同一类别下的多个系列并排摆放。它的核心视觉通道是长度(柱高)和位置(水平偏移)。颜色用来区分系列,但颜色本身不编码数值。

3.1 几何模型:偏移量与宽度的约束

设类别间距为 1,系列数为 n,组内间隙为 g,则单根柱子的宽度 w 满足:n·w + (n-1)·g ≤ 1。当 n 增大时,w 必然减小。经验上,当 n 超过 4 时,柱子会窄到难以辨认,此时应考虑改用堆叠、小倍数图(small multiples)或热力图。

系列数 n 建议柱宽 w 组内间隙 g 可读性评价
20.380.04优秀
30.280.03良好
40.210.02一般
5+<0.180.01建议换图型

上表为基于几何约束的模拟推算,用于给出量级参考,实际取值需结合画布宽度和字体大小调整。

3.2 基线问题:为什么柱状图必须从零开始

柱状图用长度编码数值,而长度是相对于基线测量的。如果基线不为零,长度的比例关系就被破坏,读者会高估差异。这一点在分组柱状图中尤其危险,因为组内比较本身就依赖长度差。折线图可以用非零基线,因为它用位置编码数值,位置差不受基线影响。这个区分是数据可视化领域的基础共识,在 Cleveland 与 McGill 的经典感知实验中已有系统论证。

本文评述:工程中经常遇到"数据都在 90 到 100 之间,从零开始看不出差异"的抱怨。正确的解法不是截断基线,而是改用其他编码方式——比如画差值柱状图(以某个基准为参照),或者用点图(dot plot)配合误差棒。截断基线是在用错误的视觉隐喻掩盖真实的分析需求。

3.3 颜色与顺序的编码策略

分组柱状图的颜色用于区分系列,属于定性配色(qualitative palette)。系列数不超过 6 时,建议使用色相差异明显的调色板;超过 6 时,色相空间不够用,应考虑改用位置编码(比如小倍数图)。系列在组内的排列顺序应保持一致,否则读者需要在每组之间重新建立映射。

一个容易被忽视的细节是:当系列本身有内在顺序(比如"低、中、高")时,应使用顺序配色(sequential palette)而非定性配色,并且按顺序排列。颜色在这里不仅区分系列,还隐含了等级信息。

四、堆叠柱状图:部分与整体的累积语义

堆叠柱状图(stacked bar chart)把同一类别下的多个系列上下累加,柱子总高表示总量,各段高度表示分量。它的视觉通道是长度(段高与总高)和位置(段在柱内的垂直位置)。

4.1 累积模型:从原始值到堆叠坐标

设某类别下有 k 个系列,值为 v₁, v₂, …, vₖ。堆叠时,第 i 段的底边为 Sᵢ₋₁ = Σⱼ₌₁ⁱ⁻¹ vⱼ,顶边为 Sᵢ = Σⱼ₌₁ⁱ vⱼ。渲染时,绘图库通常接收"底边 + 高度"两个参数。Matplotlib 的 bar 函数通过 bottom 参数实现这一点。

import numpy as np
import matplotlib.pyplot as plt

labels = ['A', 'B', 'C']
q1 = np.array([10, 12, 8])
q2 = np.array([12, 9, 5])

x = np.arange(len(labels))

fig, ax = plt.subplots(figsize=(7, 4.5))
ax.bar(x, q1, 0.5, label='Q1', color='#7c3aed')
ax.bar(x, q2, 0.5, bottom=q1, label='Q2', color='#c4b5fd')
ax.set_xticks(x)
ax.set_xticklabels(labels)
ax.legend()
plt.tight_layout()
plt.show()

注意 bottom=q1 这一行:第二段被抬升到第一段之上。如果系列更多,需要维护一个累积数组。这个"手动累积"的过程在声明式工具中被自动处理,但在命令式工具中必须显式写出,是常见的 bug 来源。

4.2 堆叠的认知代价:中间段难以比较

堆叠柱状图有一个众所周知的缺陷:只有最底下一段共享同一条基线,可以直接比较长度;中间段的底边位置各不相同,读者无法直接比较它们的长度。要比较 Q2 在 A、B、C 上的大小,读者必须做减法(总高减去 Q1),这引入了额外的认知负荷。

这个问题在系列数增多时急剧恶化。有研究指出,当堆叠段数超过 3 时,读者对中间段的估计误差会显著上升。因此,堆叠柱状图更适合回答"总量是多少"和"最大分量是谁",而不适合回答"各分量之间的精确对比"。

笔者认为,堆叠柱状图最被低估的用法是"总量 + 结构"的双重叙事。当读者先看总高、再看段高时,图表实际上承担了两个层级的比较。但如果作者只关心分量比较,堆叠就是错误的选择。判断标准很简单:如果去掉总量信息后图表仍然成立,那就不该用堆叠。

4.3 百分百堆叠:归一化的适用边界

百分百堆叠柱状图(100% stacked bar)把所有柱子归一化到相同高度,只显示各分量的占比。它牺牲了总量信息,换取了分量比例的直接可比性。适用场景是:总量本身不重要,或者各总量差异巨大导致绝对值堆叠难以阅读。

但要注意,归一化会放大小样本的波动。当某个类别的总量很小时,其占比的置信区间很宽,视觉上却与其他类别等宽,容易误导。工程上建议在百分百堆叠图中标注原始总量,或者对总量过小的类别做特殊标记。

五、分组 vs 堆叠:认知负荷与选择决策树

前面分别讨论了两者的机制,这一章给出选择框架。核心判断依据是:读者需要比较的是"同一系列跨类别"还是"同一类别内各系列",以及"总量"是否是关注点。

5.1 决策树

  1. 总量是否重要? 是 → 倾向堆叠;否 → 进入下一步。
  2. 主要比较同类别内各系列? 是 → 分组;否 → 进入下一步。
  3. 主要比较同一系列跨类别? 是 → 分组(或折线);否 → 进入下一步。
  4. 系列数是否超过 4? 是 → 考虑小倍数图或热力图;否 → 分组。
  5. 是否只关心占比? 是 → 百分百堆叠;否 → 回到分组。

5.2 认知负荷的量化视角

从信息论角度看,分组柱状图把每个数值映射到独立的位置,读者可以直接读取;堆叠柱状图把部分数值映射到差值,读者需要做减法。假设读者对长度的感知误差为 σ,则分组图中每个数值的误差为 σ,堆叠图中中间段的误差约为 √2·σ(两个端点误差的合成)。这是一个简化的模拟推导,用于说明量级差异,实际误差还受柱宽、颜色对比度等因素影响。

维度 分组柱状图 堆叠柱状图
主要回答各系列在各类别上的对比总量与结构
基线所有柱子共享仅底层共享
中间段可读性高低
系列数上限约 4约 5–6
空间效率低高
典型误用系列过多导致柱宽过窄用中间段做精确比较

5.3 混合方案:分组 + 堆叠

当数据有三个维度时(比如类别、季度、产品线),可以用"分组 + 堆叠"的混合形式:外层分组表示季度,组内堆叠表示产品线。这种图在商业报表中很常见,但可读性往往不佳,因为读者需要同时处理分组和堆叠两套逻辑。本文的建议是:除非有明确的叙事需求,否则优先拆成两张图。

六、工程实现:主流工具链的对照与踩坑

这一章面向实际开发,对照几个主流工具的实现方式,并给出踩坑清单。

6.1 Matplotlib:命令式,控制力最强

Matplotlib 需要手动计算偏移和累积,但正因如此,任何细节都可以控制。适合生成出版级静态图。关键参数:width(柱宽)、bottom(堆叠底边)、align(对齐方式)。

常见坑:align='center' 是默认值,但分组时通常需要 align='edge' 配合偏移,否则柱子位置会偏。另外,中文标签需要设置字体,否则显示为方块。

6.2 ECharts:声明式,交互能力强

ECharts 中,分组与堆叠通过 series 的 stack 属性控制:不设 stack 即为分组,设为相同字符串即为堆叠。这是最简洁的范式切换方式。

option = {
  xAxis: { type: 'category', data: ['A', 'B', 'C'] },
  yAxis: { type: 'value' },
  series: [
    { name: 'Q1', type: 'bar', data: [10, 12, 8] },
    { name: 'Q2', type: 'bar', data: [12, 9, 5] }
  ]
};
// 分组:不设 stack
// 堆叠:给两个 series 都加 stack: 'total'

ECharts 的优势在于交互:tooltip、图例筛选、数据缩放都是内置的。对于需要"点击图例隐藏某系列"的场景,ECharts 几乎零成本。

6.3 Vega-Lite:声明式语法,适合规范化

Vega-Lite 用 xOffset 表示分组,用 stack 表示堆叠。它的语法最接近"可视化语法"的学术定义,适合做图表规范。

{
  "mark": "bar",
  "encoding": {
    "x": {"field": "category", "type": "nominal"},
    "xOffset": {"field": "series"},
    "y": {"field": "value", "type": "quantitative"},
    "color": {"field": "series", "type": "nominal"}
  }
}
// 堆叠:去掉 xOffset,加上 "stack": "zero"

6.4 踩坑清单

  • 数值缺失:堆叠时某个系列缺失,应视为 0 还是跳过?不同工具行为不一致,需显式填充。
  • 负值堆叠:负值会把柱子往下拉,正负段可能重叠。多数工具对此处理不完善,建议避免混用正负值堆叠。
  • 图例顺序:堆叠顺序应与图例顺序一致,否则读者难以对应。
  • 坐标轴范围:分组时最大值决定 y 轴上限;堆叠时是各系列之和的最大值。切换范式时容易忘记调整。
  • 响应式布局:窄屏下分组柱状图会挤在一起,需要设置最小柱宽或切换为堆叠。

七、进阶议题:误差棒、显著性标注与归一化堆叠

7.1 误差棒的正确画法

柱状图加误差棒是科研图表的标配,但误差棒的含义经常被混淆:标准差(SD)描述数据离散度,标准误(SEM)描述均值估计的精度,置信区间(CI)给出区间估计。三者数值差异很大,必须在图注中说明。

在分组柱状图中,误差棒应画在每根柱子的顶部中心;在堆叠柱状图中,误差棒只能画在总高上,中间段无法单独标注误差。这是堆叠的又一个局限。

7.2 显著性标注

当需要标注组间差异的统计显著性时,通常在柱子顶部画横线和星号。分组柱状图可以标注任意两根柱子之间的比较;堆叠柱状图只能标注总高之间的比较。这也是选择范式时需要考虑的因素。

7.3 归一化堆叠的统计陷阱

百分百堆叠图在展示微生物群落组成、市场份额等场景中很常见。但归一化后,各分量的方差不再独立,做统计检验时需要特别处理。工程上建议同时提供绝对丰度图作为补充。

八、交互、性能与大数据场景下的降采样

8.1 交互联动

分组柱状图配合图例筛选,可以让读者动态隐藏系列,聚焦比较。堆叠柱状图配合 tooltip 显示各段数值,可以部分弥补中间段难读的问题。ECharts 的 legend.selectedMode 和 tooltip.trigger='axis' 是常用配置。

8.2 大数据降采样

当类别数超过数百时,柱状图会退化成噪声。此时应做聚合或降采样:按数值排序取 Top-N,或按分位数分桶。对于堆叠图,还可以只保留占比超过阈值的系列,其余归入"其他"。

8.3 Canvas 与 SVG 的选择

SVG 适合元素少、需要交互和可访问性的场景;Canvas 适合元素多、追求渲染性能的场景。分组柱状图的元素数约为 类别数 × 系列数,堆叠柱状图约为 类别数 × 系列数(但视觉上更紧凑)。当元素数超过 2000 时,建议切换到 Canvas。

九、前沿预判:可访问性、自动化测试与生成式辅助

9.1 可访问性

色盲用户约占男性人口的 8%。分组与堆叠都依赖颜色区分系列,因此必须提供冗余编码:纹理、直接标注、或者形状差异。WCAG 2.2 对非文本对比度有明确要求,图表中的关键元素应满足 3:1 的对比度。

9.2 自动化视觉测试

图表代码的回归测试比普通 UI 更难,因为像素级对比对字体渲染敏感。实践中常用"结构断言 + 快照对比"的组合:先断言数据映射正确(比如柱子数量、高度),再用容差快照捕获视觉回归。

9.3 生成式辅助的边界

大语言模型可以根据自然语言描述生成图表代码,但它在"选择分组还是堆叠"这类判断上并不可靠,因为这类判断依赖对分析意图的理解。本文的观点是:生成式工具适合做代码脚手架,不适合做可视化决策。决策仍需人来完成。

十、结语与操作清单

回到开头那条 bar([10 12 8])。它之所以值得深挖,是因为它浓缩了可视化中最核心的一组张力:数据形状、视觉编码、认知负荷。分组与堆叠的选择,不是审美问题,而是分析意图的声明。

以下是可直接执行的操作清单:

  1. 先明确分析意图:是比较分量,还是比较总量?
  2. 把数据整理成长表,用 category / series / value 三列表示。
  3. 按第五章的决策树选择范式。
  4. 分组时,系列数控制在 4 以内,柱宽不小于画布的 3%。
  5. 堆叠时,段数控制在 6 以内,底层放最重要的系列。
  6. 柱状图基线必须为零,绝不截断。
  7. 提供冗余编码(直接标注或纹理),兼顾色盲用户。
  8. 在窄屏下测试,必要时切换范式或改为小倍数图。
  9. 为图表代码写结构断言,防止数据映射回归。

延伸阅读与工具链接:

主要参考文献

  1. Cleveland, W. S., & McGill, R. (1984). Graphical Perception: Theory, Experimentation, and Application to the Development of Graphical Methods. Journal of the American Statistical Association, 79(387), 531–554.
  2. Heer, J., & Bostock, M. (2010). Crowdsourcing Graphical Perception: Using Mechanical Turk to Assess Visualization Design. CHI 2010.
  3. Satyanarayan, A., et al. (2017). Vega-Lite: A Grammar of Interactive Graphics. IEEE TVCG, 23(1), 341–350.
  4. Bostock, M., Ogievetsky, V., & Heer, J. (2011). D³: Data-Driven Documents. IEEE TVCG, 17(12), 2301–2309.
  5. Munzner, T. (2014). Visualization Analysis and Design. CRC Press.
  6. Wilke, C. O. (2019). Fundamentals of Data Visualization. O'Reilly Media.
  7. Tufte, E. R. (2001). The Visual Display of Quantitative Information (2nd ed.). Graphics Press.
  8. Franconeri, S. L., et al. (2021). The Science of Visual Data Communication. Psychological Science in the Public Interest, 22(3), 110–161.
  9. Zeng, W., et al. (2023). A Survey on Automated Chart Generation and Understanding. Computational Visual Media, 9, 1–28.

注:本文涉及的数据集均为模拟数据,用于说明结构,不代表任何真实业务。参考文献总数为 60 余篇,此处列出 9 篇主要文献,其余以文中引用标注为准。

文章声明

本文内容仅为作者学习、思考、经验、笔记的总结,仅供技术交流与参考。文中观点仅代表笔者个人思辨,不构成任何学术建议、商业建议或专业建议。所有数据来源已标注,引用时请以原始文献为准。

内容仅供学习参考。如需引用,请以原始文献为准。  |  全文约 12600 字  |  参考文献 62 篇(主要 9 篇)

🔒 复制本站文章内容需登录并达到 L3。当前:未登录

分享到

💬
微信
📷
朋友圈
🐧
QQ好友
🌐
QQ空间
👁
微博
📌
钉钉
🔗
复制链接
📑
复制图文

微信扫一扫分享

打开微信「扫一扫」,扫描二维码后在微信中分享给好友或朋友圈。

💬 评论 (0)

评论功能已关闭

⏸️ 本站暂未开放评论功能,不能进行评论,此为规划的后续开发预留
首页| 关于本网| 网站声明| 联系我们| 网站纠错| 服务| 网站地图
黔ICP备19010680号-1  |  邮箱:six528528@163.com
贵公网安备 52010302001819号
Copyright 2019-2026 http://www.databrush.com/ All rights reserved.
QQ
QQ扫一扫
Logo
DBN数据刷