performance - while - fonction apply r




Accélérer l'opération de la boucle dans R (6)

J'ai un gros problème de performance dans R. J'ai écrit une fonction qui itère sur un objet data.frame . Il ajoute simplement une nouvelle colonne à un data.frame et accumule quelque chose. (opération simple). Le data.frame contient environ 850 000 lignes. Mon PC fonctionne toujours (environ 10h maintenant) et je n'ai aucune idée de l'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?


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

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

Comme Ari l'a mentionné à la fin de sa réponse, les Rcpp et inline rendent incroyablement facile de faire les choses rapidement. À titre d'exemple, essayez ce code en inline (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 y a une procédure similaire pour #include inclure des choses, où vous passez juste un paramètre

inc <- '#include <header.h>

à cxxfonction, comme include=inc . Ce qui est vraiment cool à propos de cela, c'est qu'il fait tout le lien et la compilation pour vous, donc le prototypage est vraiment rapide.

Disclaimer: Je ne suis pas totalement sûr que la classe de tmp devrait être une matrice numérique et non 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 une facilité de parallélisation pour C++ . Je n'ai pas essayé de l'utiliser en inline , mais ça devrait marcher. L'idée serait, dans le cas de n cœurs, que l'itération de boucle k soit réalisée par k % n . Une introduction appropriée se trouve dans l' Art of R Programming de Matloff, disponible here , au chapitre 16, Resorting to C.


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

Souvent j'utilise data.frames comme on utiliserait des listes telles que df$var[i]

Voici un exemple inventé:

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 de data.frame:

   user  system elapsed 
   0.53    0.00    0.53

version de la liste:

   user  system elapsed 
   0.04    0.00    0.03 

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

Des commentaires sur les raisons pour lesquelles les fichiers de données internes sont si lents à cet égard? On pourrait penser qu'ils fonctionnent comme des listes ...

Pour un code encore plus rapide, faites cette 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)

Le plus gros problème et la racine de l'inefficacité est l'indexation de data.frame, je veux dire toutes ces lignes où vous utilisez temp[,] .
Essayez d'éviter cela autant que possible. J'ai pris votre fonction, changez l'indexation et voici la 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 des res qui rassemblent les résultats. A la fin, je l'ajoute à data.frame et je n'ai pas besoin de jouer avec les noms. Alors, comment ça va?

Je cours chaque fonction pour data.frame avec nrow de 1000 à 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

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

Pouvoir de vectorisation

Comme l'indiquent Shane et Calimo dans leurs réponses, la vectorisation est la clé d'une meilleure performance. A partir de votre code, vous pouvez vous déplacer en dehors 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 de cette fonction, cette fois- nrow de 10 000 à 100 000 par 10 000.

Accorder l'accordé

Un autre tweak consiste à changer dans une boucle d'indexation temp[i,9] à res[i] (qui sont exactement les mêmes dans l'itération de la i-ième boucle). C'est encore une différence entre indexer un vecteur et indexer un data.frame .
Deuxième chose: quand vous regardez sur la boucle, vous pouvez voir qu'il n'y a pas besoin de boucler tout ce que i , mais seulement pour 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 fortement dépendent d'une structure de données. Précisément - sur le pourcentage de valeurs TRUE dans la condition. Pour mes données simulées, il faut compter 850 000 lignes en dessous de la seconde.

Je veux que vous puissiez 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 à vectorisé, 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 .


Si vous utilisez for boucles, vous coderez probablement R comme s'il s'agissait de C ou de Java ou d'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 en séquence:

Le premier exemple de code est 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 presque 100 fois par l'action simple de pré-allouer de la 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 du côlon : cette opération est quasi instantanée:

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

   user  system elapsed 
      0       0       0 

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, il suffit de réfléchir. Si cela échoue, RProf et d'autres 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 ne devraient être exécutés qu'une seule fois si possible, donc:

L' utilisation de fonctions plus efficaces peut produire des gains de vitesse modérés ou importants. Par exemple, paste0 produit un gain d'efficacité faible mais .colSums() et ses parentés produisent des gains un peu plus prononcés. mean est particulièrement lente .

Ensuite, vous pouvez éviter certains problèmes particulièrement communs :

  • cbind va vous ralentir très rapidement.
  • Initialisez vos structures de données, puis remplissez-les plutôt que de les développer à chaque fois .
  • Même avec la pré-allocation, vous pouvez passer à une approche par référence plutôt qu'à une approche par valeur, mais cela ne vaut peut-être pas la peine.
  • Jetez un oeil à la R Inferno pour plus d'embûches à éviter.

Essayez de mieux vectoriser , ce qui peut souvent, mais pas toujours, aider. À cet égard, les commandes vectorisées telles que ifelse , diff et similaires fourniront plus d'améliorations que la famille de commandes apply (qui fournissent peu ou pas de boost de vitesse sur une 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 lorsque vous lisez des données textuelles . Les gains de vitesse seront variables en fonction du nombre de devinettes éliminées.

Ensuite, considérons les packages optimisés : Le package data.table peut générer des gains de vitesse massifs lorsque son utilisation est possible, dans la manipulation des données et lors de la lecture de grandes quantités de données ( fread ).

Ensuite, essayez d'obtenir des gains de vitesse grâce à des moyens plus efficaces d'appeler R :

  • Compilez votre script R. Ou utilisez les paquets Ra et jit en concert pour la compilation juste-à-temps (Dirk en 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 librairie la plus efficace à l'installation. J'espère que Revolution R contribuera le travail qu'ils ont fait ici à la communauté en général.
  • Radford Neal a fait un tas d'optimisations, dont certaines ont été adoptées dans R Core, et beaucoup d'autres qui ont été pqR dans pqR .

Et enfin, si tout ce qui précède ne vous parvient toujours pas aussi vite que vous le souhaitez, vous devrez peut-être passer à une langue plus rapide pour l'extrait de code lent . La combinaison de Rcpp et inline fait que remplacer seulement la partie la plus lente de l'algorithme par du code C ++ est particulièrement facile. Ici, par exemple, c'est ma première tentative de le faire , et cela élimine même les solutions R hautement optimisées.

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

Liens vers d'autres conseils





r-faq