Réponses:
Les apply
fonctions de R ne fournissent pas de performances améliorées par rapport aux autres fonctions de bouclage (par exemple for
). Une exception à cela est ce lapply
qui peut être un peu plus rapide car cela fonctionne plus en code C qu'en R (voir cette question pour un exemple de ceci ).
Mais en général, la règle est que vous devez utiliser une fonction Apply pour plus de clarté, pas pour les performances .
J'ajouterais à cela que les fonctions apply n'ont aucun effet secondaire , ce qui est une distinction importante en matière de programmation fonctionnelle avec R. Cela peut être remplacé en utilisant assign
ou <<-
, mais cela peut être très dangereux. Les effets secondaires rendent également un programme plus difficile à comprendre car l'état d'une variable dépend de l'historique.
Éditer:
Juste pour souligner cela avec un exemple trivial qui calcule récursivement la séquence de Fibonacci; cela peut être exécuté plusieurs fois pour obtenir une mesure précise, mais le fait est qu'aucune des méthodes n'a des performances significativement différentes:
> fibo <- function(n) {
+ if ( n < 2 ) n
+ else fibo(n-1) + fibo(n-2)
+ }
> system.time(for(i in 0:26) fibo(i))
user system elapsed
7.48 0.00 7.52
> system.time(sapply(0:26, fibo))
user system elapsed
7.50 0.00 7.54
> system.time(lapply(0:26, fibo))
user system elapsed
7.48 0.04 7.54
> library(plyr)
> system.time(ldply(0:26, fibo))
user system elapsed
7.52 0.00 7.58
Modifier 2:
Concernant l'utilisation des packages parallèles pour R (par exemple rpvm, rmpi, snow), ceux-ci fournissent généralement apply
des fonctions familiales (même le foreach
package est essentiellement équivalent, malgré le nom). Voici un exemple simple de la sapply
fonction dans snow
:
library(snow)
cl <- makeSOCKcluster(c("localhost","localhost"))
parSapply(cl, 1:20, get("+"), 3)
Cet exemple utilise un cluster de sockets, pour lequel aucun logiciel supplémentaire ne doit être installé; sinon vous aurez besoin de quelque chose comme PVM ou MPI (voir la page de clustering de Tierney ). snow
a les fonctions d'application suivantes:
parLapply(cl, x, fun, ...)
parSapply(cl, X, FUN, ..., simplify = TRUE, USE.NAMES = TRUE)
parApply(cl, X, MARGIN, FUN, ...)
parRapply(cl, x, fun, ...)
parCapply(cl, x, fun, ...)
Il est logique que les apply
fonctions soient utilisées pour une exécution parallèle car elles n'ont aucun effet secondaire . Lorsque vous modifiez une valeur de variable dans une for
boucle, elle est définie globalement. D'autre part, toutes les apply
fonctions peuvent être utilisées en parallèle en toute sécurité car les modifications sont locales à l'appel de fonction (à moins que vous n'essayiez d'utiliser assign
ou <<-
, auquel cas vous pouvez introduire des effets secondaires). Inutile de dire qu'il est essentiel de faire attention aux variables locales et globales, en particulier lors de l'exécution parallèle.
Éditer:
Voici un exemple trivial pour démontrer la différence entre for
et en *apply
ce qui concerne les effets secondaires:
> df <- 1:10
> # *apply example
> lapply(2:3, function(i) df <- df * i)
> df
[1] 1 2 3 4 5 6 7 8 9 10
> # for loop example
> for(i in 2:3) df <- df * i
> df
[1] 6 12 18 24 30 36 42 48 54 60
Notez comment le df
dans l'environnement parent est modifié par for
mais pas *apply
.
snowfall
paquet et d'essayer les exemples dans leur vignette. snowfall
s'appuie sur le snow
paquet et fait abstraction des détails de la parallélisation, ce qui rend plus simple l'exécution de apply
fonctions parallélisées .
foreach
depuis, il est devenu disponible et semble être très demandé sur SO.
lapply
est "un peu plus rapide" qu'une for
boucle. Cependant, là, je ne vois rien qui le suggère. Vous mentionnez seulement que lapply
c'est plus rapide que sapply
, ce qui est un fait bien connu pour d'autres raisons ( sapply
essaie de simplifier la sortie et doit donc faire beaucoup de vérification de la taille des données et des conversions potentielles). Rien de lié à for
. Est-ce que je manque quelque chose?
Parfois, l'accélération peut être importante, comme lorsque vous devez imbriquer des boucles for pour obtenir la moyenne basée sur un regroupement de plusieurs facteurs. Ici, vous avez deux approches qui vous donnent exactement le même résultat:
set.seed(1) #for reproducability of the results
# The data
X <- rnorm(100000)
Y <- as.factor(sample(letters[1:5],100000,replace=T))
Z <- as.factor(sample(letters[1:10],100000,replace=T))
# the function forloop that averages X over every combination of Y and Z
forloop <- function(x,y,z){
# These ones are for optimization, so the functions
#levels() and length() don't have to be called more than once.
ylev <- levels(y)
zlev <- levels(z)
n <- length(ylev)
p <- length(zlev)
out <- matrix(NA,ncol=p,nrow=n)
for(i in 1:n){
for(j in 1:p){
out[i,j] <- (mean(x[y==ylev[i] & z==zlev[j]]))
}
}
rownames(out) <- ylev
colnames(out) <- zlev
return(out)
}
# Used on the generated data
forloop(X,Y,Z)
# The same using tapply
tapply(X,list(Y,Z),mean)
Les deux donnent exactement le même résultat, étant une matrice 5 x 10 avec les moyennes et les lignes et colonnes nommées. Mais :
> system.time(forloop(X,Y,Z))
user system elapsed
0.94 0.02 0.95
> system.time(tapply(X,list(Y,Z),mean))
user system elapsed
0.06 0.00 0.06
Voilà. Qu'est-ce que j'ai gagné? ;-)
*apply
est parfois plus rapide. Mais je pense que le point le plus important est les effets secondaires (mis à jour ma réponse avec un exemple).
data.table
c'est encore plus rapide et je pense "plus facile". library(data.table)
dt<-data.table(X,Y,Z,key=c("Y,Z"))
system.time(dt[,list(X_mean=mean(X)),by=c("Y,Z")])
tapply
est une fonction spécialisée pour une tâche spécifique, c'est pourquoi elle est plus rapide qu'une boucle for. Il ne peut pas faire ce que peut faire une boucle for (alors que les standards le apply
peuvent). Vous comparez des pommes avec des oranges.
... et comme je viens de l'écrire ailleurs, vapply est votre ami! ... c'est comme sapply, mais vous spécifiez également le type de valeur de retour qui le rend beaucoup plus rapide.
foo <- function(x) x+1
y <- numeric(1e6)
system.time({z <- numeric(1e6); for(i in y) z[i] <- foo(i)})
# user system elapsed
# 3.54 0.00 3.53
system.time(z <- lapply(y, foo))
# user system elapsed
# 2.89 0.00 2.91
system.time(z <- vapply(y, foo, numeric(1)))
# user system elapsed
# 1.35 0.00 1.36
Mise à jour du 1er janvier 2020:
system.time({z1 <- numeric(1e6); for(i in seq_along(y)) z1[i] <- foo(y[i])})
# user system elapsed
# 0.52 0.00 0.53
system.time(z <- lapply(y, foo))
# user system elapsed
# 0.72 0.00 0.72
system.time(z3 <- vapply(y, foo, numeric(1)))
# user system elapsed
# 0.7 0.0 0.7
identical(z1, z3)
# [1] TRUE
for
les boucles sont plus rapides sur mon ordinateur Windows 10 à 2 cœurs. Je l'ai fait avec des 5e6
éléments - une boucle était de 2,9 secondes contre 3,1 secondes pour vapply
.
J'ai écrit ailleurs qu'un exemple comme celui de Shane ne met pas vraiment l'accent sur la différence de performance entre les différents types de syntaxe de boucle car le temps est entièrement passé dans la fonction plutôt que de stresser réellement la boucle. De plus, le code compare injustement une boucle for sans mémoire avec des fonctions de famille apply qui renvoient une valeur. Voici un exemple légèrement différent qui met l'accent sur ce point.
foo <- function(x) {
x <- x+1
}
y <- numeric(1e6)
system.time({z <- numeric(1e6); for(i in y) z[i] <- foo(i)})
# user system elapsed
# 4.967 0.049 7.293
system.time(z <- sapply(y, foo))
# user system elapsed
# 5.256 0.134 7.965
system.time(z <- lapply(y, foo))
# user system elapsed
# 2.179 0.126 3.301
Si vous prévoyez d'enregistrer le résultat, appliquer des fonctions familiales peut être bien plus que du sucre syntaxique.
(la simple désinscription de z n'est que de 0,2 s, donc le lapply est beaucoup plus rapide. L'initialisation du z dans la boucle for est assez rapide car je donne la moyenne des 5 dernières des 6 exécutions de manière à ce que le déplacement en dehors du système soit n'affecte guère les choses)
Une autre chose à noter cependant est qu'il existe une autre raison d'utiliser les fonctions familiales d'application indépendamment de leurs performances, de leur clarté ou de l'absence d'effets secondaires. Une for
boucle favorise généralement la mise autant que possible dans la boucle. En effet, chaque boucle nécessite la configuration de variables pour stocker des informations (entre autres opérations possibles). Les déclarations Apply ont tendance à être biaisées dans l'autre sens. Souvent, vous souhaitez effectuer plusieurs opérations sur vos données, dont plusieurs peuvent être vectorisées mais certaines peuvent ne pas l'être. Dans R, contrairement aux autres langages, il est préférable de séparer ces opérations et d'exécuter celles qui ne sont pas vectorisées dans une instruction apply (ou une version vectorisée de la fonction) et celles qui sont vectorisées comme de véritables opérations vectorielles. Cela accélère souvent considérablement les performances.
Prenant l'exemple de Joris Meys où il remplace une boucle for traditionnelle par une fonction R pratique, nous pouvons l'utiliser pour montrer l'efficacité de l'écriture de code d'une manière plus conviviale pour R pour une accélération similaire sans la fonction spécialisée.
set.seed(1) #for reproducability of the results
# The data - copied from Joris Meys answer
X <- rnorm(100000)
Y <- as.factor(sample(letters[1:5],100000,replace=T))
Z <- as.factor(sample(letters[1:10],100000,replace=T))
# an R way to generate tapply functionality that is fast and
# shows more general principles about fast R coding
YZ <- interaction(Y, Z)
XS <- split(X, YZ)
m <- vapply(XS, mean, numeric(1))
m <- matrix(m, nrow = length(levels(Y)))
rownames(m) <- levels(Y)
colnames(m) <- levels(Z)
m
Cela finit par être beaucoup plus rapide que la for
boucle et juste un peu plus lent que la tapply
fonction optimisée intégrée. Ce n'est pas parce que vapply
c'est tellement plus rapide que for
mais parce qu'il n'effectue qu'une seule opération à chaque itération de la boucle. Dans ce code, tout le reste est vectorisé. Dans la for
boucle traditionnelle de Joris Meys, de nombreuses opérations (7?) Se produisent à chaque itération et il y a pas mal de configuration juste pour qu'elle s'exécute. Notez également combien il est plus compact que la for
version.
2.798 0.003 2.803; 4.908 0.020 4.934; 1.498 0.025 1.528
, et vapply est encore mieux:1.19 0.00 1.19
sapply
50% plus lent for
et lapply
deux fois plus rapide.
y
sur 1:1e6
, non numeric(1e6)
(un vecteur de zéros). Essayer d'allouer foo(0)
à z[0]
plusieurs reprises n'illustre pas bien une for
utilisation de boucle typique . Le message est par ailleurs parfait.
Lors de l'application de fonctions sur des sous-ensembles d'un vecteur, cela tapply
peut être assez rapide qu'une boucle for. Exemple:
df <- data.frame(id = rep(letters[1:10], 100000),
value = rnorm(1000000))
f1 <- function(x)
tapply(x$value, x$id, sum)
f2 <- function(x){
res <- 0
for(i in seq_along(l <- unique(x$id)))
res[i] <- sum(x$value[x$id == l[i]])
names(res) <- l
res
}
library(microbenchmark)
> microbenchmark(f1(df), f2(df), times=100)
Unit: milliseconds
expr min lq median uq max neval
f1(df) 28.02612 28.28589 28.46822 29.20458 32.54656 100
f2(df) 38.02241 41.42277 41.80008 42.05954 45.94273 100
apply
, cependant, dans la plupart des cas, il ne fournit aucune augmentation de vitesse et, dans certains cas, peut être encore plus lent:
mat <- matrix(rnorm(1000000), nrow=1000)
f3 <- function(x)
apply(x, 2, sum)
f4 <- function(x){
res <- 0
for(i in 1:ncol(x))
res[i] <- sum(x[,i])
res
}
> microbenchmark(f3(mat), f4(mat), times=100)
Unit: milliseconds
expr min lq median uq max neval
f3(mat) 14.87594 15.44183 15.87897 17.93040 19.14975 100
f4(mat) 12.01614 12.19718 12.40003 15.00919 40.59100 100
Mais pour ces situations, nous avons colSums
et rowSums
:
f5 <- function(x)
colSums(x)
> microbenchmark(f5(mat), times=100)
Unit: milliseconds
expr min lq median uq max neval
f5(mat) 1.362388 1.405203 1.413702 1.434388 1.992909 100
microbenchmark
c'est beaucoup plus précis que system.time
. Si vous essayez de comparer system.time(f3(mat))
et system.time(f4(mat))
vous obtiendrez des résultats différents presque à chaque fois. Parfois, seul un test de référence approprié est capable de montrer la fonction la plus rapide.
apply
famille de fonctions. Par conséquent, la structuration des programmes pour qu'ils utilisent apply leur permet d'être parallélisés à un coût marginal très faible.