最近将使用的R版本升级到4.0+之后,遇到了一个以前从未遇到的报错,报错信息如下所示:
Error in UseMethod("rescale") : "rescale"没有适用于"AsIs"目标对象的方法
出现bug的代码是在使用clusterProfiler的cnetplot函数绘制一个富集分析图,正常情况下,它应该是出图如下:
出现问题的R对象是一个clusterProfiler的富集分析对象,已经把它打包到百度网盘,如果需要也可以下载尝试:
文件名:test.rds
链接:https://pan.baidu.com/s/1l2hqNw034OEBwVvfy7_01g
提取码:kezh
载入工具包:
library(tidyverse)
下载后,使用readRDS读入:
# 下载文件test.rds
# 将富集分析对象读入R
dat <- readRDS("test.rds")
class(dat)
#[1] "enrichResult"
#attr(,"package")
#[1] "DOSE"
dat是一个enrichResult类,可以使用clusterProfiler包的一系列绘图函数:
# 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看一下调用堆栈,初步定位一下问题在哪里。
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,并查看此代码运行时候的相关代码逻辑和数据情况。
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复原,比如如下示例:
# 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"
所以可以在出错代码前添加如下代码实现:
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源码如下:
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,
如下所示:
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方法,于是下述代码就会正常出图毫无问题了。
p <- dat %>% clusterProfiler::cnetplot()p
上述思路是具有普遍适用性的,在需要对ggplot2对象的泛型方法进行修改的场合,都可以构造一个自己的generic.gg方法来覆盖原来有bug的generic方法。
甚至是上述思路并不局限于ggplot2对象,因为S3类的class属性是可以编辑的,所以完全可以将原来的S3类对象定义成一个多了一个优先类名的S3类。
友情推荐