Accelerare l'operazione di loop in R


193

Ho un grosso problema di prestazioni in R. Ho scritto una funzione che scorre su un data.frameoggetto. Aggiunge semplicemente una nuova colonna a data.framee accumula qualcosa. (operazione semplice). L' data.frameha approssimativamente 850K righe. Il mio PC funziona ancora (circa 10 ore adesso) e non ho idea del tempo di esecuzione.

dayloop2 <- function(temp){
    for (i in 1:nrow(temp)){    
        temp[i,10] <- i
        if (i > 1) {             
            if ((temp[i,6] == temp[i-1,6]) & (temp[i,3] == temp[i-1,3])) { 
                temp[i,10] <- temp[i,9] + temp[i-1,10]                    
            } else {
                temp[i,10] <- temp[i,9]                                    
            }
        } else {
            temp[i,10] <- temp[i,9]
        }
    }
    names(temp)[names(temp) == "V10"] <- "Kumm."
    return(temp)
}

Qualche idea su come velocizzare questa operazione?

Risposte:


435

Il problema maggiore e la radice dell'inefficacia è l'indicizzazione di data.frame, intendo tutte queste righe in cui si utilizza temp[,].
Cerca di evitarlo il più possibile. Ho preso la tua funzione, cambio indicizzazione e qui versione_A

dayloop2_A <- function(temp){
    res <- numeric(nrow(temp))
    for (i in 1:nrow(temp)){    
        res[i] <- i
        if (i > 1) {             
            if ((temp[i,6] == temp[i-1,6]) & (temp[i,3] == temp[i-1,3])) { 
                res[i] <- temp[i,9] + res[i-1]                   
            } else {
                res[i] <- temp[i,9]                                    
            }
        } else {
            res[i] <- temp[i,9]
        }
    }
    temp$`Kumm.` <- res
    return(temp)
}

Come puoi vedere, creo un vettore resche raccoglie risultati. Alla fine lo aggiungo a data.framee non ho bisogno di scherzare con i nomi. Quindi quanto è meglio?

Corro ogni funzione per data.framecon nrowda 1.000 a 10.000 da 1.000 e misurare il tempo consystem.time

X <- as.data.frame(matrix(sample(1:10, n*9, TRUE), n, 9))
system.time(dayloop2(X))

Il risultato è

prestazione

Puoi vedere che la tua versione dipende in modo esponenziale nrow(X). La versione modificata ha una relazione lineare e il lmmodello semplice prevede che per 850.000 righe il calcolo richiede 6 minuti e 10 secondi.

Potenza di vettorializzazione

Come affermano Shane e Calimo nelle loro risposte, la vettorializzazione è la chiave per prestazioni migliori. Dal tuo codice potresti spostarti al di fuori del ciclo:

  • condizionata
  • inizializzazione dei risultati (che sono temp[i,9])

Questo porta a questo codice

dayloop2_B <- function(temp){
    cond <- c(FALSE, (temp[-nrow(temp),6] == temp[-1,6]) & (temp[-nrow(temp),3] == temp[-1,3]))
    res <- temp[,9]
    for (i in 1:nrow(temp)) {
        if (cond[i]) res[i] <- temp[i,9] + res[i-1]
    }
    temp$`Kumm.` <- res
    return(temp)
}

Confronta i risultati per queste funzioni, questa volta nrowda 10.000 a 100.000 per 10.000.

prestazione

Sintonizzazione del sintonizzato

Un altro tweak è a cambiare in un'indicizzazione ciclo temp[i,9]a res[i](che sono esattamente gli stessi in i-esima iterazione del ciclo). È di nuovo la differenza tra indicizzare un vettore e indicizzare a data.frame.
Seconda cosa: quando guardi il loop puoi vedere che non è necessario eseguire il loop su tutto i, ma solo per quelli che si adattano alle condizioni.
Quindi eccoci

dayloop2_D <- function(temp){
    cond <- c(FALSE, (temp[-nrow(temp),6] == temp[-1,6]) & (temp[-nrow(temp),3] == temp[-1,3]))
    res <- temp[,9]
    for (i in (1:nrow(temp))[cond]) {
        res[i] <- res[i] + res[i-1]
    }
    temp$`Kumm.` <- res
    return(temp)
}

Le prestazioni ottenute dipendono fortemente da una struttura di dati. Precisamente - sulla percentuale di TRUEvalori nella condizione. Per i miei dati simulati ci vuole tempo di calcolo per 850.000 righe al di sotto del secondo.

prestazione

