☰
R语言小提琴图进阶:ggplot2与tidybayes贝叶斯后验可视化实战
2026/10/14 1:14:43 网站建设 项目流程

1. 为什么小提琴图值得花时间深挖

做过数据分析的人大概都有这种体会:一组数据丢过来,均值、中位数、标准差算得明明白白,但真要跟别人讲清楚"这组数据到底长什么样",光靠几个数字根本说不透。这时候就得靠可视化。而在所有展示数据分布的图表里,小提琴图是我个人最偏爱的一种——它把箱线图的统计摘要和核密度估计的分布形状揉在了一起,既有骨架又有血肉。

但问题也来了。很多人用R画小提琴图,画出来的东西要么丑得没法放进报告,要么信息量太少跟箱线图没区别,要么就是贝叶斯分析之后的后验分布不知道怎么优雅地展示出来。我见过太多人画小提琴图就是一句geom_violin()完事,连密度曲线的平滑参数都没调过,更别提叠加观测点、分位数区间、贝叶斯最高密度区间这些东西了。

这篇内容就是冲着这些痛点来的。我会把ggplot2和tidybayes这两个包在小提琴图上的进阶用法掰开揉碎讲清楚,从最基础的几何对象原理,到多层叠加的图层逻辑,再到贝叶斯后验分布的可视化技巧,最后还会分享几个我踩过的坑和调试经验。不管你是刚学会ggplot2基础语法的新手,还是已经能画出一手漂亮图表的老手,应该都能从里面找到点有用的东西。

核心关键词先摆出来:小提琴图、ggplot2、tidybayes、核密度估计、贝叶斯后验分布、分位数区间、图层叠加。这些词后面会反复出现,但不会硬塞,都是跟着实际场景自然带出来的。

2. 小提琴图的底层逻辑与方案选型

2.1 小提琴图到底在画什么

先把原理说清楚。小提琴图的核心是核密度估计(Kernel Density Estimation,KDE)。你可以把它想象成:把每个数据点当成一颗小石子,每颗石子周围都有一圈"影响力",离石子越近的地方影响力越大。把所有石子的影响力叠加起来,就得到了一条平滑的密度曲线。这条曲线告诉你数据在不同取值附近的密集程度。

ggplot2的geom_violin()底层调用的就是R的density()函数来做这个估计。关键参数是bw(bandwidth,带宽),它控制核函数的宽度。带宽太小,密度曲线会过度拟合,出现很多毛刺;带宽太大,曲线会过度平滑,把真实的分布特征抹掉。ggplot2默认用的是Sheather-Jones方法自动选择带宽,大多数情况下够用,但遇到多峰分布或者样本量很小的时候,就需要手动干预了。

小提琴图跟箱线图的关系也值得说一嘴。箱线图展示的是五数概括(最小值、Q1、中位数、Q3、最大值),信息高度压缩但丢失了分布形状。小提琴图保留了完整的密度形状,但如果不叠加箱线图或者分位数标记,读者很难快速读出具体的中位数和四分位距。所以进阶用法的核心思路就是:用小提琴图展示形状,用叠加图层补充统计摘要。

2.2 为什么选ggplot2加tidybayes这套组合

R里面能画小提琴图的包不少,vioplot、beanplot、ggstatsplot都能干这事。但为什么我最终锁定ggplot2加tidybayes?

ggplot2的优势在于图层语法。小提琴图往往不是单独出现的,你需要叠加散点、箱线图、均值点、误差棒、显著性标记等等。ggplot2的+号语法让这些图层的叠加变得极其自然,每个图层各管各的,互不干扰。而且ggplot2的主题系统、配色系统、分面系统都是目前R可视化生态里最成熟的,没有之一。

tidybayes则是贝叶斯分析工作流里的神器。它做的事情很简单但极其关键:把贝叶斯模型输出的后验分布样本整理成tidy data格式,然后提供了一系列stat_和geom_函数,让你能直接用ggplot2的语法来画后验分布。比如stat_halfeye()可以画出带中位数点和分位数区间的小提琴图,stat_gradientinterval()可以画出渐变填充的区间图。没有tidybayes的话,你得手动从后验样本里算密度、算分位数、构造数据框,再一层层画上去,麻烦得要命。

