Os R's aplicam a família mais que o açúcar sintático?

152

... em relação ao tempo de execução e / ou memória.

Se isso não for verdade, prove com um trecho de código. Observe que a aceleração por vetorização não conta. A aceleração deve vir de apply( tapply, sapply...) em si.

Steffen
fonte

Respostas:

152

As applyfunções em R não oferecem desempenho aprimorado em relação a outras funções de loop (por exemplo for). Uma exceção a isso é lapplyque pode ser um pouco mais rápido, porque funciona mais no código C do que no R (consulte esta pergunta para um exemplo disso ).

Mas, em geral, a regra é que você deve usar uma função de aplicação para maior clareza, não para desempenho .

Eu acrescentaria que as funções de aplicação não têm efeitos colaterais , o que é uma distinção importante quando se trata de programação funcional com R. Isso pode ser substituído usando assignou <<-, mas isso pode ser muito perigoso. Os efeitos colaterais também dificultam a compreensão de um programa, pois o estado de uma variável depende do histórico.

Editar:

Apenas para enfatizar isso com um exemplo trivial que calcula recursivamente a sequência de Fibonacci; isso pode ser executado várias vezes para obter uma medida precisa, mas o ponto é que nenhum dos métodos tem desempenho significativamente diferente:

> 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 

Edição 2:

Em relação ao uso de pacotes paralelos para R (por exemplo, rpvm, rmpi, snow), eles geralmente fornecem applyfunções familiares (mesmo o foreachpacote é essencialmente equivalente, apesar do nome). Aqui está um exemplo simples da sapplyfunção em snow:

library(snow)
cl <- makeSOCKcluster(c("localhost","localhost"))
parSapply(cl, 1:20, get("+"), 3)

Este exemplo usa um cluster de soquete, para o qual nenhum software adicional precisa ser instalado; caso contrário, você precisará de algo como PVM ou MPI (consulte a página de cluster do Tierney ). snowtem as seguintes funções de aplicação:

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

Faz sentido que applyfunções devam ser usadas para execução paralela, pois não têm efeitos colaterais . Quando você altera um valor variável dentro de um forloop, ele é definido globalmente. Por outro lado, todas as applyfunções podem ser usadas com segurança em paralelo, porque as alterações são locais na chamada de função (a menos que você tente usar assignou <<-, nesse caso, você pode introduzir efeitos colaterais). Escusado será dizer que é fundamental ter cuidado com as variáveis ​​locais vs. globais, especialmente ao lidar com a execução paralela.

Editar:

Aqui está um exemplo trivial para demonstrar a diferença entre fore no *applyque diz respeito aos efeitos colaterais:

> 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

Observe como o dfambiente pai é alterado, formas não é *apply.

Shane
fonte
30
A maioria dos pacotes com vários núcleos para R também implementa paralelização por meio da applyfamília de funções. Portanto, a estruturação dos programas para que eles utilizem o aplicativo permite que eles sejam paralelizados a um custo marginal muito pequeno.
Sharpie
Sharpie - obrigado por isso! Alguma idéia para um exemplo mostrando isso (no Windows XP)?
Tal Galili
5
Eu sugeriria olhar para o snowfallpacote e tentar os exemplos em sua vinheta. snowfallconstrói sobre o snowpacote e abstrai ainda mais os detalhes da paralelização, simplificando ainda mais a execução de applyfunções paralelas .
Sharpie
1
@ Sharpe, mas observe que foreach, desde então, tornou-se disponível e parece ser muito questionado sobre o SO.
Ari B. Friedman
1
@ Shane, na parte superior da sua resposta, você vincula a outra pergunta como exemplo de um caso em que lapplyé "um pouco mais rápido" que um forloop. No entanto, não vejo nada sugerindo isso. Você mencionou apenas que lapplyé mais rápido que sapply, o que é um fato bem conhecido por outros motivos ( sapplytenta simplificar a saída e, portanto, precisa fazer muita verificação de tamanho de dados e possíveis conversões). Nada relacionado a for. Estou esquecendo de algo?
Flod #
70

Às vezes, a aceleração pode ser substancial, como quando você precisa aninhar loops para obter a média com base em um agrupamento de mais de um fator. Aqui você tem duas abordagens que fornecem exatamente o mesmo resultado:

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)

Ambos fornecem exatamente o mesmo resultado, sendo uma matriz 5 x 10 com as médias e linhas e colunas nomeadas. Mas :

> 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 

Ai está. O que eu ganhei? ;-)

Joris Meys
fonte
aah, tão doce :-) Eu estava realmente pensando se alguém iria se deparar com a minha resposta bastante tarde.
Joris Meys
1
Eu sempre classifico por "ativo". :) Não tenho certeza de como generalizar sua resposta; às vezes *applyé mais rápido. Mas acho que o ponto mais importante são os efeitos colaterais (atualizei minha resposta com um exemplo).
Shane
1
Eu acho que aplicar é especialmente mais rápido quando você deseja aplicar uma função em diferentes subconjuntos. Se houver uma solução de aplicação inteligente para um loop aninhado, acho que a solução de aplicação também será mais rápida. Na maioria dos casos, aplicar não ganha muita velocidade, eu acho, mas definitivamente concordo com os efeitos colaterais.
Joris Meys
2
Este é um tópico pouco interessante, mas, para este exemplo específico, data.tableé ainda mais rápido e acho "mais fácil". 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
Essa comparação é absurda. tapplyé uma função especializada para uma tarefa específica, é por isso que é mais rápida que um loop for. Ele não pode fazer o que um loop for pode fazer (enquanto o normal applypode). Você está comparando maçãs com laranjas.
eddi
47

