使用`dplyr`

时间:2016-04-03 22:42:17

标签: r dplyr

考虑这个示例数据:

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

此示例矢量化函数,不幸的是,只要其中一个参数为NA

,就会触发错误
myfn <- function(x, y){
    sum(x:y)
}
myfn <- Vectorize(myfn)

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

因此,针对类似情况的最常见解决方案是使用ifelse。这样的事情。

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

但这在我的情况下不起作用,因为ifelse实际上将整个向量var1var2传递给myfn而不仅仅是test时的子向量1}}是FALSE。这一切都因为myfn在收到NA时中断而中断。

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

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

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并将所有数据放回到同一数据中(&#39;原因为据我所知unfilter,有些人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)

enter image description here

正如您所看到的,使用dplyr的{​​{1}}解决方案比其rowwisebase竞争对手慢得多。

5 个答案:

答案 0 :(得分:5)

您可以考虑使用data.table,因为dplyr目前不支持in-place mutation,这就是您正在寻找的内容。

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

答案 1 :(得分:2)

如果您的原始函数没有矢量化且无法处理某些输入,那么使用Vectorize()对其进行矢量化并没有任何性能优势。相反,使用dplyr::rowwise()逐行操作:

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

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

答案 2 :(得分:2)

Here are two other options you could use in dplyr-pipes:

a) with a temporary variable

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) with a wrapper function (without changing the original 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

答案 3 :(得分:1)

您可以在完整的行上运行该函数,然后使用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

答案 4 :(得分:1)

这是采用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