前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >「R」ggplot2在R包开发中的使用

「R」ggplot2在R包开发中的使用

作者头像
王诗翔呀
发布2022-03-30 08:47:54
6.6K0
发布2022-03-30 08:47:54
举报
文章被收录于专栏:优雅R

没有特别系统的学习 tidy evaluation 这方面的高级操作,最近有空准备补一补,学习下这方面的知识。

原英文:https://github.com/tidyverse/ggplot2/blob/HEAD/vignettes/ggplot2-in-packages.Rmd

这篇文章是为在包代码中使用ggplot2的包开发人员准备的。在撰写本文时,ggplot2涉及在CRAN上的超过2,000个包和其他地方的更多包!在包中使用ggplot2编程增加了几个约束,特别是如果你想将包提交给CRAN。尤其是在R包中编程改变了从ggplot2引用函数的方式,以及在aes()vars()中使用ggplot2的非标准求值的方式。

引用ggplot2函数

与引用其他包类似,你需要在DESCRIPTION文件下的Imports条目下列出ggplot2,并使用::访问ggplot2提供的函数。(例如, ggplot2::function_name):

代码语言:javascript
复制
mpg_drv_summary <- function() {
  ggplot2::ggplot(ggplot2::mpg) + 
    ggplot2::geom_bar(ggplot2::aes(x = .data$drv)) + 
    ggplot2::coord_flip()
}

如果你经常使用ggplot2,你可能希望将ggplot2的多个函数写入NAMESPACE文件。如果你使用roxygen2[1],那么你可以利用注释代码块 #' @importFrom ggplot2 <one or more object names> (注意,这对数据集mpg不起作用)。

代码语言:javascript
复制
#' @importFrom ggplot2 ggplot aes geom_bar coord_flip
mpg_drv_summary <- function() {
  ggplot(ggplot2::mpg) + 
    geom_bar(aes(x = drv)) + 
    coord_flip()
}

