【问题标题】:create new variable defined only for a subset of the data using `dplyr`使用`dplyr`创建仅为数据子集定义的新变量
【发布时间】:2016-04-03 22:42:17
【问题描述】:

考虑这个示例数据:

set.seed(1234567)
mydf <- data.frame(var1 = runif(10), var2 = c(runif(5), rep(NA, 5)))

不幸的是,这个EXAMPLE向量化函数会在其中一个参数为NA时触发错误

myfn <- function(x, y){
    sum(x:y)
}
myfn <- Vectorize(myfn)

现在,在dplyr 链的中间,我需要使用myfn 创建一个新变量。这个新变量 (var3) 仅在 var1var2 不是 NA 时定义。

所以对于类似情况最常见的解决方案是使用ifelse。像这样。

mydf %>%
    mutate(var3 = ifelse(
        test = is.na(var2), 
        yes = NA, 
        no = myfn(var1, var2)))

但这在我的情况下不起作用,因为无论如何ifelse 实际上将整个向量var1var2 传递给myfn 而不仅仅是当testFALSE 时的子向量。这一切都会中断,因为myfn 会在收到NA 时中断。

那么,聪明的dplyr 解决方案是什么? (我可以在不使用dplyr 的情况下为此想到许多解决方案,但我只对dplyr 友好的解决方案感兴趣)

我突然想到 filter 可以提供帮助,并且确实可以使用非常易读的 dplyry 代码

mydf %>%
    filter(!is.na(var2)) %>%
    mutate(var3 = myfn(var1, var2))

        var1       var2       var3
1 0.56226084 0.62588794 0.56226084
2 0.72649850 0.24145251 0.72649850
3 0.91524985 0.03768974 0.91524985
4 0.02969437 0.51659297 0.02969437
5 0.76750970 0.81845788 0.76750970

但是我必须将其保存在一个临时对象中,然后在原始数据中创建 var3 全部使用 NA 并将所有内容重新放在同一数据中(因为据我所知 @987654351 @ 有些人有 suggested 不存在,...,但)。

所以只是为了说明我想要的输出,这段代码产生了它(根本不使用dplyr):

mydf$var3 <- NA
index <- !is.na(mydf$var2)
mydf$var3[index] <- myfn(mydf$var1[index], mydf$var2[index])
mydf

> mydf
         var1       var2       var3
1  0.56226084 0.62588794 0.56226084
2  0.72649850 0.24145251 0.72649850
3  0.91524985 0.03768974 0.91524985
4  0.02969437 0.51659297 0.02969437
5  0.76750970 0.81845788 0.76750970
6  0.48005398         NA         NA
7  0.08837960         NA         NA
8  0.86294587         NA         NA
9  0.49660306         NA         NA
10 0.85350403         NA         NA

编辑:

我接受了@krlmlr 的解决方案,因为它正是我所寻找的:清晰、易读且简洁的代码,可轻松集成到dplyr 链中。对于我的示例,此解决方案如下所示。

mydf %>%
        rowwise %>%
        mutate(var3 = if(is.na(var2)) NA else myfn(var1, var2))

但是,正如@krlmlr 在他的回答中指出的那样,逐行操作在性能方面是有代价的。对于小型数据集或单次操作,它可能并不重要,但对于较大的数据集或重复操作数百万次,它可能是相当可观的。为了说明,这里是使用microbenchmark 和三个解决方案(base、dyplr 和 data.table)的比较,它们应用于一个更大的数据集(不是海量或其他任何东西,只有 1000 行而不是我原来的示例中的 10 行)。

library(data.table)
library(dplyr)

set.seed(1234567)
mydf <- data.frame(var1 = runif(1000), var2 = c(runif(500), rep(NA, 500)))

myfn <- function(x, y){
    sum(x:y)
}
myfn <- Vectorize(myfn)

using_base <- function(){
    mydf$var3 <- NA
    index <- !is.na(mydf$var2)
    mydf$var3[index] <- myfn(mydf$var1[index], mydf$var2[index])
}

using_dplyr <- function(){
    mydf <- mydf %>%
        rowwise %>%
        mutate(var3 = if(is.na(var2)) NA else myfn(var1, var2))
}

