Динамическое мутирование нескольких столбцов при кондиционировании определенных строк

11

Я знаю, что здесь есть несколько подобных вопросов, но ни один из них, кажется, не решает точную проблему, которая у меня есть.

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

Я хочу обнулить значения столбцов значений для строк, где Key == "A" На имена столбцов ссылаются через grep:

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

Обычно для достижения того, что я хочу в этом случае, я бы использовал data.tableтак:

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

И желаемый результат выглядит так:

  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

Однако это время мне нужно использовать, так dplyrкак я работаю над командным проектом, где его используют все. Данные, которые я только что предоставил, являются иллюстративными, и мои реальные данные> 5 миллионов строк с 16 столбцами значений, которые необходимо обновить. Единственное решение, которое я мог бы придумать, это использовать mutate_atтак:

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

Тем не менее, это кажется чрезвычайно медленно на моих реальных данных. Я надеялся найти решение, которое будет более элегантным и, что более важно, более быстрым.

Я пробовал много комбинаций с использованием map, снятием кавычек, использованием !!, использованием getи :=(что досадно может быть замаскировано :=в data.table) и т. Д., Но я думаю, что мое понимание того, как эти работы просто недостаточно глубоки, чтобы построить правильное решение.

LiviusI
источник
6
Как долго это займет? DF [DF $ Key == "A", перевалы] <- 0. Я вижу, что это медленно, потому что вы вызываете ifelse и перебираете столбцы и строки.
Глупый Волк
StupidWolf, это на самом деле очень быстро с моими данными, будучи очень компактным и элегантным. Спасибо. Не стесняйтесь добавлять его как ответ, если хотите.
LiviusI
Хорошо, я могу показать вам другое решение, чтобы обойти это ..
StupidWolf

Ответы:

9

С помощью этой команды dplyr,

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

Вы фактически вычисляете оператор df $ Key == "A", n раз, где n = количество столбцов, которые у вас есть.

Обходной путь - предварительно определить строки, которые вы хотите изменить:

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

Более понятный и лучший способ, правильно указанный @IceCreamToucan (см. Комментарии ниже), - это использовать функцию replace, передавая ей дополнительные параметры:

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

Мы можем проверить все эти подходы, и я думаю, что dplyr и data.table сопоставимы.

#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
источник
4
дополнительные аргументы мутировать вычисляются один раз и передаются в качестве параметра функции , представленной (аналогично , например , lapply), так что вы можете сделать это без явного создания переменной IDX , как временнойdf %>% mutate_at(vars(contains('Val')), replace, df$Key == 'A', 0)
IceCreamToucan
Спасибо за указание на @IceCreamToucan, я этого не знал. Да, функция замены еще лучше и менее неуклюжа, чем я. Я включу это в ответ, если вы не возражаете? (кредит тебе конечно).
Глупый Волк,
После тестирования на моей машине кажется, что replaceметод немного медленнее, чем ваш оригинальный idxметод.
IceCreamToucan
1
Также я думаю, что dplyr::if_else()это быстрее, чем база ifelse().
sindri_baldur