2016-04-03 63 views
8

考慮這個例子僅數據爲數據的子集定義的新的變量:創建使用`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

回答

2

如果你原來的功能沒有矢量化,不能與某些輸入應付,有一個在使用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

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 
+0

謝謝@mtoto,是的,這樣的部分替換是'data.table'的一個很好的特性。我只是在用'dplyr'來尋找類似的東西,因爲即使我是'data.table'性能的粉絲,並沒有太多關於它的語法(一種模糊和難以理解的東西......,在這種情況下,儘管;對於這種特殊情況,代碼也非常清晰,但在某些情況下,基於data.table的解決方案的代碼很難閱讀) – elikesprogramming

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 
+0

它也是代價高昂,因爲不相關的列也被分開並且無緣無故地連在一起:-) – krlmlr

2

這裏有其他兩個選項,你可以在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 
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 
+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