...実行時間および/またはメモリについて。
これが当てはまらない場合は、コードスニペットで証明します。ベクトル化による高速化はカウントされないことに注意してください。スピードアップはapply
(tapply
、sapply
、...)自体からもたらされる必要があります。
Rのapply
関数は、他のループ関数(たとえば、for
)よりもパフォーマンスが向上しません。これの1つの例外はlapply
です。これは、RよりもCコードでより多くの作業を行うため、少し高速になります( この例についてはこの質問 を参照)。
しかし、一般的には、ルールはパフォーマンスではなく明確にするために適用関数を使用する必要があります 。
これに、apply関数には 副作用なし、これは、Rを使用した関数型プログラミングに関して重要な違いです。これは、assign
または<<-
を使用してオーバーライドできますが、非常に危険です。また、変数の状態は履歴に依存するため、副作用によりプログラムが理解しにくくなります。
編集:
フィボナッチ数列を再帰的に計算する簡単な例でこれを強調するだけです。これを複数回実行して正確な測定値を取得することもできますが、重要な点は、どのメソッドもパフォーマンスが大きく異なることはありません。
> 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
編集2:
Rの並列パッケージ(たとえば、rpvm、rmpi、snow)の使用に関しては、これらは一般にapply
ファミリー関数を提供します(名前にかかわらず、foreach
パッケージも基本的に同等です)。 sapply
のsnow
関数の簡単な例を次に示します。
library(snow)
cl <- makeSOCKcluster(c("localhost","localhost"))
parSapply(cl, 1:20, get("+"), 3)
この例では、追加のソフトウェアをインストールする必要のないソケットクラスターを使用しています。それ以外の場合は、PVMまたはMPI( Tierneyのクラスタリングページ を参照)など)が必要になります。snow
には次の適用関数があります。
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, ...)
apply
関数はには副作用。 for
ループ内で変数値を変更すると、グローバルに設定されます。一方、変更は関数呼び出しに対してローカルであるため、すべてのapply
関数を安全に並行して使用できます(assign
または<<-
を使用しようとしない限り、副作用を引き起こす可能性があります)。言うまでもなく、特に並列実行を扱う場合は、ローカル変数とグローバル変数に注意することが重要です。
編集:
副作用に関する限り、for
と*apply
の違いを示す簡単な例を次に示します。
> 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
親環境のdf
は*apply
ではなくfor
によって変更されることに注意してください。
複数の要因のグループ化に基づいて平均を取得するためにforループをネストする必要がある場合のように、スピードアップが大幅に向上する場合があります。ここには、まったく同じ結果をもたらす2つのアプローチがあります。
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)
どちらもまったく同じ結果をもたらし、平均と名前付きの行と列を含む5 x 10のマトリックスです。しかし:
> 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
行くぞ私は何に勝ちましたか? ;-)
...そして私が先ほど書いたように、vapplyはあなたの友達です! ... sapplyに似ていますが、戻り値の型も指定するため、より高速になります。
> 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
シェーンのような例では、実際にはループにストレスをかけるのではなく、関数内ですべての時間が費やされるため、さまざまな種類のループ構文のパフォーマンスの違いにストレスをかけることはないと書いています。さらに、コードは、メモリのないforループと、値を返す適用ファミリ関数を不公平に比較します。ここでは、ポイントを強調するわずかに異なる例を示します。
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
結果を保存する予定の場合、適用ファミリー関数はmuch構文糖よりも大きくなる可能性があります。
(zの単純なアンリストはわずか0.2秒であるため、lapplyははるかに高速です。forループでのzの初期化は非常に高速です。物事にはほとんど影響しません)
ただし、注意すべきもう1つの点は、パフォーマンス、明確さ、または副作用がないことに関係なく、適用ファミリー関数を使用する別の理由があることです。 for
ループは通常、ループ内に可能な限り置くことを促進します。これは、各ループでは(他の可能な操作の中でも)情報を保存するための変数のセットアップが必要だからです。適用ステートメントは、他の方法でバイアスされる傾向があります。多くの場合、データに対して複数の操作を実行する必要があります。そのいくつかはベクトル化できますが、できないものもあります。 Rでは、他の言語とは異なり、それらの操作を分離し、applyステートメント(または関数のベクトル化バージョン)でベクトル化されていない操作と、真のベクトル操作としてベクトル化された操作を実行するのが最善です。これにより、パフォーマンスが大幅に向上することがよくあります。
Joris Meysを例に取って、従来のforループを便利なR関数に置き換えると、特殊関数を使用せずに同様の高速化を実現するために、よりRに優しい方法でコードを記述する効率を示すことができます。
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
これは、for
ループよりもはるかに高速で、組み込みの最適化されたtapply
関数よりも少し遅くなります。 vapply
がfor
よりもはるかに速いためではなく、ループの各反復で1つの操作のみを実行するためです。このコードでは、他のすべてがベクトル化されています。 Joris Meysの従来のfor
ループでは、各反復で多くの(7?)操作が発生し、実行するためのかなりのセットアップがあります。また、これはfor
バージョンよりもはるかにコンパクトであることに注意してください。
ベクトルのサブセットに関数を適用する場合、tapply
はforループよりもかなり高速です。例:
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
ただし、ほとんどの場合、速度は向上せず、場合によってはさらに遅くなります。
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
しかし、これらの状況ではcolSums
と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