Dynamiczne mutowanie wielu kolumn podczas warunkowania określonych wierszy

11

Wiem, że jest tu kilka podobnych pytań, ale wydaje się, że żadne z nich nie odnosi się do konkretnego problemu, który mam.

set.seed(4)
df = data.frame(
  Key = c("A", "B", "A", "D", "A"),
  Val1 = rnorm(5),
  Val2 = runif(5),
  Val3 = 1:5
)

Chcę zerować wartości kolumn wartości dla wierszy, w których Key == "A" Do nazw kolumn odwołuje się grep:

cols = grep("Val", names(df), value = TRUE)

Zwykle, aby osiągnąć to, co chcę w tym przypadku, użyłbym data.tabletak:

library(data.table)
df = as.data.table(df)
df[Key == "A", (cols) := 0]

A pożądany wynik jest następujący:

  Key      Val1       Val2 Val3
1   A  0.000000 0.00000000    0
2   B -1.383814 0.55925762    2
3   A  0.000000 0.00000000    0
4   D  1.437151 0.05632773    4
5   A  0.000000 0.00000000    0

Jednak tym razem muszę go wykorzystać dplyr, pracując nad projektem zespołowym, w którym wszyscy go używają. Przedstawione przeze mnie dane mają charakter poglądowy, a moje rzeczywiste dane to> 5 m wierszy z 16 kolumnami wartości do aktualizacji. Jedyne rozwiązanie, jakie mogłem wymyślić, to mutate_attakie:

df %>% mutate_at(.vars = vars(cols), .funs = function(x) ifelse(df$Key == "A", 0, x))

Wydaje się jednak, że tak bardzo powolne w przypadku moich prawdziwych danych. Miałem nadzieję znaleźć rozwiązanie, które byłoby bardziej eleganckie i, co ważniejsze, szybsze.

Próbowałem wielu kombinacji przy użyciu map, bez cytowania przy użyciu !!, przy użyciu geti :=(które irytujące mogą zostać zamaskowane :=w tabeli data.tab) itp., Ale myślę, że moje rozumienie tego, jak te prace nie są po prostu wystarczająco głębokie, aby stworzyć prawidłowe rozwiązanie.

LiviusI
źródło
6
jak długo to trwa? df [df $ Key == "A", cols] <- 0. Widzę, że jest powolny, ponieważ wywołujesz ifelse i zapętlasz kolumny i wiersze.
StupidWolf,
StupidWolf, To jest naprawdę bardzo szybkie z moimi danymi, a jednocześnie jest bardzo kompaktowe i eleganckie. Dzięki. Jeśli chcesz, możesz dodać ją jako odpowiedź.
LiviusI
Ok, mogę pokazać wam inne rozwiązanie tego problemu.
StupidWolf,

Odpowiedzi:

9

Za pomocą tego polecenia dplyr

df %>% mutate_at(.vars = vars(cols), .funs = function(x) ifelse(df$Key == "A", 0, x))

Rzeczywiście oceniasz wyrażenie df $ Key == "A", n razy, gdzie n = liczba kolumn, które masz.

Jednym z obejść jest wstępne zdefiniowanie wierszy, które chcesz zmienić:

idx = which(DF$Key=="A")
DF %>% mutate_at(.vars = vars(cols), .funs = function(x){x[idx]=0;x})

Czystszym i lepszym sposobem, poprawnie wskazanym przez @IceCreamToucan (patrz komentarze poniżej), jest użycie funkcji replace, z jednoczesnym przekazaniem jej dodatkowych parametrów:

DF %>% mutate_at(.vars = vars(cols), replace, DF$Key == 'A', 0)

Możemy przetestować wszystkie te podejścia i uważam, że dplyr i data.table są porównywalne.

#simulate data
set.seed(100)
Key = sample(LETTERS[1:3],1000000,replace=TRUE)
DF = as.data.frame(data.frame(Key,matrix(runif(1000000*10),nrow=1000000,ncol=10)))
DT = as.data.table(DF)

cols = grep("[35789]", names(DF), value = TRUE)

#long method
system.time(DF %>% mutate_at(.vars = vars(cols), .funs = function(x) ifelse(DF$Key == "A", 0, x)))
user  system elapsed 
  0.121   0.035   0.156 

#old base R way
system.time(DF[idx,cols] <- 0)
   user  system elapsed 
  0.085   0.021   0.106 

#dplyr
# define function
func = function(){
       idx = which(DF$Key=="A")
       DF %>% mutate_at(.vars = vars(cols), .funs = function(x){x[idx]=0;x})
}
system.time(func())
user  system elapsed 
  0.020   0.006   0.026

#data.table
system.time(DT[Key=="A", (cols) := 0])
   user  system elapsed 
  0.012   0.001   0.013 
#replace with dplyr
system.time(DF %>% mutate_at(.vars = vars(cols), replace, DF$Key == 'A', 0))
user  system elapsed 
  0.007   0.001   0.008
StupidWolf
źródło
4
dodatkowe argumenty do mutacji są oceniane raz i przekazywane jako parametr do podanej funkcji (podobnie jak np. lapply), więc możesz to zrobić bez jawnego tworzenia zmiennej temp idx asdf %>% mutate_at(vars(contains('Val')), replace, df$Key == 'A', 0)
IceCreamToucan
Dzięki za zwrócenie uwagi na @IceCreamToucan, nie wiedziałem o tym. Tak, funkcja zamiany jest jeszcze lepsza i mniej niezręczna ode mnie. Dołączę to do odpowiedzi, jeśli nie masz nic przeciwko? (Podziękowania dla ciebie oczywiście).
StupidWolf,
Po przetestowaniu na mojej maszynie wydaje się, że replacemetoda jest nieco wolniejsza niż oryginalna idx.
IceCreamToucan
1
Myślę też, że dplyr::if_else()jest szybszy niż baza ifelse().
sindri_baldur