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?
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
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.
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:
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.
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.
Si vous voulez, vous pouvez aller plus loin, je vois au moins deux choses qui peuvent être faites:
C
pour faire du cumsum conditionnelsi 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 .
Stratégies générales pour accélérer le code R
D'abord, déterminez où 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.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:
Ra
et jit
de concert pour une compilation juste à temps (Dirk a un exemple dans cette présentation ).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
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
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."
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 #include
ing 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 .
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)
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)
}
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.
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.