Respostas:
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.
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 .
foreach, desde então, tornou-se disponível e parece ser muito questionado sobre o SO.
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?
À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 applypode). 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
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.
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.
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
sapply50% mais lento que fore lapplyduas vezes mais rápido.
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.
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
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.
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.