Как эффективно реализовать coalesce в R

фон

несколько языков SQL (я в основном использую postgreSQL) имеют функцию coalesce, которая возвращает первый ненулевой элемент столбца для каждой строки. Это может быть очень эффективно использовать, когда таблицы имеют много NULL элементов в них.

я сталкиваюсь с этим во многих сценариях в R, а также при работе с не столь структурированными данными, в которых есть много NA.

Я сам сделал наивную реализацию, но это смешно медленный.

coalesce <- function(...) {
  apply(cbind(...), 1, function(x) {
          x[which(!is.na(x))[1]]
        })
}

пример

a <- c(1,  2,  NA, 4, NA)
b <- c(NA, NA, NA, 5, 6)
c <- c(7,  8,  NA, 9, 10)
coalesce(a,b,c)
# [1]  1  2 NA  4  6

вопрос

есть ли эффективный способ реализации coalesce в R?

7 ответов


на моей машине, используя Reduce получает 5-кратное повышение производительности:

coalesce2 <- function(...) {
  Reduce(function(x, y) {
    i <- which(is.na(x))
    x[i] <- y[i]
    x},
  list(...))
}

> microbenchmark(coalesce(a,b,c),coalesce2(a,b,c))
Unit: microseconds
               expr    min       lq   median       uq     max neval
  coalesce(a, b, c) 97.669 100.7950 102.0120 103.0505 243.438   100
 coalesce2(a, b, c) 19.601  21.4055  22.8835  23.8315  45.419   100

похоже, что coalesce1 все еще доступен

coalesce1 <- function(...) {
    ans <- ..1
    for (elt in list(...)[-1]) {
        i <- is.na(ans)
        ans[i] <- elt[i]
    }
    ans
}

что еще быстрее (но более или менее рука переписывает Reduce, так меньше общей)

> identical(coalesce(a, b, c), coalesce1(a, b, c))
[1] TRUE
> microbenchmark(coalesce(a,b,c), coalesce1(a, b, c), coalesce2(a,b,c))
Unit: microseconds
               expr     min       lq   median       uq     max neval
  coalesce(a, b, c) 336.266 341.6385 344.7320 355.4935 538.348   100
 coalesce1(a, b, c)   8.287   9.4110  10.9515  12.1295  20.940   100
 coalesce2(a, b, c)  37.711  40.1615  42.0885  45.1705  67.258   100

или для сравнения больших данных

coalesce1a <- function(...) {
    ans <- ..1
    for (elt in list(...)[-1]) {
        i <- which(is.na(ans))
        ans[i] <- elt[i]
    }
    ans
}

показывал, что which() иногда может быть эффективным, даже если это будет означать второй проход через индекс.

> aa <- sample(a, 100000, TRUE)
> bb <- sample(b, 100000, TRUE)
> cc <- sample(c, 100000, TRUE)
> microbenchmark(coalesce1(aa, bb, cc),
+                coalesce1a(aa, bb, cc),
+                coalesce2(aa,bb,cc), times=10)
Unit: milliseconds
                   expr       min        lq    median        uq       max neval
  coalesce1(aa, bb, cc) 11.110024 11.137963 11.145723 11.212907 11.270533    10
 coalesce1a(aa, bb, cc)  2.906067  2.953266  2.962729  2.971761  3.452251    10
  coalesce2(aa, bb, cc)  3.080842  3.115607  3.139484  3.166642  3.198977    10

используя dplyr пакет:

library(dplyr)
coalesce(a, b, c)
# [1]  1  2 NA  4  6

Benchamark, не так быстро, как приняли решение:

coalesce2 <- function(...) {
  Reduce(function(x, y) {
    i <- which(is.na(x))
    x[i] <- y[i]
    x},
    list(...))
}

microbenchmark::microbenchmark(
  coalesce(a, b, c),
  coalesce2(a, b, c)
)

# Unit: microseconds
#                expr    min     lq     mean median      uq     max neval cld
#   coalesce(a, b, c) 21.951 24.518 27.28264 25.515 26.9405 126.293   100   b
#  coalesce2(a, b, c)  7.127  8.553  9.68731  9.123  9.6930  27.368   100  a 

