Mutation dynamique de plusieurs colonnes tout en conditionnant sur des lignes spécifiques

11

Je sais qu'il y a plusieurs questions similaires ici, mais aucune ne semble répondre au problème précis que j'ai.

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

Je veux mettre à zéro les valeurs des colonnes de valeurs pour les lignes où Clé == "A" Les noms des colonnes sont référencés via un grep:

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

Normalement, pour réaliser ce que je veux dans ce cas, j'utiliserais data.tablececi:

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

Et la sortie souhaitée est comme ceci:

  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

Cependant, cette fois, je dois l'utiliser dplyrcar je travaille sur un projet d'équipe où tout le monde l'utilise. Les données que je viens de fournir sont illustratives et mes données réelles sont> 5 m de lignes avec 16 colonnes de valeurs à mettre à jour. La seule solution que j'ai pu trouver est d'utiliser mutate_atcomme ceci:

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

Cependant, cela semble être extrêmement lent sur mes données réelles. J'espérais trouver une solution plus élégante et surtout plus rapide.

J'ai essayé de nombreuses combinaisons en utilisant map, en ne citant pas en utilisant !!, en utilisant getet :=(qui peuvent être masquées de manière agaçante par la :=table de données), etc., mais je pense que ma compréhension de la façon dont ces travaux ne sont tout simplement pas assez profonds pour construire une solution valide.

LiviusI
la source
6
combien de temps cela prend-il? df [df $ Key == "A", cols] <- 0. Je peux voir que c'est lent parce que vous appelez ifelse et faites une boucle sur les colonnes et les lignes.
StupidWolf
StupidWolf, C'est en fait très rapide avec mes données, tout en étant très compact et élégant. Merci. N'hésitez pas à l'ajouter comme réponse si vous le souhaitez.
LiviusI
Ok, je peux vous montrer une autre solution pour contourner cela.
StupidWolf

Réponses:

9

Avec cette commande dplyr,

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

Vous évaluez actuellement l'instruction df $ Key == "A", n fois, où n = le nombre de colonnes que vous avez.

Une solution consiste à prédéfinir les lignes que vous souhaitez modifier:

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

Une manière plus propre et meilleure, correctement indiquée par @IceCreamToucan (voir les commentaires ci-dessous), consiste à utiliser la fonction replace, tout en lui passant les paramètres supplémentaires:

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

Nous pouvons tester toutes ces approches, et je pense que dplyr et data.table sont 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
StupidWolf
la source
4
les arguments supplémentaires à muter sont évalués une fois et transmis en tant que paramètre à la fonction fournie (comme par exemple lapply), vous pouvez donc le faire sans créer explicitement la variable temp idx asdf %>% mutate_at(vars(contains('Val')), replace, df$Key == 'A', 0)
IceCreamToucan
Merci de l'avoir signalé @IceCreamToucan, je ne le savais pas. Ouais, la fonction de remplacement est encore meilleure et moins maladroite que moi. Je vais l'inclure dans la réponse si cela ne vous dérange pas? (crédit à vous bien sûr).
StupidWolf
Après avoir testé sur ma machine, il semble que la replaceméthode soit un peu plus lente que votre idxméthode d' origine .
IceCreamToucan
1
Je pense aussi que dplyr::if_else()c'est plus rapide que la base ifelse().
sindri_baldur