这两个包配合起来,基本上覆盖了从频率派到贝叶斯派、从简单描述到复杂模型输出的所有小提琴图需求。下面这张表对比了几种常见方案:

方案优势劣势适用场景
base Rvioplot简单直接,一行代码图层不可叠加,样式难定制快速探索性分析
ggplot2geom_violin()图层灵活,主题丰富贝叶斯输出需手动整理频率派数据分析、论文图表
ggstatsplot自动带统计检验定制性受限,依赖多快速出统计报告
ggplot2+tidybayes贝叶斯友好,图层完全可控学习曲线稍陡贝叶斯后验可视化、复杂多层图

2.3 数据准备与格式要求

不管用什么方案,数据格式都是第一道坎。ggplot2要求长格式数据(long format),也就是每一行是一个观测,列包括分组变量和数值变量。如果你手头是宽格式(每个组一列),得先用tidyr::pivot_longer()转过来。

tidybayes对数据格式的要求更具体一些。从贝叶斯模型提取后验样本时,通常用tidybayes::spread_draws()或者tidybayes::gather_draws(),得到的数据框里会有.chain、.iteration、.draw这些列,以及模型参数的列。然后stat_halfeye()之类的函数会自动识别.value列或者你指定的列来画密度。

这里有个容易忽略的点:分组变量的因子水平顺序。ggplot2默认按字母顺序排列x轴,但很多时候你需要按中位数或者均值排序。解决办法是在画图前用forcats::fct_reorder()或者forcats::fct_relevel()重新设定因子水平。这个细节后面在实操部分会展开。

3. ggplot2小提琴图的核心参数与实操要点

3.1 geom_violin的关键参数逐个拆解

geom_violin()看起来简单,但里面的参数每一个都影响最终效果。我把最常用的几个列出来,配上实际调节的经验。

scale参数控制小提琴的宽度。默认是"area",意思是所有小提琴的面积相同,宽度根据密度曲线的最大值自动调整。另一个选项是"count",宽度与样本量成正比。还有一个"width",所有小提琴宽度固定。我个人的经验是:如果各组样本量差异很大,用"count"更诚实,读者一眼就能看出哪组数据少;如果样本量差不多,用"area"视觉上更整齐。

adjust参数是带宽的乘数因子,默认是1。设成0.5会让密度曲线更崎岖,设成2会更平滑。遇到双峰分布的时候,我通常会试几个值,找到能清晰展示两个峰但又不过度拟合的那个。这个没有固定公式,得靠眼睛判断。

trim参数决定小提琴两端是否截断到数据的实际范围。默认是TRUE,意思是密度曲线只画在最小值到最大值之间。设成FALSE的话,曲线会延伸到数据范围之外,有时候能展示出分布的"尾巴",但也可能画出没有数据支撑的区域。我一般保持默认的TRUE,除非特别需要展示尾部特征。

bw参数直接指定带宽值,会覆盖adjust的设置。这个一般不用手动设,除非你对数据的尺度非常了解。

quantile参数可以控制在小提琴内部画分位数线。比如quantile = c(0.25, 0.5, 0.75)会在小提琴上画出四分位数线。但这个功能用得不多,因为通常会用单独的图层来画。

draw_quantiles参数跟quantile配合使用,指定要画哪些分位数的线。不过说实话,这个内置功能画出来的线样式比较单一,我更喜欢用geom_boxplot()或者stat_summary()来叠加,控制力更强。

3.2 叠加图层:让小提琴图信息量翻倍

单独一个小提琴图往往不够。下面几种叠加方式是我常用的组合。

叠加箱线图是最经典的搭配。geom_boxplot(width = 0.1)可以把箱线图压窄,嵌在小提琴中间。注意width参数要调小,不然箱线图会盖住小提琴的密度形状。箱线图的颜色通常设成白色或者半透明,避免喧宾夺主。

叠加散点能展示原始数据点。geom_jitter()是最常用的,但要注意width和height参数控制抖动幅度。抖动太大会让点跑到小提琴外面,太小又看不出重叠。我一般设width = 0.1左右。如果数据点很多,可以用geom_point(position = position_jitterdodge())配合dodge参数,让不同组的点错开。

叠加均值点和误差棒适合展示汇总统计。stat_summary(fun = mean, geom = "point")画均值点,stat_summary(fun.data = mean_cl_normal, geom = "errorbar")画置信区间。这里有个细节:误差棒的宽度用width参数控制,通常设成0.2左右比较协调。

