... concernant le temps d'exécution et/ou la mémoire.
Si ce n'est pas vrai, prouvez-le avec un extrait de code. Notez que l'accélération par vectorisation ne compte pas. L'accélération doit provenir de apply
(tapply
, sapply
, ...) elle-même.
Les fonctions apply
dans R n'offrent pas de performances améliorées par rapport aux autres fonctions de bouclage (par exemple for
). Une exception à cela est lapply
qui peut être un peu plus rapide car il fonctionne plus en code C qu'en R (voir cette question pour un exemple ).
Mais en général, la règle est que vous devez utiliser une fonction d'application pour plus de clarté, pas pour les performances.
J'ajouterais à cela que les fonctions d'application ont pas d'effets secondaires, qui est un distinction importante en matière de programmation fonctionnelle avec R. Ceci peut être annulé 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.
Modifier:
Juste pour souligner cela avec un exemple trivial qui calcule récursivement la séquence de Fibonacci; cela pourrait ê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
Edit 2:
En ce qui concerne l'utilisation de packages parallèles pour R (par exemple rpvm, rmpi, snow), ceux-ci fournissent généralement des fonctions de la famille apply
(même le package foreach
est essentiellement équivalent, malgré le nom). Voici un exemple simple de la fonction sapply
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 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 fonctions apply
soient utilisées pour l'exécution parallèle car elles n'ont pas effets secondaires. Lorsque vous modifiez une valeur de variable dans une boucle for
, elle est définie globalement. D'un autre côté, toutes les fonctions apply
peuvent être utilisées en parallèle en toute sécurité car les modifications sont locales à l'appel de fonction (sauf si vous essayez 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.
Modifier:
Voici un exemple trivial pour démontrer la différence entre for
et *apply
en 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 df
dans l'environnement parent est modifié par for
mais pas *apply
.
Parfois, l'accélération peut être substantielle, comme lorsque vous devez imbriquer des boucles 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é? ;-)
... et comme je viens d'écrire ailleurs, vapply est votre ami! ... c'est comme sapply, mais vous spécifiez également le type de valeur de retour, ce qui le rend beaucoup plus rapide.
> 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
J'ai écrit ailleurs qu'un exemple comme celui de Shane ne met pas vraiment l'accent sur la différence de performances entre les différents types de syntaxe de boucle, car le temps est entièrement passé dans la fonction plutôt que de souligner la boucle. En outre, 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 le 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 beaucoup plus que du sucre syntaxique.
(la simple liste 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 parce que je donne la moyenne des 5 dernières des 6 exécutions de manière à ce qu'en dehors du système. affectent guère les choses)
Une autre chose à noter cependant, c'est qu'il existe une autre raison d'utiliser les fonctions familiales indépendamment de leurs performances, de leur clarté ou de leur absence d'effets secondaires. Une boucle for
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 vraies opérations vectorielles. Cela accélère souvent considérablement les performances.
En 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 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 boucle for
et juste un peu plus lent que la fonction optimisée intégrée tapply
. Ce n'est pas parce que vapply
est beaucoup 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 Joris Meys, la boucle for
traditionnelle 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 à quel point c'est plus compact que la version for
.
Lors de l'application de fonctions sur des sous-ensembles d'un vecteur, 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 situations, il n'y a pas d'augmentation de vitesse, et dans certains cas, cela 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