web-dev-qa-db-fra.com

La famille d'application de R est-elle plus que du sucre syntaxique?

... 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.

146
steffen

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.

149
Shane

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

70
Joris Meys

... 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 
45
Tommy

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.

27
John

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
3
Michele