web-dev-qa-db-ja.com

Rの適用ファミリーは構文糖衣以上のものですか?

...実行時間および/またはメモリについて。

これが当てはまらない場合は、コードスニペットで証明します。ベクトル化による高速化はカウントされないことに注意してください。スピードアップはapplytapplysapply、...)自体からもたらされる必要があります。

146
steffen

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パッケージも基本的に同等です)。 sapplysnow関数の簡単な例を次に示します。

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によって変更されることに注意してください。

149
Shane

複数の要因のグループ化に基づいて平均を取得するために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 

行くぞ私は何に勝ちましたか? ;-)

70
Joris Meys

...そして私が先ほど書いたように、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 
45
Tommy

シェーンのような例では、実際にはループにストレスをかけるのではなく、関数内ですべての時間が費やされるため、さまざまな種類のループ構文のパフォーマンスの違いにストレスをかけることはないと書いています。さらに、コードは、メモリのない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関数よりも少し遅くなります。 vapplyforよりもはるかに速いためではなく、ループの各反復で1つの操作のみを実行するためです。このコードでは、他のすべてがベクトル化されています。 Joris Meysの従来のforループでは、各反復で多くの(7?)操作が発生し、実行するためのかなりのセットアップがあります。また、これはforバージョンよりもはるかにコンパクトであることに注意してください。

27
John

ベクトルのサブセットに関数を適用する場合、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

しかし、これらの状況ではcolSumsrowSumsがあります:

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
3
Michele