前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >R tips:debug并修复一个ggplot2绘图错误的例子

R tips:debug并修复一个ggplot2绘图错误的例子

作者头像
生信菜鸟团
发布2021-10-21 14:39:35
2.6K0
发布2021-10-21 14:39:35
举报
文章被收录于专栏:生信菜鸟团

最近将使用的R版本升级到4.0+之后,遇到了一个以前从未遇到的报错,报错信息如下所示:

代码语言:javascript
复制
Error in UseMethod("rescale") : "rescale"没有适用于"AsIs"目标对象的方法

出现bug的代码是在使用clusterProfiler的cnetplot函数绘制一个富集分析图,正常情况下,它应该是出图如下:

bug重现

出现问题的R对象是一个clusterProfiler的富集分析对象,已经把它打包到百度网盘,如果需要也可以下载尝试:

代码语言:javascript
复制
文件名:test.rds
链接:https://pan.baidu.com/s/1l2hqNw034OEBwVvfy7_01g 
提取码:kezh

载入工具包:

代码语言:javascript
复制
library(tidyverse)

下载后,使用readRDS读入:

代码语言:javascript
复制
# 下载文件test.rds
# 将富集分析对象读入R
dat <- readRDS("test.rds")
class(dat)
#[1] "enrichResult"
#attr(,"package")
#[1] "DOSE"

dat是一个enrichResult类,可以使用clusterProfiler包的一系列绘图函数:

代码语言:javascript
复制
# dotplot
clusterProfiler::dotplot(dat, showCategory = 20) + 
                        ggpubr::theme_classic2() + 
                        theme_bw() +
                        scale_y_discrete(labels = function(x, width = 60) str_wrap(x, width = width) )
# cnetplot                                         
dat %>% clusterProfiler::cnetplot() # < 报错 >
# emapplot                                         
dat %>% enrichplot::pairwise_termsim() %>% clusterProfiler::emapplot(., showCategory = 20)

定位问题

但是在绘制cnetplot的时候就会报错。出错之后,先traceback看一下调用堆栈,初步定位一下问题在哪里。

代码语言:javascript
复制
p <- dat %>% clusterProfiler::cnetplot()
p
#Error in UseMethod("rescale") : "rescale"没有适用于"AsIs"目标对象的方法

traceback()
#21: f(...)
#20: self$rescaler(x, from = range)
#19: f(..., self = self)
#18: self$rescale(self$oob(x, range = limits), limits)
#17: f(..., self = self)
#16: self$map(df[[j]])
#15: FUN(X[[i]], ...)
#14: lapply(aesthetics, function(j) self$map(df[[j]]))
#13: f(..., self = self)
#12: scale$map_df(df = df)
#11: FUN(X[[i]], ...)
#10: lapply(scales$scales, function(scale) scale$map_df(df = df))
#9: unlist(lapply(scales$scales, function(scale) scale$map_df(df = df)), recursive = FALSE)
#8: FUN(X[[i]], ...)
#7: lapply(data, scales_map_df, scales = npscales)
#6: ggplot_build.ggplot(x)
#5: NextMethod()
#4: ggplot_build.ggraph(x)
#3: ggplot_build(x)
#2: print.ggplot(x)
#1: (function (x, ...) UseMethod("print"))(x)

