Come implementare la funzione coalesce in modo efficiente in R

Sfondo

Diversi linguaggi SQL (io lo uso postgreSQL) hanno una funzione chiamata coalesce che restituisce il primo non nullo elemento di colonna per ogni riga. Questo può essere molto efficace utilizzare quando le tabelle sono un sacco di NULL elementi in loro.

Incontro questo in un sacco di scenari in R anche quando ha a che fare con dati strutturati che ha un sacco di NA li.

Ho fatto un ingenuo attuazione di me stesso, ma è terribilmente lento.

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

Esempio

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

Domanda

C’è alcun modo efficiente per implementare coalesce in R?

  • Come si fa a definire “ridicolo lento”? (La chiamata è descrivere prende ~100 microsecondi sulla mia macchina). Come molti vettori hai e da quanto tempo sono? (Per inciso, in un modo che accelera leggermente (~5%) è di fare x[!is.na(x)][1] invece di x[which(!is.na(x))[1]])
  • La difficoltà principale qui IMO è la vettorializzazione non è di grande aiuto nella risoluzione di questo problema; un sacco di elementi che sono inutilmente sondati da which e is.na, e cbind + apply potrà effettuare copie dei dati e sarà lenta per i grandi vettori. Vorrei raccomandare una Rcpp soluzione (e potrebbe provare a cucinare qualcosa di più tardi).
  • Sì, io sono abbastanza grandi vettori. Il problema a cui sto lavorando atm utilizza vettori di lunghezza 608247. Che è un po ‘ più lungo esempio.
  • coalesce() non riesce anche se tutti i parametri sono NULL. Questa è una soluzione rapida: "%??%" <- coalesce <- function(..., default = NA) apply(cbind(..., default), 1, function(x) x[which(!is.na(x))[1]])
  • Consultare le risposte alle la mia domanda e per i suggerimenti per incorporazione di fondersi con le funzioni unisce/join.
InformationsquelleAutor while | 2013-10-08

 

8 Replies
  1. 40

    Sulla mia macchina, utilizzando Reduce ottiene un 5x miglioramento delle prestazioni:

    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
    • Grazie! Questo è tremendamente più veloce! Secondo tempo di oggi Ridurre salva il mio culo. Provato a fare qualcosa di simile me, ma non riuscivo a capire. Impressionante.
    • Funziona alla grande se i vettori passati sono la stessa lunghezza. Ho avuto problemi con questo, perché stavo cercando coalesce2(NULL, c(3, 2)), così si fa.na() mi ha dato le avvertenze, mentre coalesce() si ripete solo NA due volte per la prima colonna.
    • Questo non funziona se è NULL invece di NA. Si raccomanda l’uso di is.null.
  2. 21

    Sembra coalesce1 è ancora disponibile

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

    che è ancora più veloce (ma più o meno una mano ri-scrittura di Reduce, in modo meno generale)

    > 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

    O per i più grandi in confronto dati

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

    mostrando che which() a volte può essere efficace, anche se questo implica un secondo passaggio attraverso l’indice.

    > 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
    • Interessante che è molto più veloce di Ridurre. La mia ipotesi è che è perché il loop versione di evitare il sovraccarico di una chiamata di funzione (o, eventualmente, di alcuni parametri di controllo), che potrebbe essere un collo di bottiglia quando gli ingressi sono di piccole dimensioni. Questo spiegherebbe perché i tempi sono molto più vicini con grande ingressi.
    • forse si dovrebbe aggiungere un test su un numero di argomenti di qualcosa come ” se(lunghezza(list(…)>0)){}` per evitare un errore quando si chiama coalesce1() senza argomenti..
  3. 15

    Utilizzando dplyr pacchetto:

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

    Benchamark, non è veloce come soluzione accettata:

    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 

    Ma su un set di dati più grandi, è paragonabile:

    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
  4. 9

    Ho un ready-to-utilizzare attuazione chiamato coalesce.na in il mio misc pacchetto. Sembra di essere competitivo, ma non più veloce.
    Funziona anche per i vettori di lunghezza diversa, e ha un trattamento speciale per i vettori di lunghezza uno:

                        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

    Ecco il codice:

    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
    }

    Naturalmente, come Kevin sottolineato, un Rcpp potrebbe essere la soluzione più veloce per gli ordini di grandezza.

  5. 3

    Un molto soluzione semplice è quella di utilizzare il ifelse funzione dal base pacchetto:

    coalesce3 <- function(x, y) {
    
        ifelse(is.na(x), y, x)
    }

    Anche se sembra essere più lento di coalesce2 sopra:

    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 

    È possibile utilizzare Reduce per farlo funzionare per un numero arbitrario di vettori:

    coalesce4 <- function(...) {
    
        Reduce(coalesce3, list(...))
    }
    • Certo. Ma questo non funziona per più di due vettori. Cosa succede se si dispone di una quantità arbitraria di vettori?
    • Sì che è una limitazione. Soluzione rapida è quella di utilizzare Reduce.
    • è interessante notare che, un esplicito if-else non convertire le date in numeri, ma ifelse() fa. Così si funzione date non funziona: coalesce4(NULL, lubridate::ymd('2019-05-01')) restituisce 18017
  6. 2

    Ecco la mia soluzione:

    coalesce <- function(x){
    y <- head( x[is.na(x) == F] , 1)
    return(y)
    }

    Restituisce il primo valore che non è NA e funziona su data.table, per esempio, se si desidera utilizzare la funzione coalesce su alcune colonne e questi nomi sono nel vettore di stringhe:

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

    istruzioni per l’uso:

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

  7. 1

    Un altro metodo applicato, con mapply.

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

    Seleziona il primo non-NA valore se esiste più di una. L’ultimo non-elemento mancante potrebbe essere selezionati utilizzando tail.

    Forse un po ‘più di velocità potrebbe essere spremuto fuori di questa alternativa utilizzando la bare bones .mapply funzione, che sembra un po’ diverso.

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

    .mapplydifferisce in modo significativo dal suo mancato tratteggiata cugino.

    • restituisce una lista (come Map) e, pertanto, deve essere avvolto in qualche funzione come unlist o c per restituire un vettore.
    • la serie di argomenti, di essere alimentati in parallelo con la funzione di DIVERTIMENTO devono essere indicati in un elenco a punti argomento.
    • Infine, mapply, il moreArgs argomento non dispone di un valore predefinito in modo esplicito deve essere alimentato NULL.

Lascia un commento