Accélérer le fonctionnement de la boucle en R

J’ai un gros problème de performance dans R. J’ai écrit une fonction qui itère sur un object data.frame . Il ajoute simplement une nouvelle colonne à un data.frame et accumule quelque chose. (opération simple). Le data.frame contient environ 850K 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]  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?

    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 ta fonction, changer d’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 vectorielles qui recueillent des 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 lance chaque fonction pour data.frame 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

    performance

    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 lm simple lm que pour 850 000 lignes, le calcul prend 6 minutes et 10 secondes.

    Puissance de vectorisation

    Comme Shane et Calimo l’affirment dans leurs réponses, la vectorisation est la clé d’une meilleure performance. 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 pour cette fonction, cette fois pour 10 000 à 100 000 par 10 000.

    performance

    Réglage de l'accord

    Un autre ajustement consiste à changer le temp[i,9] indexation de la boucle temp[i,9] en res[i] (qui sont exactement les mêmes dans l'itération en i-ème boucle). C'est encore la différence entre l'indexation d'un vecteur et l'indexation d'un data.frame .
    Deuxième chose: lorsque vous regardez sur la boucle, vous pouvez voir qu'il n'est pas nécessaire de faire une boucle sur tous les 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 dépendent fortement 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 du temps de calcul pour 850 000 lignes en dessous de la seconde.

    performance

    Je veux que tu puisses 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 la boucle en vectorisation, 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 .

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

    Tout d’abord, déterminer où se trouve la partie lente. Il n’est pas nécessaire d’optimiser le code qui ne fonctionne pas lentement. Pour de petites quantités de code, il suffit de penser simplement. 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 ne devraient être exécutés qu’une seule fois si possible, donc:

    • Stockez les résultats et accédez-y plutôt que de recalculer à plusieurs resockets
    • Exclure des calculs non dépendants de la boucle
    • Évitez les calculs qui ne sont pas nécessaires (par exemple, n’utilisez pas les expressions régulières avec des recherches fixes )

    L’ utilisation de fonctions plus 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 plus prononcés. mean est particulièrement lente .

    Vous pouvez alors éviter certains problèmes particulièrement courants :

    • cbind va vous ralentir très vite.
    • Initialisez vos structures de données, puis remplissez-les plutôt que de les développer à chaque fois .
    • Même avec une pré-affectation, vous pouvez passer à une approche de référence par référence plutôt qu’à une approche par référence, mais cela ne vaut peut-être pas la peine.
    • Jetez un coup d’œil au R Inferno pour plus de pièges à éviter.

    Essayez de mieux vectoriser , ce qui peut souvent mais pas toujours aider. À cet égard, les commandes vectorisées de manière inhérente, telles que ifelse , diff et autres, apporteront davantage d’améliorations que la famille de commandes apply (qui fournit peu ou pas de gain 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 lors de la lecture de données textuelles . Les gains de vitesse seront variables en fonction de la quantité de devinettes que vous éliminerez.

    Ensuite, considérez les packages optimisés : Le package data.table peut générer des gains de vitesse importants lorsque son utilisation est possible, dans la manipulation des données et dans 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 :

    • Comstackz 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 d’utiliser un BLAS optimisé. Ceux-ci fournissent des gains de vitesse généraux. Honnêtement, il est dommage que R n’utilise pas automatiquement la bibliothèque la plus efficace lors de l’installation. Si tout va bien, Revolution R consortingbuera au travail qu’ils ont fait ici dans toute la communauté.
    • Radford Neal a fait un tas d’optimisations, dont certaines ont été adoptées dans R Core, et de nombreuses autres dans PQR .

    Et enfin, si tout ce qui précède ne vous permet toujours pas d’atteindre le niveau requirejs, vous devrez peut-être passer à un langage plus rapide pour l’extrait de code lent . La combinaison de Rcpp et d’ inline rend ici le remplacement de la partie la plus lente de l’algorithme par le code C ++ particulièrement facile. C’est la première fois que j’essaie 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. Examinez 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 orientations

    Si vous utilisez for boucles for , vous êtes probablement en train de coder R comme si c’était 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 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 la simple action de pré-allocation 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 avec l’opérateur côlon : cette opération est quasi instantanée:

     system.time(a <- 1:1e5) user system elapsed 0 0 0 

    Cela pourrait être rendu beaucoup plus rapide en ignorant les boucles en utilisant des index ou des ifelse() nestedes.

     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 en inline facilitent incroyablement la tâche. Par exemple, essayez ce code en inline (avertissement: non testé):

     body <- 'Rcpp::NumericMatrix nm(temp); int nrtemp = Rccp::as(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 choses, où il suffit de passer un paramètre

     inc <- '#include  

    à cxxfunction, 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 tout à fait sûr que la classe de tmp devrait être numérique et non une masortingce numérique ou autre chose. Mais je suis surtout sûr.

    Edit: si vous avez toujours besoin de plus de rapidité après cela, OpenMP est une fonction de parallélisation qui convient à C++ . Je n'ai pas essayé de l'utiliser en inline , mais ça devrait marcher. L'idée serait, dans le cas de n cores, d'avoir l'itération de boucle k effectuée par k % n . Une introduction appropriée se trouve dans The Art of R Programming de Matloff, disponible ici , au chapitre 16, Recours à C.

    Je n’aime pas réécrire le code … Bien sûr, si et moins et les lapply sont de meilleures options, mais parfois il est difficile de faire ce choix.

    J’utilise souvent data.frames comme on utiliserait 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 de data.frame:

      user system elapsed 0.53 0.00 0.53 

    version de liste:

      user system elapsed 0.04 0.00 0.03 

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

    Des commentaires sur pourquoi en interne data.frames sont-ils 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) 

    Les réponses ici sont géniales. Un aspect mineur non couvert est que la question dit ” Mon PC fonctionne toujours (environ 10h maintenant) et je n’ai aucune idée de l’exécution “. Lors du développement, je mets toujours le code suivant en boucle pour avoir une idée de la façon dont les modifications semblent affecter la vitesse et également du temps nécessaire à sa réalisation.

     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 bien avec les 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 important, pensez à imprimer de temps en temps, car l'impression sur la console elle-même a un coût. 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) } 

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

    Une autre option consiste à éviter 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] 

    Cela sera beaucoup plus rapide et vous pourrez ensuite 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 reflection sur le problème, mais vous pouvez parfois économiser plusieurs ordres de grandeur en temps d'exécution.

    Le 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-dataset, cela aide.