マナティ

Rではじめよう![モダン]なデータ分析

第4回 Rにおけるデータ加工処理高速化の基本

統計解析向けのプログラミング言語であるRは分析モデリングを簡単な記述で実現できる一方、計算に長い時間がかかる場合があります。本記事ではデータ加工処理高速化の基礎的なテクニックを紹介します。

59404_ext_14_0.png

1. はじめに

Rは、統計解析向けのプログラミング言語です。そのため、分析モデリングを簡単な関数の記述で実現できる一方で、汎用的なプログラミング言語と同じように記述しても、計算に長い時間がかかる場合があります。本記事では、まだR言語に慣れていない人向けにデータ加工処理高速化の基礎的なテクニックを紹介します。

2. データコピーの回避

Rでは、大量のデータを扱うことも多く、データの格納方法にも気を使わなければなりません。特に気をつけることは、できるだけ最初に必要なデータの格納場所(メモリ)を確保しておくことです。これは、無駄なデータコピーを減らすためです。

例として、1,000,000個の1~10のベクトルの各要素値の2乗をすべて計算するケースを考えます。Case A1では、空ベクトルを生成し、append()を利用して、結果を1つずつ追加していきます。Case A2では、0が1,000,000個並んだベクトルを生成し、結果を1つずつ上書きしていきます。この2種類の方法の計算時間を比較してみましょう。

#Case A1
#テストデータ作成
test_data <- runif(1000000, min=1, max=10)

#時間計測開始
start_time<-proc.time()

#各要素の2乗値を計算
result <- c()
for(i in 1:length(test_data)){
  result <- append(result, test_data[i]^2)
}

#時間計測終了
end_time<-proc.time()
time <- end_time - start_time
print(time)
#Case A2 (テストデータ作成、時間計測開始/終了は省略)

#各要素の2乗値を計算
result <- numeric(nrow(test_data))
for(i in 1:length(test_data)){
  result[i] <- test_data[i]^2
}
Case A1 Case A2
1981.40 sec 1.67 sec

結果、圧倒的にCase A2の方が高速です。これは、Case A1では、append()が呼び出されるたびにresultに格納されているベクトルの全要素をコピーしているのに対して、Case A2ではresultに格納されているベクトルの各要素を更新しているだけだからです。このようにデータのコピー回数を減らすことは重要です。これはappend()だけではなく、rbind()cbind()などでも同様なことが言えます。

3. applyファミリー関数の活用

通常、繰り返し処理を行う時はforを使うことが多いと思います。もちろん、Rでもforを使うことができますが、Rではforの代わりにapplyファミリー関数を使うことが推奨されています。for自体の計算コストがapplyファミリー関数より少しだけ計算コストが大きいことに加えて、forを使うことによって、Case 1のようにfor内に計算コストが大きい処理を書きやすくなってしまうからです。

先ほどと同様に、1,000,000個の1~10のベクトルの各要素値の2乗をすべて計算するケースを考えます。Case A2のforを、sapplyvapply()に置き換えたCase A3とCase A4について考えます。新たにこの2種類の方法を追加して、計算時間を比較してみましょう。(lapply()unlist()を使ったケースは、ほぼsapply()と同様の結果なので省略します。)

#Case A3(テストデータ作成、時間計測開始/終了は省略)

#各要素の2乗値を計算
result <- sapply(test_data, function(x) x^2)
#Case A4(テストデータ作成、時間計測開始/終了は省略)

#各要素の2乗値を計算
result <- vapply(test_data, function(x) x^2, numeric(1))
Case A1 Case A2 Case A3 Case A4
1981.40sec 1.67sec 1.54sec 1.16sec

結果、Case A2,A3はほぼ変わらないですが、Case A4は計算時間が少しだけ短くなっています。これは、Case A4のvapply()はCase A3のsapply()と違い、出力の型を指定によって出力の型を推測する必要がないため計算時間が少しだけ短くなっています。また、Case A3,A4ではCase A1のようにappend()を使う書き方は自然とできなくなります。

4. ベクトル化

さらに計算時間をさらに短くする方法があります。それは、処理をベクトルの計算のみで実現する方法です。この様に、ベクトル内の各要素を計算するのではなく、ベクトル自体を1要素として扱うアプローチをベクトル化と呼びます。(Case A3,A4もtest_dataを1要素として扱っているので、ベクトル化の1種ではあります。)

先ほどと同様に、1,000,000個の1~10のベクトルの各要素値の2乗をすべて計算するケースを考えます。Case A5として、apply()を使わずにtest_data同士の積を計算するケースを新たに追加して、計算時間を比較してみましょう。

#Case A5(テストデータ作成、時間計測開始/終了は省略)

#各要素の2乗値を計算
result <- test_data * test_data
#result <- test_data^2 とも書ける
Case A1 Case A2 Case A3 Case A4 Case A5
1981.40 sec 1.67sec 1.54sec 1.16sec 0.06sec

結果、Case A5が他のケースと比較して、圧倒的に高速です。さらに、Case1と比較すると、約30,000倍も早くなっています。また、コード内容を見ても、簡潔に表現されています。このように、Rをコーディングする時は、常にベクトル化をこころがけることが重要です。

著者プロフィール

本橋智光(著者)
(Twitter:@tomomoto_Lv3) SIerにて研究員とコンサルを経験し、現在はWeb系企業のデータ分析者及びエンジニアとして勤務。趣味は、ゲーム、ゲームAIもどき開発。 ニコニコ動画:http://www.nicovideo.jp/user/21978811/video
匿名知的集団ホクソエム(著者)
ホクソエム (hoxo_m) は架空のデータ分析者であり、日本の若手のデータ分析者集団のペンネームである。当初このデータ分析者集団は秘密結社として活動し、ホクソエムを一個人として活動させ続けた。