La famille R's Apply est-elle plus qu'un sucre syntaxique?

152

... concernant le temps d'exécution et / ou la mémoire.

Si ce n'est pas le cas, prouvez-le avec un extrait de code. Notez que l'accélération par vectorisation ne compte pas. Le gain de vitesse doit provenir apply( tapply, sapply...) lui - même.

Steffen
la source

Réponses:

152

Les applyfonctions 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 lapplyqui 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 assignou <<-, 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 applydes fonctions familiales (même le foreachpackage est essentiellement équivalent, malgré le nom). Voici un exemple simple de la sapplyfonction 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 ). snowa 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 applyfonctions 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 forboucle, elle est définie globalement. D'autre part, toutes les applyfonctions 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 assignou <<-, 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 foret en *applyce 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 dfdans l'environnement parent est modifié par formais pas *apply.

Shane
la source
30
La plupart des packages multicœurs pour R implémentent également la parallélisation via la applyfamille 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.
Sharpie
Sharpie - merci pour cela! Une idée pour un exemple montrant cela (sur Windows XP)?
Tal Galili
5
Je suggérerais de regarder le snowfallpaquet et d'essayer les exemples dans leur vignette. snowfalls'appuie sur le snowpaquet et fait abstraction des détails de la parallélisation, ce qui rend plus simple l'exécution de applyfonctions parallélisées .
Sharpie
1
@Sharpie mais notez que foreachdepuis, il est devenu disponible et semble être très demandé sur SO.
Ari B.Friedman
1
@Shane, tout en haut de votre réponse, vous créez un lien vers une autre question comme exemple d'un cas où lapplyest "un peu plus rapide" qu'une forboucle. Cependant, là, je ne vois rien qui le suggère. Vous mentionnez seulement que lapplyc'est plus rapide que sapply, ce qui est un fait bien connu pour d'autres raisons ( sapplyessaie 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?
flodel
70

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é? ;-)

Joris Meys
la source
aah, si gentil :-) Je me demandais en fait si quelqu'un trouverait jamais ma réponse plutôt tardive.
Joris Meys
1
Je trie toujours par «actif». :) Je ne sais pas comment généraliser votre réponse; *applyest parfois plus rapide. Mais je pense que le point le plus important est les effets secondaires (mis à jour ma réponse avec un exemple).
Shane
1
Je pense que apply est particulièrement plus rapide lorsque vous souhaitez appliquer une fonction sur différents sous-ensembles. S'il existe une solution d'application intelligente pour une boucle imbriquée, je suppose que la solution d'application sera également plus rapide. Dans la plupart des cas, appliquer ne gagne pas beaucoup de vitesse, je suppose, mais je suis tout à fait d'accord sur les effets secondaires.
Joris Meys
2
C'est un peu hors sujet, mais pour cet exemple précis, data.tablec'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")])
dnlbrky
12
Cette comparaison est absurde. tapplyest 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 applypeuvent). Vous comparez des pommes avec des oranges.
eddi
47

... 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
Tommy
la source
Les résultats originaux ne semblent plus être vrais. forles 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.
Cole le
27

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 forboucle 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 forboucle et juste un peu plus lent que la tapplyfonction optimisée intégrée. Ce n'est pas parce que vapplyc'est tellement plus rapide que formais 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 forboucle 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 forversion.

John
la source
4
Mais l'exemple de Shane est réaliste dans la mesure où la plupart du temps est généralement passé dans la fonction, pas dans la boucle.
hadley
9
parlez pour vous-même ...:) ... Peut-être que Shane est réaliste dans un certain sens, mais dans ce même sens, l'analyse est totalement inutile. Les gens se soucieront de la vitesse du mécanisme d'itération lorsqu'ils doivent faire beaucoup d'itérations, sinon leurs problèmes sont de toute façon ailleurs. C'est vrai pour n'importe quelle fonction. Si j'écris un péché qui prend 0,001s et que quelqu'un d'autre en écrit un qui prend 0,002 qui s'en soucie ?? Eh bien, dès que vous devez en faire un tas, vous vous en souciez.
John
2
sur un Intel Xeon 12 cœurs 3Ghz, 64 bits, je vous donne des nombres assez différents - la boucle for s'améliore considérablement: pour vos trois tests, j'obtiens 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
rien101
2
Cela varie avec la version OS et R ... et dans un CPU absolu. Je viens de courir avec 2.15.2 sur Mac et je suis sapply50% plus lent foret lapplydeux fois plus rapide.
John
1
Dans votre exemple, vous voulez définir ysur 1:1e6, non numeric(1e6)(un vecteur de zéros). Essayer d'allouer foo(0)à z[0]plusieurs reprises n'illustre pas bien une forutilisation de boucle typique . Le message est par ailleurs parfait.
flodel
3

Lors de l'application de fonctions sur des sous-ensembles d'un vecteur, cela tapplypeut ê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 colSumset 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
Michèle
la source
7
Il est important de noter que (pour les petits morceaux de code) microbenchmarkc'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.
Michele