创建使用`dplyr`

问题描述:

考虑这个例子仅数据为数据的子集定义的新的变量:创建使用`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创建一个新变量。此新变量(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可以帮助和一个非常可读和dplyr而Y码确实工作

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 

但后来我不得不把它保存在一个临时对象,然后创建在所有NA原始数据var3,并把所有回一起在相同的数据(因为据我所知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

正如你可以看到,使用rowwisedplyr解决方案比其basedata.table竞争对手慢得多。

+0

你的函数只复制从'var1'非'NA'值到'var3',是意? – mtoto

+0

它是一个示例函数。这不是我的实际功能。这只是一个例子,在这里提供一个简短的可重现代码来说明问题 – elikesprogramming

+2

如何修复你的函数,使它在收到NA时不会中断? – krlmlr

如果你原来的功能没有矢量化,不能与某些输入应付,有一个在使用Vectorize()向量化它没有性能优势。相反,使用dplyr::rowwise()按行操作行:

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

注意,使用if这里是绝对安全的,因为输入长度为1

+0

感谢,'rowwise'是一个好主意。但我只看到它与'do'一起使用。我会尽力让你知道它是怎么回事。 ...,同时,我只是复制粘贴你的代码来查看输出,但它引发了这个错误“错误:不兼容的类型,期待一个数字向量”,...,我还没有调查那里发生了什么(稍后我会这样做),但是如果你在阅读之前很高兴听到可能导致错误的内容 – elikesprogramming

+2

为避免错误,请使用“NA_real_”而不是“NA”。 – eipi10

+0

@elikesprogramming:我正在使用dplyr的开发版本,错误不会发生在这里。否则,'NA_real_'是一个安全选项。 – krlmlr

你可能会考虑使用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 
+0

谢谢@mtoto,是的,这样的部分替换是'data.table'的一个很好的特性。我只是在用'dplyr'来寻找类似的东西,因为即使我是'data.table'性能的粉丝,并没有太多关于它的语法(一种模糊和难以理解的东西......,在这种情况下,尽管;对于这种特殊情况,代码也非常清晰,但在某些情况下,基于data.table的解决方案的代码很难阅读) – elikesprogramming

你可以在完整的行运行函数,然后绑定回与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 
+0

它也是代价高昂,因为不相关的列也被分开并且无缘无故地连在一起:-) – krlmlr

这里有其他两个选项,你可以在dplyr不锈钢管用途:

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 

这是采取乞求宽恕的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 
+0

感谢@jaimedash使用'tryCatch'的好主意,虽然我会在'dplyr'链中执行它,而不是在函数中部分原因是该函数并不像我发布的这个示例那么简单,尽管我可以编写一个包装函数来尝试捕获错误,但我不是这种包装的粉丝) – elikesprogramming

+0

避免重写函数是有意义的。将'tryCatch'直接放在链中作为内联包装的想法似乎很酷,但是当我尝试例如'mydf%>%mutate(var3 = tryCatch(myfn(var1,var2),error = function(e)NA) )'那么var3全是NA。如何使它工作? (PS'rowwise'也没有帮助) – jaimedash

+0

我还没有尝试过,但我认为用'rowwise'内联'tryCatch'可以工作。如果没有'rowwise',我猜它不应该起作用,因为再次,整个向量被传递给函数,'tryCatch'将会出错,并且让NA返回。无论如何,'rowwise'解决方案肯定使用'do'而不是'mutate'(这最后一个可能只适用于'dplyr'的开发版本?)。在下面他自己的答案中查看@Psidom的评论,他在那里提供的代码有效。 – elikesprogramming