web-dev-qa-db-fra.com

Accélérer le fonctionnement en boucle dans R

J'ai un gros problème de performance dans R. J'ai écrit une fonction qui itère sur un data.frame objet. Il ajoute simplement une nouvelle colonne à un data.frame et accumule quelque chose. (opération simple). Le data.frame a environ 850K lignes. Mon PC fonctionne toujours (environ 10h maintenant) et je n'ai aucune idée du temps d'exécution.

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)
}

Des idées pour accélérer cette opération?

181
Kay

Le plus gros problème et la racine de l'inefficacité est l'indexation de data.frame, je veux dire toutes les lignes où vous utilisez temp[,].
Essayez d'éviter cela autant que possible. J'ai pris votre fonction, changez l'indexation et voici version_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)
}

Comme vous pouvez le voir, je crée le vecteur res qui regroupe les résultats. À la fin, je l’ajoute à data.frame Et je n’ai pas besoin de jouer avec les noms. Alors, comment va mieux?

J'exécute chaque fonction pour data.frame Avec nrow de 1 000 à 10 000 par 1 000 et mesure le temps avec system.time

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

Le résultat est

performance

Vous pouvez voir que votre version dépend de façon exponentielle de nrow(X). La version modifiée a une relation linéaire et le modèle simple lm prévoit que, pour 850 000 lignes, le calcul prend 6 minutes et 10 secondes.

Puissance de la vectorisation

Comme Shane et Calimo l'affirment dans leurs réponses, la vectorisation est la clé d'une meilleure performance. A partir de votre code, vous pouvez sortir de la boucle:

  • conditionnement
  • initialisation des résultats (qui sont temp[i,9])

Cela conduit à ce code

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)
}

Comparez le résultat pour cette fonction, cette fois pour nrow de 10 000 à 100 000 par 10 000.

performance

Réglage du réglage

Un autre Tweak consiste à changer dans une boucle l'indexation temp[i,9] En res[i] (Qui sont exactement les mêmes dans la i-ème itération de la boucle). C'est encore la différence entre indexer un vecteur et indexer un data.frame.
Deuxième chose: quand vous regardez sur la boucle, vous voyez qu’il n’est pas nécessaire de passer en boucle sur tous les i, mais uniquement sur ceux qui correspondent à la condition.
Alors on y va

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)
}

Les performances que vous gagnez dépendent fortement d'une structure de données. Précisément - sur le pourcentage de TRUE valeurs dans la condition. Pour mes données simulées, il faut un temps de calcul de 850 000 lignes en dessous de la seconde.

performance

Si vous voulez, vous pouvez aller plus loin, je vois au moins deux choses qui peuvent être faites:

  • écrire un code C pour faire du cumsum conditionnel
  • si vous savez que dans votre séquence de données max n'est pas grande, vous pouvez changer de boucle en vectorisée tandis que, quelque chose comme

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

Le code utilisé pour les simulations et les figures est disponible sur GitHub .

419
Marek

Stratégies générales pour accélérer le code R

D'abord, déterminez la partie lente est vraiment. Il n'est pas nécessaire d'optimiser le code qui ne s'exécute pas lentement. Pour de petites quantités de code, une simple réflexion à travers cela peut fonctionner. Si cela échoue, RProf et des outils de profilage similaires peuvent être utiles.

Une fois que vous avez trouvé le goulot d'étranglement, pensez à des algorithmes plus efficaces pour faire ce que vous voulez. Les calculs doivent être exécutés une fois si possible, ainsi:

Utiliser plus fonctions efficaces peut produire des gains de vitesse modérés ou importants. Par exemple, paste0 Produit un petit gain d'efficacité, mais .colSums() et ses parents produisent des gains un peu plus prononcés. mean est particulièrement lent .

Ensuite, vous pouvez éviter certains problèmes courants:

  • cbind vous ralentira très rapidement.
  • Initialisez vos structures de données, puis complétez-les, plutôt que de les développer à chaque fois .
  • Même avec la pré-allocation, vous pouvez passer à une approche de référence, plutôt que de passer d'une valeur à une autre, mais cela ne vaut peut-être pas la peine.
  • Jetez un coup d'oeil au R Inferno pour éviter plus de pièges à éviter.

Essayez de mieux vectorisation, ce qui peut souvent mais pas toujours aider. À cet égard, les commandes vectorisées intrinsèquement telles que ifelse, diff, etc., apporteront davantage d’améliorations que la famille de commandes apply (qui fournissent peu, voire aucune accélération de la vitesse boucle bien écrite).

Vous pouvez également essayer de fournir plus d'informations aux fonctions R. Par exemple, utilisez vapply plutôt que sapply et spécifiez colClasses lors de la lecture de données textuelles . Les gains de vitesse seront variables en fonction du nombre de devinages que vous éliminez.

Ensuite, considérons paquets optimisés: Le paquet data.table peut générer d’énormes gains de vitesse lorsque son utilisation est possible, lors de la manipulation de données et de la lecture de grandes quantités de données. données (fread).

Ensuite, essayez de gagner de la vitesse avec un moyen plus efficace d’appeler R:

  • Compilez votre script R. Ou utilisez les packages Ra et jit de concert pour une compilation juste à temps (Dirk a un exemple dans cette présentation ).
  • Assurez-vous que vous utilisez un BLAS optimisé. Ceux-ci fournissent des gains de vitesse à tous les niveaux. Honnêtement, il est dommage que R n’utilise pas automatiquement la bibliothèque la plus efficace à l’installation. Espérons que Revolution R contribuera le travail qu’ils ont réalisé ici à la communauté dans son ensemble.
  • Radford Neal a effectué de nombreuses optimisations, dont certaines ont été adoptées dans R Core et de nombreuses autres qui ont été intégrées dans pqR .

