一部のデータフレームにおいてfor ループがcolMeansより速い理由
先日、タイムラインを眺めていたらこんなエントリーが出てきました。
for ループが遅いなんて,誰が言った? - 裏 RjpWiki
「データフレームの場合は colMeans より for 文の方が速い」という奇をてらうような内容です。
どうしてそうなるかについて全く解説されておらず、限定的な話なのにあたかも普遍的な事実であるかのような書き方がされています。
間違った認識が広まってしまうとよろしくないのでちゃんと検証しました。
そもそもデータフレームだとどうして遅くなるか?
colMeans の一番最初に次のような処理があります。
if (is.data.frame(x))
x <- as.matrix(x)
つまり、一番最初に行列に変換するので、後の処理は行列の場合と全く同じということです。
よって、行列に変換するところがボトルネックになっていることは明らかです。
実際、次の方法で確認することができます。
> nr <- 10000
> nc <- 1000
> set.seed(1)
> d <- as.data.frame(matrix(rnorm(nr * nc), nr, nc))
> Rprof(interval = 0.001); invisible(colMeans(d)); Rprof(NULL)
> summaryRprof()
$by.self
self.time self.pct total.time total.pct
"as.matrix.data.frame" 0.066 79.52 0.081 97.59
"unlist" 0.013 15.66 0.013 15.66
"levels" 0.002 2.41 0.002 2.41
"colMeans" 0.001 1.20 0.083 100.00
"as.matrix" 0.001 1.20 0.082 98.80
$by.total
total.time total.pct self.time self.pct
"colMeans" 0.083 100.00 0.001 1.20
"as.matrix" 0.082 98.80 0.001 1.20
"as.matrix.data.frame" 0.081 97.59 0.066 79.52
"unlist" 0.013 15.66 0.013 15.66
"levels" 0.002 2.41 0.002 2.41
$sample.interval
[1] 0.001
$sampling.time
[1] 0.083
この結果から、先ほどのエントリーと同じ形式のデータだと as.matrix の部分で colMeans 全体の処理時間のうち約99%を占めていることがわかります。
再検証
ベンチマークで利用しているデータは10,000行1,000列のデータフレームですが、for 文が有利なように恣意的に設定されている気がします。
colMeans のボトルネックは as.matrix であり、for 文のボトルネックは R 側で行われる繰り返し処理そのものです。
よって、同じデータサイズであっても1,000行10,000列のデータフレームであれば for 文が極端に遅くなります。
> library(rbenchmark)
> nr <- 10000
> nc <- 1000
> set.seed(1)
> d <- as.data.frame(matrix(rnorm(nr * nc), nr, nc))
> # 10,000行1,000列だと for 文の方が遅い
> benchmark({
+ m1 <- numeric(nc)
+ for (i in 1:nc) {
+ m1[i] <- mean(d[, i])
+ }
+ },
+ colMeans(d))
test replications elapsed relative user.self sys.self user.child
1 { 100 8.778 1.000000 8.737 0.028 0
2 colMeans(d) 100 20.765 2.365573 16.562 4.173 0
sys.child
1 0
2 0
> # 1,000行10,000列だと colMeans の方が速い
> d2 <- as.data.frame(t(d))
> benchmark({
+ m2 <- numeric(nr)
+ for (i in 1:nr) {
+ m2[i] <- mean(d2[, i])
+ }
+ },
+ colMeans(d2))
test replications elapsed relative user.self sys.self user.child
1 { 100 105.201 3.116697 100.574 4.451 0
2 colMeans(d2) 100 33.754 1.000000 29.415 4.283 0
sys.child
1 0
2 0
列数が10倍になると for 文の処理時間も約10倍になっていることがわかるかと思います。10,000個の値に対して平均を取るのも1,000個の値に対して平均を取るのも処理時間的には大差ないので、繰り返し回数が増えた分だけ遅くなっているわけです。
結論
以上をまとめると
- データサイズが大きいデータフレームは as.matrix がボトルネックになる
- for 文は繰り返し回数に比例して遅くなる
- colMeans が速いか for 文が速いかはケースバイケース
と言えます。個人的には for 文の方が速いかどうかなんて気にせず一貫して colMeans を使えば良いのではないかと思います。
ちなみに今回検証に使ったマシンのスペックは次のとおりです。
- OS: Mac OS X 10.6.8
- CPU: 2.4 GHz Intel Core i5
- R: 2.14.1