Mutando várias colunas dinamicamente enquanto condiciona em linhas específicas

11

Sei que há várias perguntas semelhantes por aqui, mas nenhuma delas parece abordar o problema exato que estou tendo.

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

Desejo zerar os valores das colunas de valor das linhas em que Key == "A" Os nomes das colunas são referenciados por meio de um grep:

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

Normalmente, para alcançar o que eu quero, neste caso, eu usaria data.tableassim:

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

E a saída desejada é assim:

  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

No entanto, desta vez eu preciso usá-lo, dplyrpois estou trabalhando em um projeto de equipe onde todos o usam. Os dados que acabei de fornecer são ilustrativos e meus dados reais são> 5m linhas com 16 colunas de valor a serem atualizadas. A única solução que eu poderia encontrar é usar mutate_atassim:

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

No entanto, isso parece ser extremamente lento nos meus dados reais. Eu esperava encontrar uma solução mais elegante e, mais importante, mais rápida.

Eu tentei muitas combinações usando map, sem citar usando !!, usando gete :=(que irritantemente podem ser mascarados pela :=data.table) etc, mas acho que meu entendimento de como esses trabalhos simplesmente não são profundos o suficiente para construir uma solução válida.

LiviusI
fonte
6
quanto tempo isso leva? df [$ Chave == "A", colunas] <- 0. Percebo que é lento porque você está chamando ifelse e fazendo um loop nas colunas e linhas.
StupidWolf #
StupidWolf, na verdade, é muito rápido com meus dados, sendo muito compacto e elegante. Obrigado. Sinta-se à vontade para adicioná-lo como resposta, se desejar.
LiviusI
Ok eu posso mostrar-lhe outra solução para contornar o problema ..
StupidWolf

Respostas:

9

Com este comando dplyr,

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

Na verdade, você está avaliando a instrução df $ Key == "A", n vezes, em que n = o número de colunas que você possui.

Uma solução alternativa é pré-definir as linhas que você deseja alterar:

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

Uma maneira mais limpa e melhor, corretamente apontada pelo @IceCreamToucan (veja os comentários abaixo), é usar a função replace, passando os parâmetros extras:

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

Podemos testar todas essas abordagens, e acho que dplyr e data.table são comparáveis.

#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
fonte
4
argumentos extras de mutação são avaliados uma vez e passado como um parâmetro para a função fornecida (semelhante ao exemplo lapply), assim você pode fazer isso sem um criar explicitamente o idx variável temporário comodf %>% mutate_at(vars(contains('Val')), replace, df$Key == 'A', 0)
IceCreamToucan
Obrigado por apontar @IceCreamToucan, eu não sabia disso. Sim, a função de substituição é ainda melhor e menos desajeitada que eu. Vou incluí-lo na resposta, se você não se importa? (crédito para você, é claro).
StupidWolf 18/11/19
Depois de testar na minha máquina, parece que o replacemétodo é um pouco mais lento que o idxmétodo original .
IceCreamToucan
11
Também acho que dplyr::if_else()é mais rápido que a base ifelse().
sindri_baldur