... e como acabei de escrever em outro lugar, vapply é seu amigo! ... é como sapply, mas você também especifica o tipo de valor de retorno que o torna muito mais rápido.

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 

Atualização de 1 de janeiro de 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
fonte
As descobertas originais não parecem mais verdadeiras. foros loops são mais rápidos no meu computador com Windows 10 e 2 núcleos. Eu fiz isso com 5e6elementos - um loop foi de 2,9 segundos vs. 3,1 segundos para vapply.
Cole
27

Escrevi em outro lugar que um exemplo como o de Shane não enfatiza realmente a diferença de desempenho entre os vários tipos de sintaxe de loop, porque todo o tempo é gasto dentro da função em vez de realmente estressar o loop. Além disso, o código compara injustamente um loop for sem memória com funções da família apply que retornam um valor. Aqui está um exemplo um pouco diferente que enfatiza o ponto.

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 

Se você planeja salvar o resultado, aplicar as funções da família pode ser muito mais que o açúcar sintático.

(a simples deslistagem de z é de apenas 0,2s, portanto, o lapply é muito mais rápido. A inicialização do z no loop for é bastante rápida, porque eu estou dando a média das últimas 5 de 6 execuções para mover fora do sistema. dificilmente afeta as coisas)

Mais uma coisa a ser observada, porém, é que há outro motivo para usar as funções da família de aplicação, independentemente de seu desempenho, clareza ou falta de efeitos colaterais. Um forloop normalmente promove a colocação o máximo possível dentro do loop. Isso ocorre porque cada loop requer a configuração de variáveis ​​para armazenar informações (entre outras operações possíveis). As instruções de aplicação tendem a ser tendenciosas de outra maneira. Muitas vezes, você deseja executar várias operações em seus dados, várias das quais podem ser vetorizadas, mas outras podem não ser. Em R, diferentemente de outros idiomas, é melhor separar essas operações e executar as que não são vetorizadas em uma instrução apply (ou versão vetorizada da função) e as que são vetorizadas como operações vetoriais verdadeiras. Isso geralmente acelera o desempenho tremendamente.

Tomando o exemplo de Joris Meys, onde ele substitui um loop for tradicional por uma função R útil, podemos usá-lo para mostrar a eficiência de escrever código de uma maneira mais amigável para R para uma aceleração semelhante sem a função especializada.

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

Isso acaba sendo muito mais rápido que o forloop e um pouco mais lento que a tapplyfunção otimizada incorporada. Não é porque vapplyé muito mais rápido que, formas porque está executando apenas uma operação em cada iteração do loop. Nesse código, todo o resto é vetorizado. No forloop tradicional de Joris Meys, muitas operações (7?) Estão ocorrendo em cada iteração e há bastante configuração apenas para sua execução. Observe também como isso é mais compacto que a forversão.

John
fonte
4
Mas o exemplo de Shane é realista, pois na maioria das vezes é gasto na função, não no loop.
Hadley
9
fale por si mesmo ...:) ... Talvez o de Shane seja realista em certo sentido, mas nesse mesmo sentido a análise é totalmente inútil. As pessoas se preocupam com a velocidade do mecanismo de iteração quando precisam fazer muitas iterações; caso contrário, seus problemas estão em outro lugar. É verdade para qualquer função. Se eu escrevo um pecado que leva 0,001s e alguém escreve outro que leva 0,002, quem se importa? Bem, assim que você tiver que fazer um monte deles, você se importa.
John
2
em um 12 núcleo 3Ghz Intel Xeon, de 64 bits, fico com números bastante diferentes para você - o loop for melhora consideravelmente: para os seus três testes, eu recebo 2.798 0.003 2.803; 4.908 0.020 4.934; 1.498 0.025 1.528, e vapply é ainda melhor:1.19 0.00 1.19
naught101
2
Isso varia com a versão do SO e do R ... e em um sentido absoluto da CPU. Acabei de rodar com 2.15.2 no Mac e fiquei sapply50% mais lento que fore lapplyduas vezes mais rápido.
John
1
No seu exemplo, você deseja definir ycomo 1:1e6, não numeric(1e6)(um vetor de zeros). Tentando alocar foo(0)a z[0]uma e outra não ilustra bem uma típica foruso loop. Caso contrário, a mensagem está no local.
Flod #
3

Ao aplicar funções sobre subconjuntos de um vetor, tapplypode ser bem mais rápido que um loop for. Exemplo:

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, no entanto, na maioria das situações, não aumenta a velocidade e, em alguns casos, pode ser ainda mais lento:

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

Mas para essas situações, temos colSumse 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
Michele
fonte
7
É importante notar que (para pequenos pedaços de código) microbenchmarké muito mais preciso que system.time. Se você tentar comparar system.time(f3(mat))e system.time(f4(mat))obterá resultados diferentes quase sempre. Às vezes, apenas um teste de benchmark adequado é capaz de mostrar a função mais rápida.
10133 Michele