← 返回 Tessera
分布3 张图iris(R 内置,150 行 × 3 组)2026-04-13

violin

几组数据的分布形状是不是不一样——不只是均值差了多少?

需要的输入
1 个分类变量 + 1 个数值变量
示例数据
iris(R 内置,150 行 × 3 组)
依赖
ggplot2 · ggpubr · biopalette · ggstatsplot
配色
walter_white2#5AB5BF #808C56 #F29E6D
成图预览另有 2 张在配方里
violin

什么时候用它

箱线图给的是五个数(中位数、四分位、须),小提琴图给的是整条密度曲线。区别在于:箱线图看不出双峰。两组数据可以有完全相同的中位数和四分位距,一组是单峰、另一组是两个峰挤出来的,箱线图上它们长得一模一样。

所以用它的场合是:你怀疑组间差的不只是位置,还有形状。 如果只关心“哪组更高”,箱线图更省版面。

怎么读

  1. 先看位置。 哪组整体偏高、哪组偏低。
  2. 再看宽度。 越宽说明那一段数值附近的样本越密。
  3. 看是不是单峰。 出现两个鼓包就说明这一组内部可能还分着亚群——这是小提琴图唯一能给、箱线图给不了的信息。
  4. 看叠上去的箱线和散点。 箱线补中位数和四分位,散点补每个真实观测。
  5. 最后才看星号。 它是补充,不能替代前面四步。

常见陷阱

  • 小样本时形状不可信。 密度估计需要足够的点,n 小于二三十时"看起来有两个峰"很可能是噪声。这是最要紧的一条——所以强烈建议叠上散点,让人自己判断有多少数据在撑着那个形状。
  • 轮廓不是数据边界。 小提琴两端来自核密度估计,会超出实际最大最小值。trim = FALSE 让尾部更完整,但那不代表那里有数据。
  • 宽度的缩放方式会改变读法。 scale = "width" 让每组最宽处一样宽,适合比较形状;scale = "count" 会让样本少的组整体变窄,同时编码样本量。不要不写参数就假定宽度只由密度决定。
  • 显著不等于重要。 p 值受样本量影响,星号说的是"差异是否明确",不是"差异有多大"。
  • 比较太多会喧宾夺主。 四组就有六对比较,括号和星号会把图占满。
  • y 轴要留白。 统计标注需要顶部空间,不留会被裁掉。
  • 整体检验和两两检验要同一套假设。 Welch ANOVA 应接 Games–Howell;Kruskal–Wallis 应接校正后的非参数两两比较。不要把 ANOVA 和 Wilcoxon 括号凑在一张图里。

配色

三个物种是三个无序类别,使用定性色板 walter_white2 中的青、橄榄和橙三色,与 grouped scatter 的物种映射保持一致。这里有三个真实分组,就只取三色;不为了展示完整色板增加虚假类别。

颜色沿三个尺度保持同一种语义:小提琴是大面积半透明填充,散点是小面积原色,箱线是纸白底上的同色轮廓。这样既能看出色板铺成大色块时会不会太重,也能看出缩成小点后是否仍可辨。深灰色只用于点边界和文字,不编码第四个变量。

小提琴的透明度固定为 0.46。透明度会把颜色与纸白背景混合,因此图上看到的浅色不是色板原值;原值由轮廓和散点保留。只给填充设透明度,不把整个图层一起变淡。

配方

方法绘图系统什么时候选它
Ageom_violin() + 图层ggplot2要把密度、四分位和原始观测分层表达,或者图要进一套统一风格的报告
Bggbetweenstats()ggplot2(ggstatsplot 封装)探索阶段想一步拿到分布、检验和效应量,不在乎版面精简

数据准备

iris 的三个物种在数据里本来就是因子,但显式指定 levels 是个好习惯——否则组的顺序取决于字母序,换个数据集就变了

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

iris_data <- iris |>
  transform(
    Species = factor(Species, levels = c("setosa", "versicolor", "virginica"))
  )

# x 轴标签带上每组样本量 —— 读者判断"这个形状可不可信"要靠它
species_labels <- setNames(
  paste0(levels(iris_data$Species), "\n(n=",
         as.integer(table(iris_data$Species)), ")"),
  levels(iris_data$Species)
)

walter_white2 <- get_palette("walter_white2", type = "qualitative")
species_colors <- setNames(
  walter_white2[c(1, 2, 4)],
  levels(iris_data$Species)
)

pairwise <- list(
  c("setosa", "versicolor"),
  c("versicolor", "virginica"),
  c("setosa", "virginica")
)

y_max <- max(iris_data$Sepal.Length)

PAPER <- "#F4F3EE"
INK   <- "#353531"

方法 A · geom_violin() 叠图层

三层各司其职:小提琴给形状、散点给原始观测、最后盖上的窄箱线给四分位和中位数。箱线放在散点之后,避免中央摘要被点遮住。

