← 返回 Tessera
误差棒2 张图模拟三组给药 × 两个指标的均值和 SD2026-04-13

point_errorbar

几个组的点估计各是多少,误差范围重不重叠?

需要的输入
分类变量 + 点估计 + 误差范围(已汇总的表,不是原始观测)
示例数据
模拟三组给药 × 两个指标的均值和 SD
依赖
ggplot2 · ggpubr · dplyr · tidyr
配色
未命名#1B9E77 #D95F02 #7570B3
成图预览另有 1 张在配方里
point_errorbar

什么时候用它

手上只有汇总统计(均值和标准差),没有原始观测——这时候箱线图和小提琴图都画不了,只能画点加误差棒。

它也常被拿来替代柱状图。替代是对的:柱子从 0 画起,会让人不自觉地按面积比较,而均值之间的差异跟"从零到均值那一段"没有关系。点估计就是一个点,误差棒就是范围,没有多余的墨水暗示别的东西。

怎么读

  1. 先确认误差棒是什么。 ±SD±SE95% CI 是三件完全不同的事——图注里没写清楚,这张图就没法读
  2. 看点的位置。 那是点估计。
  3. 看棒的长度。 短说明估计稳/组内齐,长说明不稳/离散大——取决于第 1 条是哪一种。
  4. 看重不重叠。 误差棒重叠通常意味着差异不明确,但不重叠也不等于显著(尤其 SD 棒,它和显著性没有直接关系)。
  5. 看 y 轴从哪开始。 点图不必从 0 起,所以要留意轴的范围有没有把差异放大。

常见陷阱

  • 三种误差棒混用。 这是最常见也最严重的错误。SD 描述数据的离散,SE 和 CI 描述估计的不确定性,样本量大时后两者会缩得很小而 SD 不会。
  • 拿误差棒判断显著性。 两条 SE 棒不重叠约等于 p < 0.05,但两条 SD 棒不重叠什么也说明不了。要下结论就做检验。
  • 不同量纲挤在一个 y 轴上。 一个指标 50–70、另一个 2–3,画在一起小的那个会塌成一条线。分面。
  • 误差棒帽子太宽。 width 太大时帽子会比数据点还抢眼,0.2 左右够了。
  • 汇总数据画不出分布。 有原始观测的话,点加误差棒是信息量最少的那种画法。

配色

三个剂量组是有序的分类,但上面用的是定性配色(ColorBrewer 的 Dark2 前三个)。这是有意的:剂量顺序已经由 x 轴从左到右表达了,颜色再编码一次是重复——一个变量用一个视觉通道就够

黑白打印的场合可以再映射一个 shape。但只在颜色真的不够用时加,平时多一个通道就是多一分噪音。

配方

方法绘图系统什么时候选它
Ageom_errorbar() + geom_point()ggplot2所有指标在同一个量纲上,一个坐标系放得下
B+ facet_wrap(scales = "free_y")ggplot2多个指标量纲差得远——这是绝大多数真实情况

数据准备

这份数据已经是汇总表:一行是一个指标在一个组里的均值和 SD。

library(dplyr)
library(ggplot2)
library(ggpubr)

set.seed(20250821)

summary_data <- tidyr::expand_grid(
  metric = c("Response score", "Marker count"),
  status = c("Control", "Low dose", "High dose")
) |>
  mutate(
    # expand_grid 里后一个变量变得快,所以行序是
    # (Response×Control, Response×Low, Response×High, Marker×Control, …)
    # 直接按这个顺序给六个基准值,比 case_when 分支清楚,也不依赖回收规则
    base      = c(52.4, 58.7, 65.1,   2.1,  2.5,  3.2),
    noise_sd  = c( 1.2,  1.2,  1.2,  0.15, 0.15, 0.15),
    mean = round(base + rnorm(n(), 0, noise_sd), 2),
    sd   = round(c(runif(3, 5.0, 7.5), runif(3, 0.7, 1.4)), 2),
    # 显式定 levels:不定的话 x 轴会按字母排成 Control / High / Low,剂量顺序就乱了
    metric = factor(metric, levels = c("Response score", "Marker count")),
    status = factor(status, levels = c("Control", "Low dose", "High dose"))
  ) |>
  select(metric, status, mean, sd)

