在R包中定义自定义dplyr方法

And*_*rew 8 r r-s3 dplyr r-package tidyverse

我有一个自定义的包summary(),print()对于具有特定类对象的方法.这个软件包还使用了很棒dplyr的数据包进行操作 - 我希望我的用户能够编写同时使用我的软件包和dplyr的脚本.

其他人在这里这里注意到的一个障碍是dplyr动词不保留自定义类 - 这意味着ungroup命令可以剥离我的自定义类的data.frames,从而搞乱方法调度summary等.

Hadley说"正确执行此操作取决于您 - 您需要为每个dplyr方法定义一个方法,以便正确恢复所有类和属性"并且我正在尝试接受建议 - 但我无法弄清楚如何正确包装dplyr动词.

这是一个简单的玩具示例.假设我已经定义了一个cars类,我有一个自定义summary.

这很有效

library(tidyverse)

class(mtcars) <- c('cars', class(mtcars))

summary.cars <- function(x, ...) {
  #gather some summary stats
  df_dim <- dim(x)
  quantile_sum <- map(mtcars, quantile)

  cat("A cars object with:\n")
  cat(df_dim[[1]], 'rows and ', df_dim[[2]], 'columns.\n')

  print(quantile_sum)

}

summary(mtcars)
Run Code Online (Sandbox Code Playgroud)

这是问题所在

small_cars <- mtcars %>% filter(cyl < 6)
summary(small_cars)
class(small_cars)
Run Code Online (Sandbox Code Playgroud)

那个summary调用small_cars只给我一般的摘要,而不是我的自定义方法,因为在dplyr过滤后small_cars不再保留cars类.

我尝试了什么

首先,我尝试围绕filter(filter.cars)编写自定义方法.这不起作用,因为filter实际上包装filter_允许非标准评估.

所以我filter_cars对象编写了一个自定义方法,试图实现@jwdink的建议

filter_.cars <- function(df, ...) {

  old_classes <- class(df)
  out <- dplyr::filter_(df, ...)
  new_classes <- class(out)

  class(out) <- c(new_classes, old_classes) %>% unique()

  out
}
Run Code Online (Sandbox Code Playgroud)

这不起作用 - 我得到一个无限递归错误:

Error: evaluation nested too deeply: infinite recursion / options(expressions=)?
Error during wrapup: evaluation nested too deeply: infinite recursion / options(expressions=)?
Run Code Online (Sandbox Code Playgroud)

我想要做的就是获取传入的df上的类,移交给dplyr,然后返回与dplyr调用之前相同的类名的对象. 如何更改我的filter_包装器来实现这一目标? 谢谢!

Jon*_*oll 8

您的新filter_方法尝试应用于定义中的新类,因此递归.

根据您链接的问题中的建议,尝试filter_在更新的方法之前删除该新类.

class(out) <- class(out)[-1]
Run Code Online (Sandbox Code Playgroud)


jwd*_*ink 8

线程中提供了进一步的建议,所以我想我会用最好的做法更新,这是使用NextMethod().

filter_.cars <- function(.data, ...) {
   result <- NextMethod()
   reclass(.data, result)
}
Run Code Online (Sandbox Code Playgroud)

哪个reclass是通用的,至少会重新添加类:

reclass <- function(x, result) {
  UseMethod('reclass')
}

reclass.default <- function(x, result) {
  class(result) <- unique(c(class(x)[[1]], class(result)))
  result
}
Run Code Online (Sandbox Code Playgroud)

但是您可以为您的类定义一个自定义方法,该方法还可以复制属性:

reclass.cars <- function(x, result) {
  class(result) <- unique(c(class(x)[[1]], class(result)))
  attr(result,'cars') <- attr(x,'cars')
  result
}
Run Code Online (Sandbox Code Playgroud)

我实际上认为一个更好的默认方法只是假设有一个属性,其名称与类相同:

reclass.default <- function(x, result) {
  class(result) <- unique(c(class(x)[[1]], class(result)))
  attr(result, class(x)[[1]]) <- attr(x, class(x)[[1]])
  result
}
Run Code Online (Sandbox Code Playgroud)

请注意,对于dplyr 0.7,不推荐使用动词的下划线版本.如果你的'汽车'类继承自tbl_df,你需要为非下划线动词编写一个方法.但是,您可能希望保留下划线版本以实现向后兼容性.

鉴于所有这些复制,我有点像这里的副词的想法.

preservatively <- function(fun) {
  function(x, ...) {
    result <- NextMethod()
    reclass(x, result)
  }
}
Run Code Online (Sandbox Code Playgroud)

然后你的包装里的东西很简洁:

filter_.cars <- preservatively(filter_)
filter.cars <- preservatively(filter)
mutate_.cars <- preservatively(mutate_)
mutate.cars <- preservatively(mutate)
Run Code Online (Sandbox Code Playgroud)

等等


编辑:

不要用preservatively.如果有人用命名的第一个参数调用dplyr动词,它将会中断,因为名称通常.data不是x.

filter.cars <- preservatively(filter)
filter(my_data, condition) # good
filter(.data = my_data, condition) # oh no
Run Code Online (Sandbox Code Playgroud)

如果事实证明副词可以起作用,我会更新这个答案.否则,我想这真的不再冗长:

filter.cars <- function(.data, ...) reclass(.data, NextMethod())
Run Code Online (Sandbox Code Playgroud)