но на большем наборе данных он сопоставим:

aa <- sample(a, 100000, TRUE)
bb <- sample(b, 100000, TRUE)
cc <- sample(c, 100000, TRUE)

microbenchmark::microbenchmark(
  coalesce(aa, bb, cc),
  coalesce2(aa, bb, cc))

# Unit: milliseconds
#                   expr      min       lq     mean   median       uq      max neval cld
#   coalesce(aa, bb, cc) 1.708511 1.837368 5.468123 3.268492 3.511241 96.99766   100   a
#  coalesce2(aa, bb, cc) 1.474171 1.516506 3.312153 1.957104 3.253240 91.05223   100   a

у меня есть готовая к использованию реализация под названием coalesce.na на мой пакет смешанная. Кажется, он конкурентоспособен, но не самый быстрый. Он также будет работать для векторов разной длины и имеет специальную обработку для векторов длины один:

                    expr        min          lq      median          uq         max neval
    coalesce(aa, bb, cc) 990.060402 1030.708466 1067.000698 1083.301986 1280.734389    10
   coalesce1(aa, bb, cc)  11.356584   11.448455   11.804239   12.507659   14.922052    10
  coalesce1a(aa, bb, cc)   2.739395    2.786594    2.852942    3.312728    5.529927    10
   coalesce2(aa, bb, cc)   2.929364    3.041345    3.593424    3.868032    7.838552    10
 coalesce.na(aa, bb, cc)   4.640552    4.691107    4.858385    4.973895    5.676463    10

вот код:

coalesce.na <- function(x, ...) {
  x.len <- length(x)
  ly <- list(...)
  for (y in ly) {
    y.len <- length(y)
    if (y.len == 1) {
      x[is.na(x)] <- y
    } else {
      if (x.len %% y.len != 0)
        warning('object length is not a multiple of first object length')
      pos <- which(is.na(x))
      x[pos] <- y[(pos - 1) %% y.len + 1]
    }
  }
  x
}

конечно, как отметил Кевин, решение Rcpp может быть быстрее на порядок.


вот мое решение:

coalesce <- function(x){ y <- head( x[is.na(x) == F] , 1) return(y) } Он возвращает первый vaule, который не является NA, и он работает на data.table, например, если вы хотите использовать coalesce на нескольких Столбцах, и эти имена столбцов находятся в векторе строк:

column_names <- c("col1", "col2", "col3")

как использовать:

ranking[, coalesce_column := coalesce( mget(column_names) ), by = 1:nrow(ranking)]


другой метод применения, с mapply.

mapply(function(...) {temp <- c(...); temp[!is.na(temp)][1]}, a, b, c)
[1]  1  2 NA  4  6

это выбирает первое значение не-NA, если существует более одного. Последний не-отсутствующий элемент может быть выбран с помощью tail.

возможно, немного больше скорости можно выжать из этой альтернативы, используя голые кости .mapply функция, которая выглядит немного по-другому.

unlist(.mapply(function(...) {temp <- c(...); temp[!is.na(temp)][1]},
               dots=list(a, b, c), MoreArgs=NULL))
[1]  1  2 NA  4  6

.mapplyотличается важными способами от своего не-пунктирного кузена.

  • возвращает список (как Map) и поэтому должен быть обернут в некоторую функцию, такую как unlist или c возвращает вектор.
  • набор аргументов, которые будут подаваться параллельно функции в FUN, должен быть задан в списке аргументу dots.
  • наконец, mapply, аргумент moreArgs не имеет значения по умолчанию, поэтому необходимо явно ввести NULL.

A очень простое решение-использовать С base пакет:

coalesce3 <- function(x, y) {

    ifelse(is.na(x), y, x)
}

хотя он кажется медленнее, чем coalesce2 выше:

test <- function(a, b, func) {

    for (i in 1:10000) {

        func(a, b)
    }
}

system.time(test(a, b, coalesce2))
user  system elapsed 
0.11    0.00    0.10 

system.time(test(a, b, coalesce3))
user  system elapsed 
0.16    0.00    0.15 

можно использовать Reduce чтобы он работал для произвольного числа векторов:

coalesce4 <- function(...) {

    Reduce(coalesce3, list(...))
}