即使你的包里使用了很多的ggplot2函数,将ggplot2列入Depends条目或者将它整个导入NAMESPACE(例如,通过#' @import ggplot2)都是不明智的。将ggplot2列入Depends会让你的包在被加载/测试的同时加载ggplot2。这会让其他想要使用你包的人通过::使用你的函数而无需加载它。同样地,导入ggplot2全部450个导出对象到你的命名空间会让分离你的包和ggplot2包的责任变得困难,特别是读者会搞不清这些函数到底来自哪里。

我个人碰到过很多这种情况。有时候在开发R包时为了保证正常运行,不得不将依赖包列入Depdens。实际上,如上所说,这一方面会让使用者懵逼,另一方面会造成开发病毒式感染,既不方便调试错误, 想要使用你的包开发的人又不得不将你的包列入Depends

在包函数中使用 aes()vars()

为了使用ggplot2创建图形,你很可能至少要使用一次aes()函数。如果你的图形使用了分面操作,你可能也会使用vars()用来指向绘图数据。而这两个函数都使用了非标准计算,如果你在包中直接使用它,后面再CMD check的使用会引入一个note。

所有的Error, warning和note都需要解决才能上传到CRAN。

代码语言:javascript
复制
mpg_drv_summary <- function() {
  ggplot(ggplot2::mpg) + 
    geom_bar(aes(x = drv)) + 
    coord_flip()
}
N  checking R code for possible problems (2.7s)
   mpg_drv_summary: no visible binding for global variable ‘drv’
   Undefined global functions or variables:
     drv

这大体又分为3种情况:

  • 你事先已经知道了列名和表达式。
  • 你用字符串向量来表示列名。
  • 由用户指定列名和表达式,而你想要你的函数能够有aes()同样的方式执行非标准计算。

如果你已经像上面的例子一样事先知道了列名,你可以使用来自rlang[2]的代词.data指代你要使用的图层数据。为了避免CMD check抛出note,你需要#' @importFrom rlang .data注释块引入该符号名。(通常你可以通过usethis::use_package_doc()生成包的注释块)

代码语言:javascript
复制
mpg_drv_summary <- function() {
  ggplot(ggplot2::mpg) + 
    geom_bar(aes(x = .data$drv)) + 
    coord_flip()
}

如果你的列名是字符串向量(例如, col = "drv"),使用 .data[[col]] 这种方式:

代码语言:javascript
复制
col_summary <- function(df, col) {
  ggplot(df) + 
    geom_bar(aes(x = .data[[col]])) + 
    coord_flip()
}
col_summary(mpg, "drv")

如果列名或者表达式是由用户提供的,你可以以 {{ col }}的方式将其传入aes()vars()。这种tidy eval计算符号会捕捉用户提供的表达式,并将其传递给使用非标准计算的函数,如aes()vars()

代码语言:javascript
复制
col_summary <- function(df, col) {
  ggplot(df) + 
    geom_bar(aes(x = {{ col }})) + 
    coord_flip()
}
col_summary(mpg, drv)

你可能看到了其他的一些方式可以达到相同的目的,但我们(ggplot2的作者)只会保证上述的用法在未来也是有效的。特别的,不要使用aes_()aes_string(),它们已经过时了,未来的版本中将不再支持。

这里有一些删减,没有特别的意义。

常规任务最佳实践

使用ggplot2可视化一个对象

ggplot2在包中通常用于可视化对象(例如,在一个plot()-风格的函数中)。例如,一个包可能定义了 如下一个S3类用于表达式不同离散值的概率:

代码语言:javascript
复制
mpg_drv_dist <- structure(
  c(
    "4" = 103 / 234,
    "f" = 106 / 234,
    "r" = 25 / 234
  ),
  class = "discrete_distr"
)

R中需要的类都有plot()方法,但想要依赖一个单一的plot()为你的每个用户都提供他们所需要的可视化需求是不现实的。然而,提供一个 plot()用于一个对象的可视化总结帮助用户理解该对象是有帮助的。为了满足你的所有用户,我们建议写一个函数将这个对象转换为一个数据框(如果更加复杂,可以是包含数据框的列表)。一个很好的例子是ggdendro[3],它创建系统树图但同时计算出数据以方便用户干自己想要做的事情。对于上面的例子,函数可能是这样的:

代码语言:javascript
复制
discrete_distr_data <- function(x) {
  tibble::tibble(
    value = names(x),
    probability = as.numeric(x)
  )
}
discrete_distr_data(mpg_drv_dist)
#> # A tibble: 3 x 2
#>   value probability
#>   <chr>       <dbl>
#> 1 4           0.440
#> 2 f           0.453
#> 3 r           0.107

通常,plot()的使用者调用它是为了它的副作用:它生成一个图形用于展示。这与ggplot()不同,除非交互使用或者显式地调用print(),否则是不是展示的。因为这个原因,ggplot2定义了一个自己的泛型函数autoplot(),调用它会返回一个ggplot()

代码语言:javascript
复制
#' @importFrom ggplot2 autoplot
autoplot.discrete_distr <- function(object, ...) {
  plot_data <- discrete_distr_data(object)
  ggplot(plot_data, aes(.data$value, .data$probability)) +
    geom_col() +
    coord_flip() +
    labs(x = "Value", y = "Probability")
}

一旦定义了 autoplot(),可以接着创建一个plot()方法包含(打印)绘图步骤:

代码语言:javascript
复制
#' @importFrom graphics plot
plot.discrete_distr <- function(x, ...) {
  print(autoplot(x, ...))
}

如果你不懂S3类,实现像plot()或者autoplot()这种泛型是一个不好的实践,因为这限制了包开发者自己控制S3用于实现自己的方法。不应该停止你创建自己的函数可视化对象!

创建一个新的主题

当创建一个新的主题时,从已有主题出发总是好的实践(例如,theme_grey()),然后使用%+replace%替换需要该包的元素。这是一种好的策略,哪怕几乎所有的元素都要替换,如果不这样做会让我们通过添加元素优化主题变得困难。ggthemes[4]包中有很多好的主题作为参考。

代码语言:javascript
复制
#' @importFrom ggplot2 %+replace%
theme_custom <- function(...) {
  theme_grey(...) %+replace% 
    theme(
      panel.border = element_rect(size = 1, fill = NA),
      panel.background = element_blank(),
      panel.grid = element_line(colour = "grey80")
    )
}
mpg_drv_summary() + theme_custom()

在加载包之后计算主题是很重要的。如果没有,则会将主题对象存储在编译后的包的字节码中,而该字节码可能与安装的ggplot2不一致!如果你的包有一个默认的可视化主题,正确的加载方法是使用一个返回默认主题的函数:

代码语言:javascript
复制
default_theme <- function() {
  theme_custom()
}
mpg_drv_summary2 <- function() {
  mpg_drv_summary() + default_theme()
}

测试ggplot2输出

我们建议使用vdiffr[5]测试ggplot2的输出,这是一个管理可视化测试案例的工具(这是我们测试ggplot2的方式之一)。如果ggplot2或者你代码的改变对可视化输出引入了改变,当你在本地或者Travis运行测试时会失败。为了使用vdiffr,你需要将testthat[6](通过usethis::use_testthat()初始化)和vdiffr加入DESCRIPTIONSuggests条目。然后,使用 vdiffr::expect_doppleganger(<name of plot>, <ggplot object>)创建一个测试。

代码语言:javascript
复制
test_that("output of ggplot() is stable", {
  vdiffr::expect_doppelganger("A blank plot", ggplot())
})

ggplot2在Suggests

如果你在包中使用ggplot2,大概率你会想要将它列入Imports。如果你想要将它列入Suggests,那么你不能使用#' @importFrom ggplot2 ...载入函数,但是如果你仍然想要使用ggplot2的像%+replace%这样的中缀操作符号,你可以在函数中进行赋值,然后再使用。

代码语言:javascript
复制
theme_custom <- function(...) {
  `%+replace%` <- ggplot2::`%+replace%`
  
  ggplot2::theme_grey(...) %+replace% 
    ggplot2::theme(panel.background = ggplot2::element_blank())
}

通过,如果你为ggplot2的autoplot()等泛型创建了新的方法,ggplot2应该列入Imports。如果出于一些原因你想要将其保留在Suggests,那么可以利用vctrs::s3_register()仅当ggplot2被安装时才注册你的泛型函数。为了达到这样的目的,你需要拷贝和粘贴vctrs::s3_register()的源代码,以避免引入vctrs[7]作为依赖。

代码语言:javascript
复制
.onLoad <- function(...) {
  if (requireNamespace("ggplot2", quietly = TRUE)) {
    vctrs::s3_register("ggplot2::autoplot", "discrete_distr")
  }
}

参考资料

[1]roxygen2: https://cran.r-project.org/package=roxygen2

[2]rlang: https://rlang.r-lib.org/

[3]ggdendro: https://cran.r-project.org/package=ggdendro

[4]ggthemes: https://cran.r-project.org/package=ggthemes

[5]vdiffr: https://cran.r-project.org/package=vdiffr

[6]testthat: https://testthat.r-lib.org/

[7]vctrs: https://vctrs.r-lib.org/

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

本文分享自 优雅R 微信公众号,前往查看

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

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

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 引用ggplot2函数
  • 在包函数中使用 aes() 和 vars()
  • 常规任务最佳实践
    • 使用ggplot2可视化一个对象
      • 创建一个新的主题
        • 测试ggplot2输出
          • ggplot2在Suggests
            • 参考资料
            相关产品与服务
            对象存储
            对象存储(Cloud Object Storage,COS)是由腾讯云推出的无目录层次结构、无数据格式限制,可容纳海量数据且支持 HTTP/HTTPS 协议访问的分布式存储服务。腾讯云 COS 的存储桶空间无容量上限,无需分区管理,适用于 CDN 数据分发、数据万象处理或大数据计算与分析的数据湖等多种场景。
            领券
            问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档