Folioevanzhou.org
← 返回 Tessera
R2 张图2026-04-13

point_errorbar

One point per group for the estimate and a bar for its spread, with nothing drawn between zero and the estimate.

示例数据
mtcars
配色
bcell_atlas
语言
R
成图预览另有 1 张在配方里
point_errorbar

Introduction

点加误差棒展示每组的汇总统计:一个点表示均值,一根棒表示预先定义好的误差范围。手上只有均值和标准差时,它仍然能画;手上有原始观测时,也可以先按组汇总成同样的输入表。

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

读的时候按这个顺序:

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

Example Data

这张图层最终要的输入是:已经汇总好的表——分类变量 + 点估计 + 误差范围,一行一个组。原始观测需要先汇总;而一旦汇总,分布形状和单个观测就不会出现在图里。

这里直接从 mtcars 衍生汇总表:按 4、6、8 缸分组,分别计算燃油效率 mpg 和车重 wt 的均值与 SD。两个指标的量纲差着一个数量级——这正是 recipe 2 要解决的问题。

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

# 公开地址,和数据集页上「下载 CSV」给的是同一个
cars <- read.csv("https://assets.evanzhou.org/tessera/csv/mtcars.csv")

summary_data <- cars |>
  transmute(
    cylinders = factor(cyl, levels = c(4, 6, 8), labels = c("4 cylinders", "6 cylinders", "8 cylinders")),
    `Fuel economy (mpg)` = mpg,
    `Weight (1000 lb)` = wt
  ) |>
  tidyr::pivot_longer(
    cols = -cylinders,
    names_to = "metric",
    values_to = "value"
  ) |>
  summarise(
    mean = mean(value),
    sd = sd(value),
    .by = c(metric, cylinders)
  ) |>
  mutate(metric = factor(metric, levels = c("Fuel economy (mpg)", "Weight (1000 lb)")))

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

Palettes

三个气缸组是有序的分类,这里从站内定性色板 bcell_atlas 选取橙、紫、绿三色。跳过与橙色过近的粉色,让细误差棒之间仍有足够区分;气缸数顺序则继续由 x 轴从左到右表达。

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

# 按组名绑定,不按位置:分面之后各面板的行序不一定一致
group_levels <- levels(summary_data$cylinders)
cols <- setNames(
  get_palette("bcell_atlas", type = "qualitative")[c(1, 4, 3)],
  group_levels
)

Recipe

No. Method Input Data Palettes
1 ggplot2 plot_data cols
2 ggplot2 plot_data cols

1 · 单坐标系

两个指标共用一个 y 轴。同一个坐标系里能直接比高低,这是最强的比较方式。

#| fig: basic
#| fig-width: 6
#| fig-height: 4
ggplot(plot_data, aes(x = cylinders, y = mean, color = cylinders)) +
  geom_errorbar(
    aes(ymin = ymin, ymax = ymax),
    width = 0.2,       # 帽子宽度;再大就比数据点还抢眼
    linewidth = 1
  ) +
  geom_point(size = 3) +   # 点画在误差棒之后,才会压在棒上面
  labs(
    x = "Cylinders", y = "Value",
    color = "Cylinders"    # 误差棒含义写在图注;图例只负责识别组别
  ) +
  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,后者被压成了贴着轴的三个点。量纲差一个数量级,一个坐标系就放不下了。

2 · 按指标分面

#| fig: facet
#| fig-width: 6.5
#| fig-height: 4
ggplot(plot_data, aes(x = cylinders, y = mean, color = cylinders)) +
  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 = "Cylinders", y = "Value", color = "Cylinders") +
  expand_limits(y = 0) +
  scale_color_manual(values = cols) +
  # x 轴标签缩短,三组挤在窄面板里才不会叠字
  scale_x_discrete(labels = c("4 cylinders" = "4", "6 cylinders" = "6", "8 cylinders" = "8")) +
  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

代价是跨面板不能比高低了:y 轴不同的两个面板之间,位置的高低没有意义。这一点最好在图注里说一句。

Constraints

  • 误差棒是什么,图上看不出来。 SD 描述数据的离散,SE 和 CI 描述估计的不确定性;样本量大时后两者会缩得很小,而 SD 不会。三者画出来长得完全一样,图注不写这张图就没法读。
  • 别拿误差棒判断显著性。 重叠与显著性的关系取决于误差棒类型、置信水平和比较设计,不能直接从两根棒换算出 p 值。要下结论就做检验。
  • y 轴不从 0 起。 点图本来就不必从 0 画,所以轴范围能把差异放大到任意程度。看图先看轴。
  • 汇总数据画不出分布。 同样的均值和 SD 可以来自完全不同的形状。有原始观测就该画箱线或小提琴,点加误差棒是信息量最少的那种。

两个 recipe 怎么比

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

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

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