叠加显著性标记是论文图表的刚需。可以用ggsignif包,也可以用geom_segment()和geom_text()手动画。手动画的好处是完全可控,坏处是每组比较都要算坐标,比较繁琐。我一般先用ggsignif快速出图,如果样式不满意再手动调整。

3.3 配色、主题与分面技巧

配色方面,ggplot2默认的灰色系其实挺耐看,但如果是分组比较多的情况,建议用scale_fill_brewer()或者scale_fill_viridis_d()。viridis系列的好处是色盲友好,打印成黑白也能区分。如果分组只有两三个,手动指定颜色更直接:scale_fill_manual(values = c("#E69F00", "#56B4E9", "#009E73"))。

主题方面,theme_minimal()和theme_classic()是我最常用的两个。theme_minimal()干净利落,适合网页展示;theme_classic()带坐标轴线,适合学术论文。如果要进一步定制,theme()函数里的参数非常多,常用的有axis.text(坐标轴文字)、legend.position(图例位置)、panel.grid(网格线)。我通常会写一个自定义主题函数,把常用设置打包,避免每次画图都重复写一堆theme()。

分面(facet)是小提琴图展示多组数据时的利器。facet_wrap(~ group)可以按分组变量自动排列子图,facet_grid(rows ~ cols)可以同时按两个变量分面。分面的时候要注意y轴刻度是否统一。默认是统一的,方便比较;如果各组数据量级差异很大,可以用scales = "free_y"让y轴自由变化,但这样比较起来就不太直观了。我的建议是:如果目的是比较组间差异,统一y轴;如果目的是展示每组内部的分布形状,自由y轴。

4. tidybayes进阶:贝叶斯后验分布的小提琴图

4.1 tidybayes的核心函数与工作流

tidybayes的核心价值在于把贝叶斯模型的后验分布样本"tidy化",然后用ggplot2的语法来画。整个工作流大致是这样的:先用brms、rstanarm或者rstan拟合模型,然后用spread_draws()或者gather_draws()提取后验样本,最后用stat_halfeye()、stat_gradientinterval()等函数画图。

stat_halfeye()是我用得最多的一个。它画出来的图一半是小提琴(密度曲线),一半是区间(通常是中位数和分位数区间)。默认情况下,它会画出中位数点、50%分位数区间(粗线)和95%分位数区间(细线)。这个组合信息量非常大:密度形状告诉你后验分布的形态,区间告诉你参数的不确定性范围,中位数点告诉你点估计。

stat_gradientinterval()是另一个常用的,它画的是渐变填充的区间图,没有密度曲线。适合展示多个参数的后验区间对比,视觉上比stat_halfeye()更紧凑。

stat_eye()是stat_halfeye()的别名,功能一样,名字更短。stat_halfeye()的参数里,point_interval控制用哪种区间(默认是median_qi,即中位数和分位数区间),.width控制区间宽度(默认是0.66和0.95),.width控制密度曲线的宽度。

4.2 从模型输出到小提琴图的完整链路

假设你用brms拟合了一个简单的线性模型,有一个分组变量group和一个连续预测变量x。模型拟合完之后,提取后验样本的代码如下:

library(brms) library(tidybayes) library(ggplot2) # 假设模型已经拟合好了,叫fit # 提取分组截距的后验样本 posterior_samples <- fit %>% spread_draws(b_Intercept, r_group[group,]) %>% mutate(group_mean = b_Intercept + r_group)

这段代码的意思是:提取固定截距b_Intercept和分组随机截距r_group,然后相加得到每组的后验均值。spread_draws()会自动把结果整理成tidy格式,每一行是一个后验样本。

然后画图:

posterior_samples %>% ggplot(aes(x = group, y = group_mean, fill = group)) + stat_halfeye(.width = c(0.5, 0.95), point_interval = median_qi) + scale_fill_brewer(palette = "Set2") + labs(x = "分组", y = "后验均值") + theme_minimal()

这里stat_halfeye()的.width参数设成了c(0.5, 0.95),意思是同时画出50%和95%的分位数区间。point_interval = median_qi指定用中位数和分位数区间,这是贝叶斯分析里最常用的汇总方式。

4.3 贝叶斯小提琴图与频率派小提琴图的本质差异

