Respostas:
As apply
funçõ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 é lapply
que 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 assign
ou <<-
, 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 apply
funções familiares (mesmo o foreach
pacote é essencialmente equivalente, apesar do nome). Aqui está um exemplo simples da sapply
funçã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 ). snow
tem 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 apply
funçõ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 for
loop, ele é definido globalmente. Por outro lado, todas as apply
funçõ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 assign
ou <<-
, 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 for
e no *apply
que 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 df
ambiente pai é alterado, for
mas não é *apply
.
snowfall
pacote e tentar os exemplos em sua vinheta. snowfall
constrói sobre o snow
pacote e abstrai ainda mais os detalhes da paralelização, simplificando ainda mais a execução de apply
funções paralelas .
foreach
, desde então, tornou-se disponível e parece ser muito questionado sobre o SO.
lapply
é "um pouco mais rápido" que um for
loop. 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 ( sapply
tenta 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?
À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? ;-)
*apply
é mais rápido. Mas acho que o ponto mais importante são os efeitos colaterais (atualizei minha resposta com um exemplo).
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")])
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 apply
pode). Você está comparando maçãs com laranjas.
... 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
for
os loops são mais rápidos no meu computador com Windows 10 e 2 núcleos. Eu fiz isso com 5e6
elementos - um loop foi de 2,9 segundos vs. 3,1 segundos para vapply
.
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 for
loop 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 for
loop e um pouco mais lento que a tapply
função otimizada incorporada. Não é porque vapply
é muito mais rápido que, for
mas porque está executando apenas uma operação em cada iteração do loop. Nesse código, todo o resto é vetorizado. No for
loop 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 for
versão.
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
sapply
50% mais lento que for
e lapply
duas vezes mais rápido.
y
como 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 for
uso loop. Caso contrário, a mensagem está no local.
Ao aplicar funções sobre subconjuntos de um vetor, tapply
pode 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 colSums
e 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
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.
apply
famí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.