ggplot2中用stat_summary+geom_segment绘制密度图分位数报错问题
问题描述
想要在ggplot2密度图上绘制分位数线,要求线段止于密度曲线顶端,不使用geom_vline。尝试以下代码:
dat <- data.frame(x = rnorm(1e5)) ggplot( data = dat, aes(x = x) ) + geom_density() + stat_summary( aes(y = 0,yend = after_stat(density)), fun = \(x) quantile(x,0.5), geom = "segment", orientation = "y" )
但出现报错:
Error in `stat_summary()`: ! Problem while mapping stat to aesthetics. ℹ Error occurred in the 2nd layer. Caused by error in `map_statistic()`: ! Aesthetics must be valid computed stats. ✖ The following aesthetics are invalid: ✖ `yend = after_stat(density)` ℹ Did you map your stat in the wrong layer? Run `rlang::last_trace()` to see where the error occurred.
已知可以提前在ggplot外计算密度再手动绘制线段,也知道ggridges包有相关功能但依赖脊线图。困惑点在于:将yend = after_stat(density)替换为固定值yend = 1时代码可正常运行,为何无法调用计算变量density?希望找到用stat_summary和after_stat实现需求的正确方式。
原因分析
stat_summary() 对应的统计变换(StatSummary)核心是计算数据的摘要统计量(如分位数、均值),它的内置计算变量仅包含与摘要结果相关的字段(如y、y_min、y_max),不会生成density密度统计量。
调用after_stat(density)时,ggplot在当前stat_summary图层的计算变量里找不到该字段,因此报错;而使用固定值yend=1时,不需要依赖统计变换生成的变量,自然能正常运行。此外,不同图层的统计变量相互独立,geom_density生成的density变量无法直接被stat_summary图层调用。
解决方案
方法1:结合预计算密度与stat_summary
如果坚持使用stat_summary,需要先将密度值合并到原始数据中,让stat_summary能访问到该变量:
dat <- data.frame(x = rnorm(1e5)) # 预计算x对应的密度值并合并到数据框 dens <- density(dat$x) dat$density <- approx(dens$x, dens$y, xout = dat$x)$y ggplot(dat, aes(x = x)) + geom_density() + stat_summary( aes(y = 0, yend = after_stat(max(density))), fun = \(x) quantile(x, 0.5), # 此处计算中位数,可替换为其他分位数 geom = "segment", orientation = "y", color = "red", # 自定义线段颜色 size = 1 # 自定义线段粗细 )
这里after_stat(max(density))的作用是:针对分位数对应的分组(此处为全局单组),提取该分位数位置对应的最大密度值(即曲线顶端的密度)。
方法2:直接用stat_density实现更简洁
既然密度值由密度统计生成,直接用stat_density绘制线段会更高效,无需额外预处理数据:
dat <- data.frame(x = rnorm(1e5)) # 定义需要绘制的分位数 quantiles <- c(0.25, 0.5, 0.75) ggplot(dat, aes(x = x)) + geom_density() + stat_density( geom = "segment", aes( x = after_stat(x), xend = after_stat(x), y = 0, yend = after_stat(density) ), # 筛选出分位数对应的位置 data = ~ dplyr::filter(.x, x %in% quantile(x, quantiles)), color = "red", size = 1 )
注:如果分位数在密度曲线插值点上无法精准匹配,可使用approx函数手动计算分位数对应的密度值,再用annotate绘制线段。
内容的提问来源于stack exchange,提问作者joran

