Ruby での統計処理は R より遅いものの結構速い

Rails アプリでまとまった数の DB のレコードをロードし、統計処理をして結果を DB に記録するということをしたところ、完了するまで10時間以上かかってしまいかなり遅いと感じた。 改善したいと思いコードを見直そうとしたのだが、これを R で実装したらかなり改善できるのではないかと思った。 既存のコードを R に移植するのはそこそこ時間がかかりそうだったので、簡略化した処理で実行時間の比較をし、

比較方法

  • DB から 10 万件のレコードを読み、単純移動平均 (n = 25) を計算して DB に記録する
  • 実行時間は time コマンドの結果とする

環境および実装

DB

テストデータのテーブル定義は以下

-- 統計対象
CREATE TABLE `test_data` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `value` float NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB;

-- 単純移動平均の結果
CREATE TABLE `smas` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `value` float NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB;
Ruby
require "active_record"
require "activerecord-import"

class TestData < ActiveRecord::Base
end

class SMA < ActiveRecord::Base
  def self.calculate(test_data, n)
    0.upto(test_data.size - 1 - n).map do |i|
      [test_data[i, n].sum(&:value) / n]
    end
  end
end

ActiveRecord::Base.establish_connection(adapter: "mysql2", database: "testdb", username: "user", password: "password")

columns = %w(value)
test_data = TestData.all
sma_values = SMA.calculate(test_data, 25).compact
SMA.import(columns, sma_values, validate: false)

R

  • R 3.5.1
  • RMySQL
library(RMySQL)

conn <- dbConnect(dbDriver("MySQL"), dbname = "testdb", user = "user", password = "password")
dat <- dbReadTable(conn, "test_data")
sma <- na.omit(filter(dat$value, rep(1:1, 25)))

valuesSQL <- paste(
  "(NULL, ",
  sma,
  ")",
  sep = "",
  collapse = ","
)
insertSQL <- paste(
  "INSERT INTO `smas` (`id`, `value`) VALUES",
  valuesSQL
)

dbSendQuery(conn, insertSQL)

dbDisconnect(conn)

実行結果

今回試した環境では、Ruby での実装は R の 3.5 倍ほどの時間がかかることがわかった。

> time bundle exec ruby calculate.rb 

real    0m4.210s
user    0m3.175s
sys     0m0.129s
> time Rscript --vanilla calculate.R 
 要求されたパッケージ DBI をロード中です 
<MySQLResult:-1925505208,0,1>
[1] TRUE
 警告メッセージ: 
Closing open result sets 

real    0m1.273s
user    0m0.576s
sys     0m0.031s

なお Ruby においては bundle exec ruby -r active_record -r activerecord-import -e "" だけでも 0.5 秒程度かかるので、実質的な差はより少ないと考えられる。

まとめ

Ruby による実装では R よりも時間がかかったものの、R の 3.5 倍程度の差にとどまるということがわかった。 これは処理対象となるレコードが 10 万件のものなので、レコード数や性能要件によっては Ruby で実装しても問題ないと判断することも多いと思われる。

また Ruby でも ActiveRecord モデルを使用せずレコードのデータを Array の Array でロード・処理することでより高速化はできるかもしれない。

なおここで使用したコードは GitHub に push した (nowlinuxing/activerecord-vs-R) ので興味のある方はどうぞ。