Enfin, si tout ce qui précède ne vous permet toujours pas d'obtenir la réponse aussi rapide que vous le souhaitez, vous devrez peut-être passer à un langue plus rapide pour le fragment de code lent. La combinaison de Rcpp et inline rend ici particulièrement simple le remplacement de la partie la plus lente de l’algorithme par du code C++. Voici, par exemple, ma première tentative , et les solutions R, même très optimisées, s'envolent.

Si vous avez toujours des problèmes après tout cela, vous avez simplement besoin de plus de puissance de calcul. Regardez dans la parallélisation ( http://cran.r-project.org/web/views/HighPerformanceComputing.html ) ou même des solutions basées sur un GPU (gpu-tools).

Liens vers d'autres conseils

133
Ari B. Friedman

Si vous utilisez des boucles for, vous coderez probablement R comme s'il s'agissait de C ou Java ou autre chose. Le code R correctement vectorisé est extrêmement rapide.

Prenons par exemple ces deux bits de code simples pour générer une liste de 10 000 entiers dans l'ordre:

Le premier exemple de code montre comment coder une boucle en utilisant un paradigme de codage traditionnel. Il faut 28 secondes pour terminer

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

Vous pouvez obtenir une amélioration près de 100 fois par la simple action de préallocation de mémoire:

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

   user  system elapsed 
   0.30    0.00    0.29 

Mais en utilisant l’opération vectorielle de base R en utilisant l’opérateur deux-points : cette opération est quasi instantanée:

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

   user  system elapsed 
      0       0       0 
34
Andrie

Cela pourrait être fait beaucoup plus rapidement en sautant les boucles en utilisant des index ou des instructions imbriquées ifelse().

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."
17
Shane

Comme Ari a mentionné à la fin de sa réponse, les packages Rcpp et inline facilitent énormément la tâche pour accélérer les choses. Par exemple, essayez ceci inline code (avertissement: non testé):

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)
}

Il existe une procédure similaire pour #includeing choses, où vous venez de passer un paramètre

inc <- '#include <header.h>

à cxxfunction, comme include=inc. Ce qui est vraiment génial, c’est qu’il fait tous les liens et la compilation pour vous, le prototypage est donc très rapide.

Clause de non-responsabilité: Je ne suis pas tout à fait sûr que la classe de tmp soit numérique et non pas une matrice numérique ou autre chose. Mais je suis surtout sûr.

Edit: si vous avez encore besoin de plus de vitesse après cela, OpenMP est un utilitaire de parallélisation bon pour C++. Je n'ai pas essayé de l'utiliser depuis inline, mais cela devrait fonctionner. L'idée serait, dans le cas de n cœurs, que l'itération de boucle k soit effectuée par k % n. Une introduction appropriée se trouve dans Matloff The Art of R Programming, disponible ici , au chapitre 16, Recours à C .

7
jclancy

Je n'aime pas réécrire le code ... Bien sûr, si ifelse et lapply sont de meilleures options, il est parfois difficile de le faire.

J'utilise fréquemment data.frames comme des listes telles que df$var[i]

Voici un exemple composé:

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
})

version data.frame:

   user  system elapsed 
   0.53    0.00    0.53

version de la liste:

   user  system elapsed 
   0.04    0.00    0.03 

17 fois plus rapide à utiliser une liste de vecteurs qu'un data.frame.

Des commentaires sur la raison pour laquelle les data.frames internes sont si lents à cet égard? On pourrait penser qu'ils fonctionnent comme des listes ...

Pour un code encore plus rapide, faites ceci class(d)='list' au lieu de d=as.list(d) et class(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)
7
Chris

Les réponses ici sont super. Un aspect mineur non couvert est que la question dit "Mon PC fonctionne toujours (environ 10h maintenant) et je n'ai aucune idée du temps d'exécution". Je mets toujours le code suivant en boucle lorsque je développe pour comprendre comment les changements semblent affecter la vitesse et aussi pour surveiller le temps que cela prendra.

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)
}

Fonctionne aussi avec lapply.

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

Si la fonction dans la boucle est assez rapide mais que le nombre de boucles est grand, envisagez simplement d'imprimer de temps en temps car l'impression sur la console elle-même entraîne une surcharge. par exemple.

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)
}
3
rookie

En R, vous pouvez souvent accélérer le traitement de la boucle en utilisant les fonctions de la famille apply (dans votre cas, ce serait probablement replicate). Jetez un coup d'œil au paquetage plyr qui fournit des barres de progression.

Une autre option consiste à éviter complètement les boucles et à les remplacer par des arithmétiques vectorisées. Je ne sais pas exactement ce que vous faites, mais vous pouvez probablement appliquer votre fonction à toutes les lignes à la fois:

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

Ce sera beaucoup plus rapide et vous pourrez alors filtrer les lignes avec votre condition:

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'arithmétique vectorisée nécessite plus de temps et de réflexion sur le problème, mais vous pouvez parfois économiser plusieurs ordres de grandeur en temps d'exécution.

2
Calimo

Traitement avec data.table est une option viable:

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

Si vous ignorez les gains possibles du filtrage des conditions, c'est très rapide. Évidemment, si vous pouvez faire le calcul sur le sous-ensemble de données, cela vous aidera.

0
Bulat