You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

ggplot2 geom_flat_violin雨云图分布曲线显示异常问题咨询

geom_flat_violin()绘制雨云图密度曲线异常收缩问题

问题现象

使用R语言开展数据可视化绘制雨云图(raincloud plot)时,发现geom_flat_violin()生成的密度曲线存在异常表现:

  • density密度曲线在部分场景下对数据values取值异常敏感,分布曲线形状和数据取值存在本不应出现的关联,暂未找到让二者恢复独立的方法
  • 已排查到明确规律:曲线会依据数据中的最低值出现收缩,且收缩效应会作用于低值所在的整个分面面板,而非仅影响包含该低值的子分组

前置说明

  • 雨云图官方包未收录至CRAN,测试时分别验证了作者公开的源码、第三方开源的geom_flat_violin实现版本,两个版本均可稳定复现问题
  • 曾尝试ggridges::geom_density_ridges()、{ggdist}等其他可绘制雨云图的方案,但这些方案默认对平滑参数、图形形状的控制灵活度不足,无法满足定制化绘图需求

可复现代码

library(reshape2)
library(ggplot2)
source("https://gist.githubusercontent.com/benmarwick/2a1bb0133ff568cbe28d/raw/fb53bd97121f7f9ce947837ef1a4c65a73bffb3f/geom_flat_violin.R")

# 加载数据并转换为长格式
data(iris)
miris <- melt(iris,id.vars = "Species", measure.vars = colnames(iris)[1:4], variable.name = "measurement")

## 1. 直接使用原始数据绘图,4个分面面板中有2个的曲线出现水平方向的"压扁"收缩
ggplot(miris, aes(x = Species, y = value, fill = Species)) +
    geom_flat_violin(position = position_nudge(x = .15, y = 0)) +
    facet_wrap(~measurement) 

## 2. 调整最小值所在分组的取值后,对应分面的所有曲线都恢复正常(调整其他分组无法解决问题)
airis <- miris
# 提取需要调整的数据索引
inds <- intersect(which(airis$Species == "setosa"), which(airis$measurement == "Petal.Width"))
# 给该组赋值更大的数值
airis$value[inds] <- rnorm(length(inds), 3, 0.5)

ggplot(airis, aes(x = Species, y = value, fill = Species)) +
    geom_flat_violin(position = position_nudge(x = .15, y = 0)) +
    facet_wrap(~measurement) 

## 第二张图中"Petal.Width"分面下所有物种的分布曲线都被放大,但实际仅修改了setosa组的Petal.Width取值

效果对比

雨云图异常效果对比图

图中左侧为原始数据直接绘图效果,右侧为调整小值后的绘图效果,可直观看到仅修改setosa组Petal.Width的取值后,Petal.Width分面下所有物种的分布曲线都出现了放大。


问题原因

这个异常的核心是第三方开源版geom_flat_violin()的密度缩放逻辑作用范围错误:

  • 原生ggplot2的geom_violin()会为每个分组单独计算核密度估计,再将每个分组内的密度值归一化到0-1区间映射小提琴宽度,因此不同分组的分布形状不会互相干扰
  • 广泛流传的第三方geom_flat_violin源码为了实现单侧扁平的视觉效果,改写了密度绘制逻辑,但遗漏了分组归一化步骤:它会先计算当前分面面板下所有数据的密度最大值,再用这个全局最大值统一缩放该分面下所有分组的曲线宽度
  • 当分面内存在一组取值范围极窄、密度峰值极高的数据时(比如案例中setosa的Petal.Width取值集中在0.1-0.6的窄区间),全局密度最大值会被拉得非常高,其他取值范围更宽、密度峰值更低的分组,曲线宽度就会被整体压缩得很窄,呈现出"压扁收缩"的视觉效果。这也是为什么仅修改最小值所在组的取值,整个分面的所有曲线都会变宽——本质是全局密度最大值被降低了。

可行修复方案

方案1:修改geom_flat_violin源码,添加分组内宽度归一化(最推荐)

找到源码中核密度计算完成、生成绘图数据的段落,在密度值density列计算完成后,添加按分面+分组维度的归一化逻辑,再用归一化后的数值映射宽度:

# 在密度计算代码后新增如下步骤
data <- data |>
  dplyr::group_by(PANEL, group) |>
  dplyr::mutate(scaled_density = density / max(density)) |>
  dplyr::ungroup()
# 将后续绘制逻辑中原来映射宽度的density字段替换为scaled_density

修改后每个分组的扁平小提琴最大宽度会统一,不会再出现同个分面下部分分组曲线被压扁的问题,同时可以完整保留对平滑带宽、轮廓形状等所有参数的自定义控制权。

方案2:调用函数时手动指定参数规避全局缩放

如果不想修改源码,可以在调用geom_flat_violin()时传入固定带宽参数,同时开启分组宽度归一化(部分高版本源码支持该参数):

ggplot(miris, aes(x = Species, y = value, fill = Species)) +
  geom_flat_violin(
    position = position_nudge(x = .15, y = 0),
    bw = 0.2, # 根据自身数据的取值范围调整固定带宽,避免窄分布组密度峰值异常偏高
    scale = "width" # 强制按分组归一化宽度
  ) +
  facet_wrap(~measurement)

方案3:换用维护更完善的ggdist包实现相同效果

ggdist包目前处于活跃维护状态,其中的stat_slab()几何对象可以完全替代geom_flat_violin()的单侧扁平小提琴效果,默认按分组归一化宽度,且支持所有平滑参数、形状参数的自定义调整:

library(ggdist)
ggplot(miris, aes(x = Species, y = value, fill = Species)) +
  stat_slab(
    position = position_nudge(x = .15, y = 0),
    side = "left", # 设置为单侧绘制,和flat_violin视觉效果完全一致
    normalize = "groups", # 按分组归一化宽度
    adjust = 1 # 平滑度调整参数,用法和geom_density的adjust参数完全一致
  ) +
  facet_wrap(~measurement)

内容的提问来源于stack exchange,提问作者MARO

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.09.02 08:09:42