这里得说清楚一个概念上的区别。频率派的小提琴图,密度曲线是基于观测数据的核密度估计,反映的是样本的分布形状。贝叶斯的小提琴图,密度曲线是基于后验样本的核密度估计,反映的是参数的不确定性分布。两者看起来都是小提琴,但含义完全不同。

频率派小提琴图的宽度变化,告诉你数据在不同取值附近的密集程度。贝叶斯小提琴图的宽度变化,告诉你参数在不同取值附近的概率密度。前者是描述性的,后者是推断性的。

这个区别在实际解读的时候非常重要。我见过有人把贝叶斯后验的小提琴图当成数据分布图来解读,那就完全跑偏了。后验分布的小提琴图,重点看的是区间宽度和中位数位置,而不是密度曲线的形状本身。

tidybayes还提供了一个很实用的功能:stat_pointinterval()。它只画区间和点,不画密度曲线。当你需要对比很多个参数的后验区间时,这个函数比stat_halfeye()更清爽。参数多了之后,密度曲线会显得很乱,只画区间反而更清晰。

5. 常见问题与排查技巧实录

5.1 小提琴图常见异常与解决方案

画小提琴图遇到问题太正常了。下面这张表整理了我踩过的坑和对应的解决办法:

问题现象可能原因解决方案
小提琴形状过于崎岖带宽太小增大adjust参数(如1.5-2)或手动设bw
小提琴形状过于平滑,看不出双峰带宽太大减小adjust参数(如0.5-0.8)
小提琴超出数据范围trim = FALSE设trim = TRUE或检查数据是否有异常值
各组小提琴宽度差异过大scale = "count"改用scale = "area"统一面积
叠加的箱线图盖住小提琴width参数太大减小geom_boxplot()的width(如0.1)
散点抖动后跑到小提琴外geom_jitter()的width太大减小width(如0.05-0.1)
分面后y轴刻度不统一默认scales = "fixed"设scales = "free_y"(但注意比较性)
贝叶斯后验图区间太宽先验太弱或数据太少检查先验设定,或增加采样量
stat_halfeye()报错找不到.value列数据格式不对检查spread_draws()的输出列名

5.2 性能优化与大数据量处理

数据量大的时候,小提琴图会变得很慢。主要瓶颈在核密度估计和渲染两个环节。核密度估计的复杂度跟样本量成正比,渲染的复杂度跟图形元素数量成正比。

优化策略有几个。第一,先聚合再画图。如果数据有几十万个点,没必要每个点都参与密度估计。可以先用dplyr::sample_n()随机抽样,或者按分组计算分位数后用分位数来近似密度。第二,减少不必要的图层。散点图层在大数据量下最耗性能,如果数据点超过几千个,考虑用geom_bin2d()或者geom_hex()替代。第三,用ggrastr包把散点图层栅格化。ggrastr::geom_point_rast()可以把点图层转成栅格图像,大幅减少渲染时间,而且输出PDF时文件大小也会小很多。

贝叶斯后验样本通常不会太大(几千到几万条),性能问题不突出。但如果用了spread_draws()提取了大量参数,数据框可能会变得很宽,这时候用gather_draws()或者只提取需要的参数会更高效。

5.3 图表可读性的几个关键决策

画图最终是为了沟通。可读性比技术正确性更重要。几个我反复验证过的原则:

颜色不要超过五种。超过五种颜色,读者就很难记住每种颜色对应哪个组了。如果分组确实多,考虑用分面代替颜色映射。

图例能省则省。如果x轴已经标了组名,图例就是冗余的。用theme(legend.position = "none")去掉,图面更干净。

坐标轴标签要完整。不要用缩写,不要用变量名直接当标签。labs(x = "实验组别", y = "测量值(单位:毫米)")这样写,读者不用猜。

标题和副标题要传达信息。不要写"小提琴图"这种废话标题。写"实验组B的测量值分布显著右偏"这种有信息量的标题,读者一眼就能抓住重点。

字体大小要够。论文图表的字体通常要求8-10pt,但如果是PPT展示,至少14pt起步。theme(text = element_text(size = 14))可以全局调整。

5.4 从探索性分析到出版级图表的迭代路径