Voglio che tu possa andare oltre, vedo almeno due cose che possono essere fatte:

  • scrivere un Ccodice per fare un cumsum condizionale
  • se sai che nella sequenza massima dei tuoi dati non è grande, puoi cambiare loop in vettorializzato mentre, qualcosa del genere

    while (any(cond)) {
        indx <- c(FALSE, cond[-1] & !cond[-n])
        res[indx] <- res[indx] + res[which(indx)-1]
        cond[indx] <- FALSE
    }

Il codice utilizzato per simulazioni e figure è disponibile su GitHub .


2
Dato che non riesco a trovare un modo per chiedere a Marek privatamente, come sono stati generati quei grafici?
carbontwelve,

@carbontwelve Stai chiedendo dati o grafici? Le trame sono state realizzate con un pacchetto reticolare. Se ho tempo, metto il codice da qualche parte sul web e te lo avviso.
Marek,

@carbontwelve Ooops, ho sbagliato :) Questi sono grafici standard (dalla base R).
Marek,

@Gregor Purtroppo no. È cumulativo, quindi non è possibile vettorializzarlo. Semplice esempio: res = c(1,2,3,4)ed condè tutto TRUE, poi risultato finale dovrebbe essere: 1, 3(cause 1+2), 6(seconda causa è ora 3, e il terzo è 3anche), 10( 6+4). Facendo semplice sommatoria hai 1, 3, 5, 7.
Marek,

Ah, avrei dovuto pensarci più attentamente. Grazie per avermi mostrato l'errore.
Gregor Thomas,

132

Strategie generali per accelerare il codice R.

In primo luogo, capire dove si trova davvero la parte lenta. Non è necessario ottimizzare il codice che non viene eseguito lentamente. Per piccole quantità di codice, semplicemente pensare attraverso di esso può funzionare. In caso contrario, possono essere utili RProf e strumenti di profilazione simili.

Una volta individuato il collo di bottiglia, pensa ad algoritmi più efficienti per fare ciò che desideri. I calcoli dovrebbero essere eseguiti solo una volta, se possibile, quindi:

L'uso di funzioni più efficienti può produrre guadagni di velocità moderati o grandi. Ad esempio, paste0produce un piccolo guadagno di efficienza ma i .colSums()suoi parenti producono guadagni un po 'più pronunciati. meanè particolarmente lento .

Quindi puoi evitare alcuni problemi particolarmente comuni :

  • cbind ti rallenterà molto rapidamente.
  • Inizializza le tue strutture dati, quindi compilale, anziché espanderle ogni volta .
  • Anche con la pre-allocazione, è possibile passare a un approccio pass-by-reference piuttosto che a un approccio pass-by-value, ma potrebbe non valere la pena.
  • Dai un'occhiata alla R Inferno per ulteriori insidie ​​da evitare.

Cerca una migliore vettorializzazione , che spesso può ma non sempre aiutare. A questo proposito, comandi intrinsecamente vettorizzati come ifelse, diffe simili forniranno un miglioramento maggiore rispetto alla applyfamiglia di comandi (che forniscono un aumento di velocità minimo o nullo su un ciclo ben scritto).

Si può anche provare a fornire maggiori informazioni alle funzioni R . Ad esempio, utilizzare vapplypiuttosto chesapply e specificare colClassesdurante la lettura di dati basati su testo . I guadagni di velocità saranno variabili in base alla quantità di ipotesi eliminate.

Successivamente, prendi in considerazione pacchetti ottimizzati : il data.tablepacchetto può produrre enormi guadagni di velocità laddove il suo utilizzo è possibile, nella manipolazione dei dati e nella lettura di grandi quantità di dati ( fread).

Quindi, prova ad aumentare la velocità attraverso mezzi più efficienti per chiamare R :

  • Compila il tuo script R. Oppure usa i pacchetti Rae jitin concerto per la compilazione just-in-time (Dirk ha un esempio in questa presentazione ).
  • Assicurati di utilizzare un BLAS ottimizzato. Questi forniscono guadagni di velocità su tutta la linea. Onestamente, è un peccato che R non usi automaticamente la libreria più efficiente durante l'installazione. Speriamo che Revolution R contribuirà con il lavoro che hanno svolto qui alla comunità generale.
  • Radford Neal ha fatto un sacco di ottimizzazioni, alcune delle quali sono state adottate in R Core e molte altre che sono state biforcute in pqR .

