Заменяйте части переменной с помощью числовых индексов в dplyr. Нужно ли создавать индексный столбец и использовать ifelse?

На одном этапе в более длинной цепочке функций dplyr мне нужно заменить части переменной с помощью числовых индексов, чтобы указать, какие элементы заменить.

Мои данные выглядят следующим образом:

df1 <- data.frame(grp = rep(1:2, each = 3),
                  a = 1:6,
                  b = rep(c(10, 20), each = 3))
df1   
#   grp a  b
# 1   1 1 10
# 2   1 2 10
# 3   1 3 10
# 4   2 4 20
# 5   2 5 20
# 6   2 6 20

Предположим, что в каждой группе мы хотим заменить элементы в переменной a соответствующими элементами в b в одной или нескольких позициях. В этом простом примере я использую один индекс (id), но это может быть вектор индексов. Во-первых, вот как я сделал бы это с помощью ddply:

library(plyr)
id <- 2    
ddply(.data = df1, .variables = .(grp), function(x){
  x$a[id] <- x$b[id]
  x
})

#   grp  a  b
# 1   1  1 10
# 2   1 10 10
# 3   1  3 10
# 4   2  4 20
# 5   2 20 20
# 6   2  6 20

В dplyr я мог бы подумать о различных способах замены. (1) Используйте do с анонимной функцией, аналогичной функции, используемой в ddply. (2) Используйте mutate: объедините вектор, где замена "вставлена" с помощью числовой индексации. Это, вероятно, только полезно для одного индекса. (3) Используйте mutate: создайте вектор индекса и используйте условную замену с помощью ifelse (см. Здесь здесь, здесь, здесь и здесь).

detach("package:plyr", unload = TRUE)
library(dplyr)

# (1)
fun_do <- function(df){
  l <- df %.%
    group_by(grp) %.%
    do(function(dat){
      dat$a[id] <- dat$b[id]
      dat
    })
  do.call(rbind, l)
}

# (2)
fun_mut <- function(df){
  df %.%
  group_by(grp) %.%
  mutate(
    a = c(a[1:(id - 1)], b[id], a[(id + 1):length(a)])
    )
}

# (3)
fun_mut_ifelse <- function(df){
  df %.%
    group_by(grp) %.%
    mutate(
      idx = 1:n(),
      a = ifelse(idx %in% id, b, a)) %.%
    select(-idx)
}

fun_do(df1)
fun_mut(df1)
fun_mut_ifelse(df1)

В тесте с немного большим набором данных "вставка головоломки" выполняется быстрее, но опять же этот метод, вероятно, подходит только для отдельных замен. И это выглядит не очень чисто...

set.seed(123)
df2 <- data.frame(grp = rep(1:200, each = 3),
                  a = rnorm(600),
                  b = rnorm(600))

library(microbenchmark)
microbenchmark(fun_do(df2),
               fun_mut(df2),
               fun_mut_ifelse(df2),
               times = 10)

# Unit: microseconds
#                expr       min        lq    median        uq       max neval
#         fun_do(df2) 48443.075 49912.682 51356.631 53369.644 55108.769    10
#        fun_mut(df2)   891.420   933.996  1019.906  1066.663  1155.235    10
# fun_mut_ifelse(df2)  2503.579  2667.798  2869.270  3027.407  3138.787    10

Чтобы проверить влияние части do.call(rbind в функции do, попробуйте без нее:

fun_do2 <- function(df){
  df %.%
    group_by(grp) %.%
    do(function(dat){
      dat$a[2] <- dat$b[2]
      dat
    })
}
fun_do2(df1)

Затем новый тест большего набора данных:

df3 <- data.frame(grp = rep(1:2000, each = 3),
                  a = rnorm(6000),
                  b = rnorm(6000))

microbenchmark(fun_do(df3),
               fun_do2(df3),
               fun_mut(df3),
               fun_mut_ifelse(df3),
               times = 10)

Опять же, простая "вставка" выполняется быстрее, а функция do теряет почву. В тексте справки do описывается как "дополнение общего назначения" к другим функциям dplyr. Для меня это казалось естественным выбором для анонимной функции. Однако я был удивлен, что do был намного медленнее, также когда пропущена часть не dplyr rbind. В настоящее время документация do довольно скудная, поэтому я задаюсь вопросом, злоупотребляю ли я этой функцией и что могут быть более подходящими (недокументированными?) Способами do?

У меня не было хитов индексов/индексов, когда я искал dplyr текст справки или vignette. Итак, теперь я задаюсь вопросом:
Существуют ли другие методы dplyr для замены частей переменной с использованием числовых индексов, которые я забыл? В частности, является ли создание столбца индекса в сочетании с ifelse способом перехода или существуют более прямые альтернативы a[i] <- b[i]?


Изменить следующий комментарий от @G.Grothendieck(Спасибо!). Добавлена ​​альтернатива replace (кандидат для 'См. Также' в ?[).

fun_replace <- function(df){
  df %.%
    group_by(grp) %.%
    mutate(
      a = replace(a, id, b[id]))
}
fun_replace(df1)

microbenchmark(fun_do(df3),
               fun_do2(df3),
               fun_mut(df3),
               fun_mut_ifelse(df3),
               fun_replace(df3),
               times = 10)

# Unit: milliseconds
#                expr        min         lq     median         uq        max neval
#         fun_do(df3) 685.154605 693.327160 706.055271 712.180410 851.757790    10
#        fun_do2(df3) 291.787455 294.047747 297.753888 299.624730 302.368554    10
#        fun_mut(df3)   5.736640   5.883753   6.206679   6.353222   7.381871    10
# fun_mut_ifelse(df3)  24.321894  26.091049  29.361553  32.649924  52.981525    10
#    fun_replace(df3)   4.616757   4.748665   4.981689   5.279716   5.911503    10

replace функция наиболее быстрая и, безусловно, проще в использовании, чем fun_mut, когда имеется более одного индекса.

Изменить 2 fun_do и fun_do2 больше не работает в dplyr 0.2; Error: Results are not data frames at positions:

Ответы

Ответ 1

Здесь гораздо более быстрый подход к изменению:

library(data.table)

# select rows we want, then assign b to a for those rows, in place
fun_dt = function(dt) dt[dt[, .I[id], by = grp]$V1, a := b]

# benchmark
df4 = data.frame(grp = rep(1:20000, each = 3),
                 a = rnorm(60000),
                 b = rnorm(60000))
dt4 = as.data.table(df4)

library(microbenchmark)

# using fastest function from OP
microbenchmark(fun_dt(dt4), fun_replace(df4), times = 10)
#Unit: milliseconds
#             expr      min        lq    median        uq       max neval
#      fun_dt(dt4) 15.62325  17.22828  18.42445  20.83768  21.25371    10
# fun_replace(df4) 99.03505 107.31529 116.74830 188.89134 286.50199    10