using_datatable <- function(){
    setDT(mydf)[!is.na(var2), var3 := myfn(var1, var2)]
}

library(microbenchmark)
mbm <- microbenchmark(
    using_base(), using_dplyr(), using_datatable(), 
    times = 1000)

library(ggplot2)
autoplot(mbm)

如您所见,使用rowwisedplyr 解决方案比其basedata.table 的竞争对手慢得多。

【问题讨论】:

  • 您的函数只是将非NA 值从var1 复制到var3,这是有意的吗?
  • 这是一个示例函数。那不是我的实际功能。这只是一个示例,在这里提供一个简短的可重现代码来说明问题
  • 如何修复你的函数,使其在收到 NA 时不会中断?

标签: r dplyr


【解决方案1】:

您或许可以考虑使用data.table,因为dplyr 目前不支持in-place mutation,而这正是您所寻找的。​​p>

library(data.table)
setDT(mydf)[!is.na(var2), var3 := myfn(var1, var2)]
#        var1       var2       var3
# 1: 0.56226084 0.62588794 0.56226084
# 2: 0.72649850 0.24145251 0.72649850
# 3: 0.91524985 0.03768974 0.91524985
# 4: 0.02969437 0.51659297 0.02969437
# 5: 0.76750970 0.81845788 0.76750970
# 6: 0.48005398         NA         NA
# 7: 0.08837960         NA         NA
# 8: 0.86294587         NA         NA
# 9: 0.49660306         NA         NA
#10: 0.85350403         NA         NA

【讨论】:

  • 感谢@mtoto,是的,这种部分替换是data.table 的一个非常好的功能。我只是在使用dplyr 寻找类似的东西,因为尽管我是data.table 性能的粉丝,但并不太关心它的语法(有点晦涩难懂,......,不过在这种情况下不是;对于这种特殊情况,代码也很清楚,但在某些情况下,基于data.table 的解决方案的代码很难阅读)
【解决方案2】:

如果您的原始函数未矢量化并且无法处理某些输入,则使用 Vectorize() 对其进行矢量化不会带来性能优势。而是使用dplyr::rowwise()逐行操作:

iris %>%
  rowwise %>%
  mutate(x = if (Sepal.Length < 5) 1 else NA) %>%
  ungroup

请注意,在这里使用if 是非常安全的,因为输入的长度为 1。

【讨论】:

  • 谢谢,rowwise 是个好主意。但我只看到它与do 一起使用。我会尽力让你知道情况如何。 ...同时,我只是复制粘贴了您的代码以查看输出,但它引发了此错误“错误:不兼容的类型,需要一个数字向量”,...,我还没有调查那里发生了什么(稍后我会这样做),但是如果您在阅读此内容之前很高兴听到可能导致错误的原因
  • 为避免该错误,请使用NA_real_ 而不是NA
  • @elikesprogramming:我使用的是dplyr的开发版,这里不会出现错误。否则,NA_real_ 是一个安全的选择。
  • 是的,开发版修复了这个问题,...,谢谢,您的解决方案在我看来非常 dyplr-y(最重要的是,清晰简洁的代码)并且效果很好(在开发版dyplr)
【解决方案3】:

您可以在 dplyr-pipes 中使用以下两个选项:

a) 带有临时变量

mutate(mydf, temp = !(is.na(var1) | is.na(var2)),
       var3 = replace(NA, temp, myfn(var1[temp], var2[temp])),
       temp = NULL)
#         var1       var2       var3
#1  0.56226084 0.62588794 0.56226084
#2  0.72649850 0.24145251 0.72649850
#3  0.91524985 0.03768974 0.91524985
#4  0.02969437 0.51659297 0.02969437
#5  0.76750970 0.81845788 0.76750970
#6  0.48005398         NA         NA
#7  0.08837960         NA         NA
#8  0.86294587         NA         NA
#9  0.49660306         NA         NA
#10 0.85350403         NA         NA

b) 带有包装函数(不改变原来的myfn):

myfn2 <- function(x, y) {
  i <- !(is.na(x) | is.na(y))
  res <- rep(NA, length(x))
  res[i] <- myfn(x[i], y[i])
  res
}

