2016.09.06
第4回 Rにおけるデータ加工処理高速化の基本
統計解析向けのプログラミング言語であるRは分析モデリングを簡単な記述で実現できる一方、計算に長い時間がかかる場合があります。本記事ではデータ加工処理高速化の基礎的なテクニックを紹介します。
1. はじめに
Rは、統計解析向けのプログラミング言語です。そのため、分析モデリングを簡単な関数の記述で実現できる一方で、汎用的なプログラミング言語と同じように記述しても、計算に長い時間がかかる場合があります。本記事では、まだR言語に慣れていない人向けにデータ加工処理高速化の基礎的なテクニックを紹介します。
2. データコピーの回避
Rでは、大量のデータを扱うことも多く、データの格納方法にも気を使わなければなりません。特に気をつけることは、できるだけ最初に必要なデータの格納場所(メモリ)を確保しておくことです。これは、無駄なデータコピーを減らすためです。
例として、1,000,000個の1~10のベクトルの各要素値の2乗をすべて計算するケースを考えます。Case A1では、空ベクトルを生成し、append()
を利用して、結果を1つずつ追加していきます。Case A2では、0が1,000,000個並んだベクトルを生成し、結果を1つずつ上書きしていきます。この2種類の方法の計算時間を比較してみましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | #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) |
1 2 3 4 5 6 7 | #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
を、sapply
とvapply()
に置き換えたCase A3とCase A4について考えます。新たにこの2種類の方法を追加して、計算時間を比較してみましょう。(lapply()
+unlist()
を使ったケースは、ほぼsapply()
と同様の結果なので省略します。)
1 2 3 4 | #Case A3(テストデータ作成、時間計測開始/終了は省略) #各要素の2乗値を計算 result <- sapply(test_data, function (x) x^2) |
1 2 3 4 | #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
同士の積を計算するケースを新たに追加して、計算時間を比較してみましょう。
1 2 3 4 5 | #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をコーディングする時は、常にベクトル化をこころがけることが重要です。