StatsBeginner: 初学者の統計学習ノート

統計学およびR、Pythonでのプログラミングの勉強の過程をメモっていくノート。たまにMacの話題。

for、apply、ベクトル演算の処理速度の比較

ツイッターのライムラインで、forループをapplyに置き換えた場合の高速化の話が流れていて(こちら)、気になって検索したところ、applyよりむしろwithを使えと言っている人がいた。
r - apply() is slow - how to make it faster or what are my alternatives? - Stack Overflow


ちなみにこれは、with関数が早いというより、要するに「データフレームの行ごとの計算」を「ベクトル同士の計算」に置き換えることによって速くなっていると考えたほうがよい。withのメリットは、データフレームの列を指定するときに'd$'を書かなくでよくなるという点にある。


以下では、1万行×3列のデータで、横向きに標本分散を計算して1万個取り出す処理を、

  • forループで空のベクトルにアペンドしていく
  • forループで長さを指定したベクトルを更新していく(Pythonのリストの処理でこうすると速くなる場合があったのでやってみる)
  • applyを使う
  • applyを使い、かつ関数を事前にコンパイルしておく
  • ベクトル同士の計算にする($で列を取り出す)
  • ベクトル同士の計算にする(indexで列を取り出す)
  • ベクトル同士の計算にする(with関数をつかう)

の7通りで試してみた。

# パッケージよみこみ
library(rbenchmark)  # 速度を測る作業のラッパー
library(compiler)    # 関数をコンパイルする

# データフレームさくせい
d <- data.frame(
  x1 = runif(10000),
  x2 = runif(10000),
  x3 = runif(10000)
)

# 標本分散を出す関数を定義しておく
svar <- function(x){
  return(sum((x-mean(x))^2)/(length(x)))
}


# 関数をコンパイルする
c.svar <- cmpfun(svar)

# 処理時間を比較する
bm1 <- benchmark(
  'for_append' = {
    v1 <- c()
    for(i in 1:nrow(d)){
      v1 <- c(v1, svar(unlist(d[i,])))
    }
  },
  'for_replace' = {
    v2 <- rep(NA, nrow(d))
    for(i in 1:nrow(d)){
      v2[i] <- svar(unlist(d[i,]))
    }
  },
  'apply' = {
    v3 <- apply(d, 1, svar)
  },
  'apply+compile' = {
    v4 <- apply(d, 1, c.svar)
  },
  'vectorize_$' = {
    v5 <- ((d$x1-(d$x1+d$x2+d$x3)/3)^2 + (d$x2-(d$x1+d$x2+d$x3)/3)^2 + (d$x3-(d$x1+d$x2+d$x3)/3)^2)/3
  },
  'vectorize_index' = {
    v6 <- ((d[,1]-(d[,1]+d[,2]+d[,3])/3)^2 + (d[,2]-(d[,1]+d[,2]+d[,3])/3)^2 + (d[,3]-(d[,1]+d[,2]+d[,3])/3)^2)/3
  },
  'vectorize_with' = {
    v7 <- with(d, ((x1-(x1+x2+x3)/3)^2 + (x2-(x1+x2+x3)/3)^2 + (x3-(x1+x2+x3)/3)^2)/3)
  },
  replications = 100)

# いらん列を消して経過時間でソートしておく
bm1 <- bm1[1:4] %>%
  arrange(elapsed)


replications = 100というのは100回処理を繰り返して比較してるということ。
以下のような結果が得られた。

             test replications elapsed relative
1  vectorize_with          100   0.032    1.000
2     vectorize_$          100   0.034    1.063
3 vectorize_index          100   0.042    1.313
4   apply+compile          100   6.618  206.812
5           apply          100   7.565  236.406
6     for_replace          100  52.767 1648.969
7      for_append          100  93.641 2926.281


elapsedが経過時間を表し、relativeは最速のものとの比を示したもの。
とにかく、ベクトル同士の演算に置き換えられる場合はめちゃめちゃ速くなることが分かる。アクセスの仕方で差はほぼ無く、withを使うと記述が簡単になるのでwithを使っておけばよさそうだ。
applyはforループより1桁速いが、それでもベクトル演算に比べると2桁倍の時間がかかっている。コンパイルしても凄く速くはならない。
forループは、上書き方式にするとアペンド方式より速いが、いずれにしてもベクトル演算の数千倍の時間がかかっている。


↓のように値を置き換えるような処理だと、ベクトル演算にはできないので、とりあえずforよりはapplyを使っておくのが良さそうな気がする。コンパイルしたほうが遅くて草。【追記】Twitterで指摘をもらって気づいたけど、↓の例だと縦方向にも処理できるのでベクトル化容易やなww 横方向にしか処理できない内容であとで試そう。

> ### 値を置換する処理をforとapplyで比較する
> 
> # ベクトル中の0.5より小さい値を0に置換する関数
> rpl <- function(x){
+   replace(x, which(x < 0.5), 0)
+ }
> 
> c.rpl <- cmpfun(rpl)
> 
> bm2 <- benchmark(
+   'for' = {
+     d.new1 <- d
+     for(i in 1:nrow(d)){
+       d[i,] <- rpl(d[i,])
+     }
+   },
+   'for+compile' = {
+     d.new2 <- d
+     for(i in 1:nrow(d)){
+       d[i,] <- c.rpl(d[i,])
+     }
+   },
+   'apply' = {
+     d.new3 <- d
+     apply(d.new2, 1, rpl)
+   },
+   'apply+compile' = {
+     d.new4 <- d
+     apply(d.new2, 1, c.rpl)
+   },
+   replications = 5
+ )
> 
> bm2[1:4] %>% arrange(elapsed)
           test replications elapsed relative
1         apply            5   0.246    1.000
2 apply+compile            5   0.247    1.004
3           for            5  20.957   85.191
4   for+compile            5  22.890   93.049