E infine, se tutto quanto sopra non ti rende ancora abbastanza veloce come ti serve, potresti dover passare a una lingua più veloce per lo snippet di codice lento . La combinazione di Rcppe inlinequi rende particolarmente semplice la sostituzione della parte più lenta dell'algoritmo con il codice C ++. Qui, ad esempio, è il mio primo tentativo di farlo e fa esplodere anche soluzioni R altamente ottimizzate.

Se dopo tutto questo ti rimangono ancora problemi, hai solo bisogno di più potenza di calcolo. Esamina la parallelizzazione ( http://cran.r-project.org/web/views/HighPerformanceComputing.html ) o anche soluzioni basate su GPU ( gpu-tools).

Collegamenti ad altre indicazioni


36

Se stai usando i forloop, molto probabilmente stai codificando R come se fosse C o Java o qualcos'altro. Il codice R opportunamente vettorializzato è estremamente veloce.

Prendiamo ad esempio questi due semplici bit di codice per generare un elenco di 10.000 numeri interi in sequenza:

Il primo esempio di codice è come si codificherebbe un loop usando un paradigma di codifica tradizionale. Ci vogliono 28 secondi per completare

system.time({
    a <- NULL
    for(i in 1:1e5)a[i] <- i
})
   user  system elapsed 
  28.36    0.07   28.61 

Puoi ottenere un miglioramento quasi 100 volte con la semplice azione di pre-allocazione della memoria:

system.time({
    a <- rep(1, 1e5)
    for(i in 1:1e5)a[i] <- i
})

   user  system elapsed 
   0.30    0.00    0.29 

Ma usando l'operazione vettoriale di base R usando l'operatore due punti :questa operazione è praticamente istantanea:

system.time(a <- 1:1e5)

   user  system elapsed 
      0       0       0 

+1 anche se considererei il tuo secondo esempio poco convincente in quanto a[i]non cambia. Ma system.time({a <- NULL; for(i in 1:1e5){a[i] <- 2*i} }); system.time({a <- 1:1e5; for(i in 1:1e5){a[i] <- 2*i} }); system.time({a <- NULL; a <- 2*(1:1e5)})ha un risultato simile.
Henry,

@ Henry, bel commento, ma come fai notare, i risultati sono gli stessi. Ho modificato l'esempio per inizializzare un a rep(1, 1e5)- i tempi sono identici.
Andrie,

17

Questo potrebbe essere reso molto più veloce saltando i loop utilizzando indici o ifelse()istruzioni nidificate .

idx <- 1:nrow(temp)
temp[,10] <- idx
idx1 <- c(FALSE, (temp[-nrow(temp),6] == temp[-1,6]) & (temp[-nrow(temp),3] == temp[-1,3]))
temp[idx1,10] <- temp[idx1,9] + temp[which(idx1)-1,10] 
temp[!idx1,10] <- temp[!idx1,9]    
temp[1,10] <- temp[1,9]
names(temp)[names(temp) == "V10"] <- "Kumm."

Grazie per la risposta. Cerco di capire le tue dichiarazioni. La riga 4: "temp [idx1,10] <- temp [idx1,9] + temp [which (idx1) -1,10]" ha causato un errore perché la lunghezza dell'oggetto più lungo non è un multiplo della lunghezza del oggetto più corto. "temp [idx1,9] = num [1: 11496]" e "temp [which (idx1) -1,10] = int [1: 11494]" quindi mancano 2 righe.
Kay,

Se fornisci un campione di dati (usa dput () con poche righe), lo riparerò per te. A causa del quale () - 1 bit, gli indici sono disuguali. Ma dovresti vedere come funziona da qui: non è necessario alcun ciclo o applicazione; usa solo funzioni vettoriali.
Shane,

1
Wow! Ho appena cambiato un blocco funzione nidificato if..else e mappatamente, in una funzione nidificata ifelse e ho ottenuto una velocità 200x!
James,

Il tuo consiglio generale è giusto, ma nel codice ti sei perso di fatto, quel ivalore -th dipende da i-1-th quindi non possono essere impostati nel modo in cui lo fai (usando which()-1).
Marek,

8

Non mi piace riscrivere il codice ... Anche ovviamente ifelse e lapply sono opzioni migliori, ma a volte è difficile adattarlo.

Spesso uso data.frames come si usano elenchi come df$var[i]

Ecco un esempio inventato:

nrow=function(x){ ##required as I use nrow at times.
  if(class(x)=='list') {
    length(x[[names(x)[1]]])
  }else{
    base::nrow(x)
  }
}

system.time({
  d=data.frame(seq=1:10000,r=rnorm(10000))
  d$foo=d$r
  d$seq=1:5
  mark=NA
  for(i in 1:nrow(d)){
    if(d$seq[i]==1) mark=d$r[i]
    d$foo[i]=mark
  }
})

system.time({
  d=data.frame(seq=1:10000,r=rnorm(10000))
  d$foo=d$r
  d$seq=1:5
  d=as.list(d) #become a list
  mark=NA
  for(i in 1:nrow(d)){
    if(d$seq[i]==1) mark=d$r[i]
    d$foo[i]=mark
  }
  d=as.data.frame(d) #revert back to data.frame
})

versione data.frame:

   user  system elapsed 
   0.53    0.00    0.53

versione elenco:

   user  system elapsed 
   0.04    0.00    0.03 

17 volte più veloce per utilizzare un elenco di vettori rispetto a un data.frame.

Qualche commento sul perché internamente data.frames è così lento in questo senso? Si potrebbe pensare che funzionino come liste ...

Per un codice ancora più veloce, fai questo class(d)='list'invece di d=as.list(d)eclass(d)='data.frame'

system.time({
  d=data.frame(seq=1:10000,r=rnorm(10000))
  d$foo=d$r
  d$seq=1:5
  class(d)='list'
  mark=NA
  for(i in 1:nrow(d)){
    if(d$seq[i]==1) mark=d$r[i]
    d$foo[i]=mark
  }
  class(d)='data.frame'
})
head(d)

1
Probabilmente è grazie al sovraccarico di [<-.data.frame, che in qualche modo viene chiamato quando lo fai d$foo[i] = marke potrebbe finire per creare una nuova copia del vettore di tutto il data.frame su ogni <-modifica. Farebbe una domanda interessante su SO.
Frank,

2
@Frank It (i) deve garantire che l'oggetto modificato sia ancora un data.frame valido e (ii) afaik ne faccia almeno una copia, possibilmente più di una. Il sottoassegnazione dei frame di dati è noto per essere lento e se si osserva il codice sorgente lungo non è davvero sorprendente.
Roland

@Frank, @Roland: la df$var[i]notazione passa attraverso la stessa [<-.data.framefunzione? Ho notato che è piuttosto lungo davvero. In caso contrario, quale funzione utilizza?
Chris

@Chris credo che d$foo[i]=markvenga tradotto approssimativamente in d <- `$<-`(d, 'foo', `[<-`(d$foo, i, mark)), ma con un certo uso di variabili temporanee.
Tim Goodman,

7

Come ha detto Ari alla fine della sua risposta, i pacchetti Rcppe inlinerendono incredibilmente facile rendere le cose veloci. Ad esempio, prova questo inlinecodice (avviso: non testato):

body <- 'Rcpp::NumericMatrix nm(temp);
         int nrtemp = Rccp::as<int>(nrt);
         for (int i = 0; i < nrtemp; ++i) {
             temp(i, 9) = i
             if (i > 1) {
                 if ((temp(i, 5) == temp(i - 1, 5) && temp(i, 2) == temp(i - 1, 2) {
                     temp(i, 9) = temp(i, 8) + temp(i - 1, 9)
                 } else {
                     temp(i, 9) = temp(i, 8)
                 }
             } else {
                 temp(i, 9) = temp(i, 8)
             }
         return Rcpp::wrap(nm);
        '

settings <- getPlugin("Rcpp")
# settings$env$PKG_CXXFLAGS <- paste("-I", getwd(), sep="") if you want to inc files in wd
dayloop <- cxxfunction(signature(nrt="numeric", temp="numeric"), body-body,
    plugin="Rcpp", settings=settings, cppargs="-I/usr/include")

dayloop2 <- function(temp) {
    # extract a numeric matrix from temp, put it in tmp
    nc <- ncol(temp)
    nm <- dayloop(nc, temp)
    names(temp)[names(temp) == "V10"] <- "Kumm."
    return(temp)
}

C'è una procedura simile per le #includecose, in cui si passa solo un parametro

inc <- '#include <header.h>

a cxxfunction, come include=inc. La cosa davvero interessante di questo è che fa tutto il collegamento e la compilazione per te, quindi la prototipazione è davvero veloce.

Disclaimer: non sono del tutto sicuro che la classe di tmp debba essere una matrice numerica e non numerica o qualcos'altro. Ma sono per lo più sicuro.

Modifica: se dopo hai ancora bisogno di più velocità, OpenMP è una struttura di parallelizzazione adatta C++. Non ho provato a usarlo da inline, ma dovrebbe funzionare. L'idea sarebbe, nel caso di nnuclei, hanno iterazione del ciclo keffettuato dal k % n. Un'introduzione adatto si trova in Matloff è The Art of R programmazione , disponibili qui , nel capitolo 16, Il ricorso al C .


3

Le risposte qui sono fantastiche. Un aspetto secondario non trattato è che la domanda afferma "Il mio PC funziona ancora (circa 10 ore adesso) e non ho idea del tempo di esecuzione ". Ho sempre inserito il seguente codice in loop durante lo sviluppo per avere un'idea di come i cambiamenti sembrano influenzare la velocità e anche per monitorare il tempo necessario per il completamento.

dayloop2 <- function(temp){
  for (i in 1:nrow(temp)){
    cat(round(i/nrow(temp)*100,2),"%    \r") # prints the percentage complete in realtime.
    # do stuff
  }
  return(blah)
}

Funziona anche con lapponi.

dayloop2 <- function(temp){
  temp <- lapply(1:nrow(temp), function(i) {
    cat(round(i/nrow(temp)*100,2),"%    \r")
    #do stuff
  })
  return(temp)
}

Se la funzione all'interno del loop è piuttosto veloce ma il numero di loop è elevato, prendere in considerazione solo la stampa ogni tanto poiché la stampa sulla console stessa ha un sovraccarico. per esempio

dayloop2 <- function(temp){
  for (i in 1:nrow(temp)){
    if(i %% 100 == 0) cat(round(i/nrow(temp)*100,2),"%    \r") # prints every 100 times through the loop
    # do stuff
  }
  return(temp)
}

Un'opzione simile, stampa la frazione i / n. Ho sempre qualcosa di simile cat(sprintf("\nNow running... %40s, %s/%s \n", nm[i], i, n))dato che di solito cerco cose con nome (con nomi dentro nm).
Frank,

2

In R, puoi spesso velocizzare l'elaborazione del loop usando le applyfunzioni della famiglia (nel tuo caso, probabilmente lo sarebbe replicate). Dai un'occhiata al plyrpacchetto che fornisce barre di avanzamento.

Un'altra opzione è quella di evitare del tutto i loop e sostituirli con aritmetica vettoriale. Non sono sicuro esattamente cosa stai facendo, ma probabilmente puoi applicare la tua funzione a tutte le righe contemporaneamente:

temp[1:nrow(temp), 10] <- temp[1:nrow(temp), 9] + temp[0:(nrow(temp)-1), 10]

Questo sarà molto più veloce e quindi puoi filtrare le righe in base alle tue condizioni:

cond.i <- (temp[i, 6] == temp[i-1, 6]) & (temp[i, 3] == temp[i-1, 3])
temp[cond.i, 10] <- temp[cond.i, 9]

L'aritmetica vettorializzata richiede più tempo e riflessione sul problema, ma a volte è possibile salvare diversi ordini di grandezza nei tempi di esecuzione.


14
sei a posto sul fatto che le funzioni vettoriali saranno più veloci dei loop o apply () ma non è vero che apply () sia più veloce dei loop. In molti casi si applica () è semplicemente astrarre il loop dall'utente ma continuando a eseguire il loop. Vedi questa domanda precedente: stackoverflow.com/questions/2275896/…
JD Long

0

L'elaborazione con data.tableè un'opzione praticabile:

n <- 1000000
df <- as.data.frame(matrix(sample(1:10, n*9, TRUE), n, 9))
colnames(df) <- paste("col", 1:9, sep = "")

library(data.table)

dayloop2.dt <- function(df) {
  dt <- data.table(df)
  dt[, Kumm. := {
    res <- .I;
    ifelse (res > 1,             
      ifelse ((col6 == shift(col6, fill = 0)) & (col3 == shift(col3, fill = 0)) , 
        res <- col9 + shift(res)                   
      , # else
        res <- col9                                 
      )
     , # else
      res <- col9
    )
  }
  ,]
  res <- data.frame(dt)
  return (res)
}

res <- dayloop2.dt(df)

m <- microbenchmark(dayloop2.dt(df), times = 100)
#Unit: milliseconds
#       expr      min        lq     mean   median       uq      max neval
#dayloop2.dt(df) 436.4467 441.02076 578.7126 503.9874 575.9534 966.1042    10

Se si ignorano i possibili guadagni dal filtro delle condizioni, è molto veloce. Ovviamente, se puoi fare il calcolo sul sottoinsieme di dati, aiuta.


2
Perché stai ripetendo il suggerimento di utilizzare data.table? È già stato fatto più volte nelle risposte precedenti.
IRTFM,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.