可以看到,ggplot2对象p在绘制图形时发生了错误:第一个调用方法是print泛型方法,然后方法分发到print.ggplot方法上(#2),再然后是调用ggplot_build泛型方法,并且最终方法分发到ggplot_build.ggplot方法上(#6)。

ggplot_build.ggplot方法是ggplot2包中的一个方法,call stack中可以发现是它的lapply(data, scales_map_df, scales = npscales)这条命令出错(#7)。

看一下这个函数的源码,也可以找到这条代码的所在位置。

这个时候可以对这个方法进行debug,并查看此代码运行时候的相关代码逻辑和数据情况。

代码语言:javascript
复制
debug(ggplot2:::ggplot_build.ggplot)
p 
# 此时会进入debug界面, 一路单步执行下去,直到要出错的代码时停止

data %>% glimpse() # 查看数据data的详情信息
#List of 5
# $ :'data.frame':    500 obs. of  5 variables:
#  ..$ PANEL: Factor w/ 1 level "1": 1 1 1 1 1 1 1 1 1 1 ...
#  ..$ x    : num [1:500] 1.56 1.55 1.53 1.52 1.51 ...
#  ..$ y    : num [1:500] -0.0388 -0.0364 -0.034 -0.0316 -0.0292 ...
#  ..$ index: num [1:500] 0 0.0101 0.0202 0.0303 0.0404 ...
#  ..$ group: chr [1:500] "1" "1" "1" "1" ...
# $ :'data.frame':    5 obs. of  6 variables:
#  ..$ size  : num [1:5] 1 1 1 1 1
#  ..$ x     : num [1:5] 1.561 0.846 -0.763 -1.041 0.395
#  ..$ y     : num [1:5] -0.0388 1.4197 1.1899 -0.4107 -1.1701
#  ..$ colour: 'AsIs' chr [1:5] "#E5C494" "#E5C494" "#E5C494" "#E5C494" ...
#  ..$ PANEL : Factor w/ 1 level "1": 1 1 1 1 1
#  ..$ group : int [1:5] 1 1 1 1 1
#  .. ..- attr(*, "n")= int 1
# $ :'data.frame':    1 obs. of  6 variables:
#  ..$ size  : 'AsIs' num 3  <-----------------------------------此处的数值3被AsIs对象包裹----------------------------------------->
#  ..$ x     : num 0.2
#  ..$ y     : num 0.198
#  ..$ colour: 'AsIs' chr "#B3B3B3"
#  ..$ PANEL : Factor w/ 1 level "1": 1
#  ..$ group : int 1
#  .. ..- attr(*, "n")= int 1
# $ :'data.frame':    1 obs. of  5 variables:
#  ..$ x    : num 0.2
#  ..$ y    : num 0.198
#  ..$ label: chr "1019"
#  ..$ PANEL: Factor w/ 1 level "1": 1
#  ..$ group: int -1
#  .. ..- attr(*, "n")= int 1
# $ :'data.frame':    5 obs. of  5 variables:
#  ..$ x    : num [1:5] 1.561 0.846 -0.763 -1.041 0.395
#  ..$ y    : num [1:5] -0.0388 1.4197 1.1899 -0.4107 -1.1701
#  ..$ label: chr [1:5] "RNA polymerase II CTD heptapeptide repeat kinase activity" "cyclin-dependent protein serine/threonine kinase activity" "cyclin-dependent protein kinase activity" "cyclin binding" ...
#  ..$ PANEL: Factor w/ 1 level "1": 1 1 1 1 1
#  ..$ group: int [1:5] -1 -1 -1 -1 -1
#  .. ..- attr(*, "n")= int 1

可以发现,data中的一个数值3被封装为AsIs对象了,而这基本上是出错的直接原因了。

如果是一步一步逐一检视data对象的话,可以知道它大概是在data <- lapply(data, scales_transform_df, scales = scales)这条命令处出现了AsIs的问题,当然这并不一定是出错的根本原因。

不过对于要修复这个问题来说,目前的信息已经足够了。

如何使用RStudio进行debug代码以及如何自由的查看R中的函数源码,在以前的R tips推文中都曾经说过。

解决方案

既然data这个数据有问题,可以在出错代码前,先将data中的AsIs对象给还原了,AsIs可以使用unclass复原,比如如下示例:

代码语言:javascript
复制
# I 数值 
I(1) # [1] 1
class(I(1)) # [1] "AsIs"
class(unclass(I(1))) # [1] "numeric"

# II 字符 
I('a') # [1] "a"
class(I('a')) # [1] "AsIs"
class(unclass(I('a'))) # [1] "character"

所以可以在出错代码前添加如下代码实现:

代码语言:javascript
复制
for(i in seq_along(data)){
  is_AsIs <- vapply(data[[i]], function(x) "AsIs" %in% class(x), FUN.VALUE = logical(1))
  if(sum(is_AsIs) > 0){
    col_with_bug <- which(is_AsIs)
    for(j in col_with_bug){
      data[[i]][[j]] <- unclass(data[[i]][[j]]) # 使用unclass将AsIs还原
    }
  }
}

现在只有一个问题,就是如何将这个fix bug代码添加到ggplot_build.ggplot函数中。由于这个函数的源码在ggplot2包中,现在是没办法直接修改这个函数的源码的,但是我们可以再创建一个ggplot_build.ggplot函数去覆盖原来的函数,只需要保证新建的ggplot_build.ggplot函数可以优先被调用就可以了。

先看一下正常情况下ggplot_build.ggplot函数的被调用细节,它是被print.ggplot调用的,print.ggplot源码如下:

代码语言:javascript
复制
ggplot2:::print.ggplot
#function (x, newpage = is.null(vp), vp = NULL, ...) 
#{
#    set_last_plot(x)
#    if (newpage) 
#        grid.newpage()
#    grDevices::recordGraphics(requireNamespace("ggplot2", 
#       quietly = TRUE), list(), getNamespace("ggplot2"))
#    data <- ggplot_build(x)
#    gtable <- ggplot_gtable(data)
#    if (is.null(vp)) {
#        grid.draw(gtable)
#    }
#    else {
#        if (is.character(vp)) 
#            seekViewport(vp)
#        else pushViewport(vp)
#        grid.draw(gtable)
#        upViewport()
#    }
#    if (isTRUE(getOption("BrailleR.VI")) && rlang::is_installed("BrailleR")) {
#        print(asNamespace("BrailleR")$VI(x))
#    }
#    invisible(x)
#}

调用代码是data <- ggplot_build(x),它是被直接调用的(第8行)。

前文中,我们已经知道ggplot_build是一个S3泛型方法,而此处的x其实是ggplot2对象。而且默认情况下,ggplot2对象的类名中有一个更优先的类名gg,

如下所示:

代码语言:javascript
复制
p2 <- iris %>% ggplot(aes(x = Sepal.Length, y = Sepal.Width)) + geom_point()
class(p2)
#[1] "gg"     "ggplot"

于是我们就可以利用S3类方法的分发机制,创建一个ggplot_build.gg方法,这样就可以保证我们创建的方法一定是优先调用的:

这个函数的源码直接复制于ggplot2:::ggplot_build.ggplot源码,并添加了上述所说的fix bug代码,由于是自己创建的函数,需要将原本属于ggplot2的函数全部改成绝对引用。

注意,只是导入ggplot2包是没有用的,因为不是所有的方法都是ggplot2包的导出方法,所以绝对引用也必须是三个“:”的绝对引用。

执行了ggplot_build.gg的定义代码后,现在的绘图代码在需要调用ggplot_build方法时会被自动分发到自定义创建的ggplot_build.gg方法,于是下述代码就会正常出图毫无问题了。

代码语言:javascript
复制
p <- dat %>% clusterProfiler::cnetplot()p

上述思路是具有普遍适用性的,在需要对ggplot2对象的泛型方法进行修改的场合,都可以构造一个自己的generic.gg方法来覆盖原来有bug的generic方法。

甚至是上述思路并不局限于ggplot2对象,因为S3类的class属性是可以编辑的,所以完全可以将原来的S3类对象定义成一个多了一个优先类名的S3类。

友情推荐

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2021-10-19,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 生信菜鸟团 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • bug重现
  • 定位问题
  • 解决方案
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档