Est-ce que R applique une famille plus que le sucre syntaxique?

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

Si ce n’est pas vrai, le prouver avec un extrait de code. Notez que l’accélération de la vectorisation ne compte pas. L’accélération doit venir d’ apply ( tapply , sapply , …) elle-même.

Les fonctions d’ apply de R n’offrent pas de meilleures performances que les autres fonctions de bouclage (par exemple for ). Une exception à cette lapply est lapply qui peut être un peu plus rapide car elle fait plus de travail en code C que dans 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é et non pour la performance .

J’appendais à cela que les fonctions d’application n’ont pas d’effets secondaires , ce qui est une distinction importante en matière de functional programming 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.

Modifier:

Juste pour souligner ceci avec un exemple sortingvial qui calcule de manière récursive 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 paquets parallèles pour R (par exemple, rpvm, rmpi, snow), ceux-ci fournissent généralement des fonctions de famille d'application (même le package foreach est essentiellement équivalent, malgré le nom). Voici un exemple simple de la fonction sapply dans la 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 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 apply fonctions d’ apply 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 boucle for , elle est définie globalement. D'un autre côté, toutes apply fonctions d' apply peuvent être utilisées en toute sécurité en parallèle car les modifications sont locales à l'appel de la 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 par rapport aux variables globales, en particulier lorsqu'il s'agit d'une exécution parallèle.

Modifier:

Voici un exemple sortingvial pour démontrer la différence entre for et *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 que le df dans l'environnement parent est modifié par mais non *apply .

Parfois, les accélérations peuvent être importantes, 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 masortingce 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 ton ami! … c’est comme sapply, mais vous spécifiez également le type de valeur de retour 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 n’insiste pas vraiment 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 réellement mettre la boucle sous contrainte. 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 souligne 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 de sauvegarder le résultat, alors les fonctions familiales peuvent être beaucoup plus que du sucre syntaxique.

(la simple liste de z ne fait que 0.2s, 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 des 6 dernières exécutions, donc déplacer en dehors de system.time serait affecte peu les choses)

Une autre chose à noter est qu'il existe une autre raison d'utiliser les fonctions familiales indépendamment de leurs performances, de leur clarté ou de l'absence d'effets secondaires. Une boucle for favorise généralement la mise autant que possible dans la boucle. Cela est dû au fait que chaque boucle nécessite la configuration de variables pour stocker des informations (entre autres opérations possibles). Les instructions 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 d'autres 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 vectorisées en tant que véritables 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 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 tapply optimisée tapply . Ce n'est pas parce que vapply est tellement plus rapide que for mais parce qu'il ne fait qu'une seule opération à chaque itération de la boucle. Dans ce code, tout le rest est vectorisé. Dans Joris Meys traditionnel for boucle, de nombreuses opérations (7?) Se produisent à chaque itération et il y a pas mal de configuration à exécuter. Notez aussi que c'est beaucoup plus compact que for version.

Lors de l’application de fonctions sur des sous-ensembles d’un vecteur, tapply peut être plus 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 ne fournit pas une augmentation de la 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 les colSums et les 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