mutate(mydf, var3 = myfn2(var1, var2))
#         var1       var2       var3
#1  0.56226084 0.62588794 0.56226084
#2  0.72649850 0.24145251 0.72649850
#3  0.91524985 0.03768974 0.91524985
#4  0.02969437 0.51659297 0.02969437
#5  0.76750970 0.81845788 0.76750970
#6  0.48005398         NA         NA
#7  0.08837960         NA         NA
#8  0.86294587         NA         NA
#9  0.49660306         NA         NA
#10 0.85350403         NA         NA

【讨论】:

    【解决方案4】:

    您可以在完整的行上运行该函数,然后使用NA 绑定回这些行(尽管这比if...else 方法更迂回):

    mydf %>% filter(complete.cases(.)) %>% 
      mutate(var3 = myfn(var1, var2)) %>%
      bind_rows(mydf %>% filter(!complete.cases(.)))
    
             var1       var2       var3
            (dbl)      (dbl)      (dbl)
    1  0.56226084 0.62588794 0.56226084
    2  0.72649850 0.24145251 0.72649850
    3  0.91524985 0.03768974 0.91524985
    4  0.02969437 0.51659297 0.02969437
    5  0.76750970 0.81845788 0.76750970
    6  0.48005398         NA         NA
    7  0.08837960         NA         NA
    8  0.86294587         NA         NA
    9  0.49660306         NA         NA
    10 0.85350403         NA         NA
    

    【讨论】:

    • 这也很昂贵,因为不相关的列也被撕开并无缘无故地粘在一起:-)
    【解决方案5】:

    这是采用pythonic style 请求宽恕而不是请求许可的好案例。

    您可以使用 tryCatch 解决此问题,并完全避免条件测试:

    myfn <- function(x, y){ 
      tryCatch(sum(x:y), error = function(e) NA)
    }
    

    然后

    myfn <- Vectorize(myfn)
    mydf %>%
        mutate(var3 = myfn(var1, var2))
    

    给出想要的结果

             var1       var2       var3
    1  0.56226084 0.62588794 0.56226084
    2  0.72649850 0.24145251 0.72649850
    3  0.91524985 0.03768974 0.91524985
    4  0.02969437 0.51659297 0.02969437
    5  0.76750970 0.81845788 0.76750970
    6  0.48005398         NA         NA
    7  0.08837960         NA         NA
    8  0.86294587         NA         NA
    9  0.49660306         NA         NA
    10 0.85350403         NA         NA
    

    附录

    当然,最好只在正确类型的错误上传递 NA,即

    > tryCatch(sum(NA:NA), error = function(e) print(str(e)))
    List of 2
     $ message: chr "NA/NaN argument"
     $ call   : language NA:NA
     - attr(*, "class")= chr [1:3] "simpleError" "error" "condition"
    NULL
    

    【讨论】:

    • 感谢@jaimedash 使用tryCatch 的好主意,虽然我会在dplyr 链中而不是在函数中这样做(部分是因为函数不像我发布的这个例子那么简单,虽然我可以编写一个包装函数来尝试捕获错误,但我不喜欢这种包装)
    • 避免重写函数是有意义的。将tryCatch 作为内联包装器直接放入链中的想法似乎很酷,但是当我尝试例如mydf %&gt;% mutate(var3 = tryCatch(myfn(var1, var2), error = function(e) NA)) 时,var3 完全不适用。如何让它发挥作用? (PS rowwise 也无济于事)
    • 我没试过,但我认为内联 tryCatchrowwise 可以工作。如果没有rowwise,我想它不应该工作,因为,再次,整个向量被传递给函数,tryCatch 将得到一个错误并给你 NA 回来。在任何情况下,rowwise 解决方案肯定可以使用do 而不是mutate(这最后一个可能只适用于dplyr 的开发版本?)。在下面他自己的答案中查看@Psidom 的评论,他在那里提供的代码有效。
    • +1 谢谢!是的,我看到 mutate + rowwise 在 0.4.3 中被破坏,但在 dev 中已修复(例如,hadley/dplyr github repo 中的 issue #1643)
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2016-09-15
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-06-08
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多