# 误差范围先算好,画的时候直接用 —— 也方便在这里一眼确认用的是 SD 不是 SE
plot_data <- summary_data |>
  mutate(ymin = mean - sd, ymax = mean + sd)

cols <- c("Control" = "#1b9e77", "Low dose" = "#d95f02", "High dose" = "#7570b3")

方法 A · 单坐标系

#| fig: basic
#| fig-width: 6
#| fig-height: 4
ggplot(plot_data, aes(x = status, y = mean, color = status)) +
  geom_errorbar(
    aes(ymin = ymin, ymax = ymax),
    width = 0.2,       # 帽子宽度;再大就比数据点还抢眼
    linewidth = 1
  ) +
  geom_point(size = 3) +   # 点画在误差棒之后,才会压在棒上面
  labs(
    x = "Group", y = "Value",
    color = "Mean ± SD"    # 图例标题直接写明误差棒是什么 —— 这是最省事的交代方式
  ) +
  expand_limits(y = 0) +   # 把 0 纳入范围,避免轴范围过窄放大差异
  scale_color_manual(values = cols) +
  theme_pubr(base_size = 12)
point_errorbar — basic
point_errorbar-basic

画出来就能看到问题:Response score 在 50–70,Marker count 在 2–3,后者被压成了贴着轴的三个点。

方法 B · 按指标分面

#| fig: facet
#| fig-width: 6.5
#| fig-height: 4
ggplot(plot_data, aes(x = status, y = mean, color = status)) +
  geom_errorbar(aes(ymin = ymin, ymax = ymax), width = 0.2, linewidth = 1) +
  geom_point(size = 3) +
  facet_wrap(
    ~metric,
    scales = "free_y"   # 关键:每个面板用自己的 y 轴范围,否则分面也救不了
  ) +
  labs(x = "Group", y = "Value", color = "Mean ± SD") +
  expand_limits(y = 0) +
  scale_color_manual(values = cols) +
  # x 轴标签缩短,三组挤在窄面板里才不会叠字
  scale_x_discrete(labels = c("Control" = "Control",
                              "Low dose" = "Low", "High dose" = "High")) +
  theme_pubr(base_size = 12) +
  theme(
    strip.background = element_blank(),   # 去掉分面标题的灰底,减少一层框
    strip.text = element_text(face = "bold", size = 12,
                              margin = margin(t = 14, b = 15)),
    legend.position = "top"
  )
point_errorbar — facet
point_errorbar-facet

两种方法怎么选

  1. 量纲一致就用 A。 同一个坐标系里能直接比高低,这是最强的比较方式,别轻易放弃。
  2. 量纲不一致必须用 B,而且要 scales = "free_y" 只分面不放开 y 轴等于没分——两个面板还是共用一个范围。
  3. 代价是跨面板不能比高低了。 y 轴不同的两个面板之间,位置高低没有意义。这时读者只能在每个面板内部比较,这一点最好在图注里说一句
  4. B 还有个小代价:两个面板共用一个 y 轴标题。 上面写的 y = "Value",对两个量纲不同的指标来说是句废话——真正的指标名只出现在分面标题里。要让每个面板有自己的 y 轴标题,就得放弃 facet_wrap:两张图分开画,再用 patchwork 的 p1 | p2 拼,plot_layout(guides = "collect") 把重复的图例合成一个。代码长不少,换来的是两个面板在视觉上就是两张独立的图——读者不会再想着比它们的高低。
运行环境4 个包 · 2026-08-03

R version 4.5.1 (2025-06-13 ucrt) · x86_64-w64-mingw32

  • dplyr 1.2.1
  • ggplot2 4.0.3
  • ggpubr 0.6.2
  • tidyr 1.3.2