Mutar múltiples columnas dinámicamente mientras se acondiciona en filas específicas

11

Sé que hay varias preguntas similares por aquí, pero ninguna de ellas parece abordar el problema preciso que estoy teniendo.

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

Quiero poner a cero los valores de las columnas de valor para las filas donde Clave == "A" Los nombres de columna se referencian a través de un grep:

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

Normalmente para lograr lo que quiero en este caso, usaría data.tableasí:

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

Y la salida deseada es así:

  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

Sin embargo, esta vez necesito usarlo, dplyrya que estoy trabajando en un proyecto de equipo donde todos lo usan. Los datos que acabo de proporcionar son ilustrativos y mis datos reales son> 5m de filas con 16 columnas de valor para actualizar. La única solución que se me ocurre es usar mutate_atasí:

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

Sin embargo, esto parece ser extremadamente lento en mis datos reales. Esperaba encontrar una solución que fuera más elegante y, lo que es más importante, más rápida.

He intentado muchas combinaciones usando map, sin comillas usando !!, usando gety :=(que molestamente puede enmascararse en el :=archivo data.table), etc., pero creo que mi comprensión de cómo funcionan estos simplemente no es lo suficientemente profunda como para construir una solución válida.

LiviusI
fuente
66
¿Cuánto tiempo lleva esto? df [df $ Key == "A", cols] <- 0. Puedo ver que es lento porque estás llamando ifelse y recorriendo las columnas y filas.
StupidWolf
StupidWolf, esto es realmente muy rápido con mis datos, a la vez que es muy compacto y elegante. Gracias. Siéntase libre de agregarlo como respuesta si lo desea.
LiviusI
Ok, puedo mostrarte otra solución para
evitarlo

Respuestas:

9

Con este comando dplyr,

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

En realidad está evaluando la declaración df $ Key == "A", n veces, donde n = el número de columnas que tiene.

Una solución es predefinir las filas que desea cambiar:

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

Una forma más limpia y mejor, señalada correctamente por @IceCreamToucan (ver comentarios a continuación), es usar la función reemplazar, mientras le pasa los parámetros adicionales:

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

Podemos poner a prueba todos estos enfoques, y creo que dplyr y data.table son comparables.

#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
Estúpido Lobo
fuente
44
los argumentos adicionales para mutar se evalúan una vez y se pasan como un parámetro a la función proporcionada (similar a, por ejemplo, lapply), por lo que puede hacerlo sin crear explícitamente la variable temporal idx comodf %>% mutate_at(vars(contains('Val')), replace, df$Key == 'A', 0)
IceCreamToucan
Gracias por señalarlo @IceCreamToucan, no lo sabía. Sí, la función de reemplazo es aún mejor y menos torpe que yo. ¿Lo incluiré en la respuesta si no le importa? (crédito para ti, por supuesto).
StupidWolf
Después de probar en mi máquina, parece que el replacemétodo es un poco más lento que el idxmétodo original .
IceCreamToucan
1
También creo que dplyr::if_else()es más rápido que la base ifelse().
sindri_baldur