#| fig: layered
#| fig-width: 8
#| fig-height: 7
violin_plot <- ggplot(
  iris_data,
  aes(x = Species, y = Sepal.Length, fill = Species, color = Species)
) +
  geom_violin(
    trim      = TRUE,    # 轮廓停在实际最小值和最大值;尾部形状仍是密度估计
    scale     = "width", # 每组最宽处相同,宽度只用来比较形状
    width     = 0.88,
    alpha     = 0.46,
    linewidth = 0.9
  ) +
  geom_jitter(
    # 种子绑在 position 上,而不是在外面写一句 set.seed()。抖动是**每次绘制时**
    # 算的,不是建对象时算的 —— 同一个对象画两次,点就落在不同位置。下面那张
    # 带统计标注的图用的正是这个对象,不绑种子的话两张图的散点会对不上。
    position = position_jitter(width = 0.08, seed = 123),  # 只水平抖,垂直抖会改数值
    shape = 21,
    color = INK,
    size  = 1.8,
    stroke = 0.35,
    alpha = 0.72
  ) +
  geom_boxplot(
    width = 0.11,           # 压窄,当作小提琴内部的一根"骨架"
    outlier.shape = NA,     # 离群点已经由上面的散点完整呈现
    fill = PAPER,
    alpha = 0.92,
    linewidth = 0.85
  ) +
  scale_x_discrete(labels = species_labels) +
  scale_y_continuous(
    breaks = seq(4, 8, 0.5),
    expand = expansion(mult = c(0.04, 0.08))
  ) +
  scale_fill_manual(values = species_colors) +
  scale_color_manual(values = species_colors) +
  guides(fill = "none", color = "none") + # x 轴已经写了组名,图例是重复信息
  labs(
    title = "Sepal length by species",
    subtitle = "Violin: density · box: quartiles · dots: observations",
    x = NULL,
    y = "Sepal length (cm)"
  ) +
  theme_pubr(base_size = 13, legend = "none") +
  theme(
    plot.title = element_text(face = "bold", size = 17),
    plot.subtitle = element_text(color = "#57564F", size = 10.5,
                                 margin = margin(b = 10)),
    axis.title.y = element_text(face = "bold", margin = margin(r = 10)),
    plot.background = element_rect(fill = PAPER, colour = NA),
    panel.background = element_rect(fill = PAPER, colour = NA)
  )

violin_plot
violin — layered
violin-layered

主图不放统计括号,让形状、四分位和原始点保持主角。要把统计结果直接加上去,就在同一个 violin_plot 上叠 stat_compare_means():它内部计算检验,括号位置由数据范围和 step.increase 自动安排,不需要另造结果表或手写绝对坐标。

整体使用 Kruskal–Wallis,两两使用 Wilcoxon 并做 Holm 校正;两层都是秩检验,假设保持一致。星号只负责快速扫读,具体 p 值仍应进入正文或结果表。

#| fig: stats
#| fig-width: 8
#| fig-height: 7
violin_plot +
  stat_compare_means(
    comparisons   = pairwise,
    method        = "wilcox.test",
    p.adjust.method = "holm",
    label         = "p.signif",
    step.increase = 0.09,
    tip.length    = 0.008,
    size          = 4.2,
    bracket.size  = 0.55
  ) +
  stat_compare_means(
    method = "kruskal.test",
    label.x = 1.05,
    label.y = y_max * 1.22,
    hjust = 0,
    size = 4
  ) +
  scale_y_continuous(
    breaks = seq(4, 10, 1),
    expand = expansion(mult = c(0.04, 0.3))
  ) +
  labs(subtitle = "Kruskal–Wallis overall · pairwise Wilcoxon with Holm correction") +
  theme(plot.margin = margin(12, 18, 16, 24))
violin — stats
violin-stats

这张图保留全部三对比较,是为了完整示范 API。真实分析里若只有预先指定的一两对比较,直接缩短 pairwise 列表;不要先算完所有组合,再按显著性挑着画。

方法 B · ggbetweenstats()

一个函数把小提琴、箱线、散点、整体检验、两两比较和效应量全都摆出来。

#| fig: ggstats
#| fig-width: 8
#| fig-height: 6
library(ggstatsplot)

ggbetweenstats(
  data = iris_data,
  x    = Species,
  y    = Sepal.Length,
  type = "parametric",          # 参数检验;"nonparametric" 走 Kruskal-Wallis
  pairwise.comparisons = TRUE,
  pairwise.display = "significant",
  p.adjust.method = "holm",
  var.equal = FALSE,            # Welch ANOVA;两两比较对应 Games–Howell
  mean.ci   = TRUE,
  results.subtitle = TRUE,      # 副标题里那一串统计量
  centrality.point.args = list(size = 4, color = INK),
  centrality.label.args = list(size = 3, nudge_x = 0.38,
                               segment.linetype = 4),
  point.args = list(
    position = position_jitterdodge(dodge.width = 0.55, seed = 123),
    alpha = 0.55,
    size = 2.2,
    stroke = 0.25
  ),
  boxplot.args = list(width = 0.18, alpha = 0.12),
  violin.args = list(width = 0.68, alpha = 0.28),
  ggtheme = ggpubr::theme_pubr(base_size = 14),
  ggplot.component = scale_color_manual(values = species_colors),
  title = "Iris: Sepal Length by Species",
  xlab = "", ylab = "Sepal Length"
)
violin — ggstats
violin-ggstats

两种方法怎么选

  1. 正式展示用 A。 三种视觉层各回答一个问题,字体、留白和颜色能与同一份报告里的其它图对齐。
  2. 探索阶段用 B。 一次拿到 Welch ANOVA、Games–Howell、效应量和置信区间,适合判断下一步该报告什么。
  3. 不要把 B 原样当成发表图。 副标题、比较括号、均值和侧边说明都在竞争注意力;确定结论后,回到 A,只保留研究问题真正需要的信息。
运行环境4 个包 · 2026-08-12T12:37:01.414+0800

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

  • biopalette 0.1.0
  • ggplot2 4.0.3
  • ggpubr 0.6.2
  • ggstatsplot 0.13.0