我画图的习惯是分三步走。第一步是快速探索,用最简的代码快速出图,看看数据长什么样,有没有异常值,分布形状如何。这一步不追求美观,追求速度。第二步是结构调整,确定要展示哪些组、用哪种叠加方式、要不要分面。这一步会反复试几种方案,比较哪种信息传达最有效。第三步是精修美化,调整配色、字体、间距、图例位置,直到可以直接放进报告或者论文。

这个迭代路径的好处是,每一步的目标很明确,不会在探索阶段就纠结配色,也不会在精修阶段才发现结构不对要推倒重来。我见过很多人一上来就想画一张完美的图,结果在细节上耗了大量时间,最后发现整体结构就不对。

tidybayes的图也是同样的逻辑。先用stat_halfeye()快速看一下后验分布的大致形态,确定哪些参数值得重点展示,然后再精修。tidybayes的默认样式其实已经挺不错了,微调一下就能用。

6. 几个让我印象深刻的调试案例

说两个实际调试中遇到的案例,都是比较典型的。

第一个是关于密度曲线截断的。有一次画一组反应时数据的小提琴图,发现小提琴的尾部延伸到了负值区域。反应时不可能为负,这明显不对。检查后发现是trim = FALSE导致的,密度估计在数据范围之外仍然有非零值。改成trim = TRUE后问题解决。但这个案例让我意识到,密度估计本质上是一种平滑,它不知道数据的物理约束。如果数据有明确的边界(如时间、长度、比例),画图时一定要检查小提琴有没有越界。

第二个是关于贝叶斯后验区间宽度的。有一次用stat_halfeye()画后验分布,发现95%区间宽得离谱,几乎覆盖了整个参数空间。排查后发现是先验设得太弱(用了默认的平坦先验),而数据量又少,导致后验几乎完全由先验决定。后来换了一个信息性先验,区间就收窄到了合理范围。这个案例说明,贝叶斯小提琴图的区间宽度直接反映了先验和数据的博弈结果,看图的时候不能只看区间宽不宽,还要想想先验是怎么设的。

这两个案例的共同教训是:画图之前先理解数据,画图之后要检查合理性。小提琴图很漂亮,但漂亮不等于正确。密度曲线越界、区间异常宽、分组顺序不对,这些都是需要肉眼检查的。

7. 一些让图表更出彩的小技巧

最后分享几个我常用的"加分项",能让小提琴图从"能用"变成"好看"。

用ggbeeswarm包替代geom_jitter()。ggbeeswarm::geom_quasirandom()画出来的散点不会重叠,每个点都在自己的位置上,比随机抖动更整齐。数据量不大的时候效果特别好。

用gghalves包画半小提琴图。gghalves::geom_half_violin()可以只画一半小提琴,另一半画散点或者箱线图。这种"左半边右半边"的布局在展示两组对比的时候特别直观。

用patchwork包组合多张图。如果一张图放不下所有信息,用patchwork把多张小提琴图拼在一起,比用分面更灵活。patchwork的语法极其简单,p1 + p2 + p3就搞定了。

用ragg包提高渲染质量。ragg::agg_png()渲染出来的PNG比默认的png()设备清晰很多,特别是文字和线条的边缘。如果输出格式是PNG,强烈建议用ragg。

用showtext包解决中文字体问题。ggplot2默认不支持中文,画出来的中文会变成方框。showtext::showtext_auto()加上theme(text = element_text(family = "你的中文字体"))就能解决。这个坑几乎每个用R画中文图表的人都踩过。

用ggplot2的sec_axis()加副坐标轴。有时候需要在同一张图上展示两种量纲的数据,sec_axis()可以在右侧加一个副坐标轴,比画两张图更紧凑。

这些小技巧单独看都不复杂,但组合起来能让图表的专业度和美观度提升一个档次。我自己的习惯是维护一个"图表模板"脚本,把常用的主题设置、配色方案、字体配置都写好,每次画新图的时候直接source()进来,省去重复配置的时间。

说到底,小提琴图也好,贝叶斯后验图也好,工具只是手段,核心还是把数据里的故事讲清楚。ggplot2和tidybayes给了我们足够灵活的工具箱,剩下的就是多画、多看、多调。我刚开始用tidybayes的时候,stat_halfeye()的参数调了整整一个下午才满意,但现在回头看,那个下午花得值——后面每次画贝叶斯后验图,都是几分钟的事。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询