2018年4月3日火曜日

Ruby で重回帰分析をしてみた

概要

重回帰分析は複数の変数を与えたときにある 1 つの Y が得られるときの関数を求めることができる分析手法です
Ruby で簡単に使えるライブラリがあるので紹介します

環境

  • macOS 10.13.2
  • Ruby 2.4.1p111
  • statsample 2.1.0

ライブラリインストール

  • bundle init
  • vim Gemfile
gem "statsample"
gem "nokogiri"

今回適当な Web 上のデータをスクレーピングするので nokogiri はそのためにインストールしています

学習用サンプルコード

全体としてはこんな感じです

  • learn.rb
# coding: utf-8
require 'statsample'
require 'nokogiri'

charset = 'utf-8'
html = open('http://npb.jp/bis/2017/stats/bat_c.html') do |f|
  f.read
end

doc = Nokogiri::HTML.parse(html, nil, charset)
data = []
doc.xpath('//div[@id="stdivmaintbl"]').each do |node|
  node.css('tr').each { |tr|
    next if tr.css('td').count <= 1
    d = []
    tr.css('td').each { |td|
      d.push(td.text.to_f)
    }
    data.push d
  }
end

h = {}
h['rate'] = data.map {|i| i[3]}
h['game'] = data.map {|i| i[4]}
h['plate'] = data.map {|i| i[5]}
h['bat'] = data.map {|i| i[6]}
h['run'] = data.map {|i| i[7]}
h['hit'] = data.map {|i| i[8]}
h['double'] = data.map {|i| i[9]}
h['triple'] = data.map {|i| i[10]}
h['shot'] = data.map {|i| i[11]}
h['total'] = data.map {|i| i[12]}
h['rbi'] = data.map {|i| i[13]}
h['steal'] = data.map {|i| i[14]}
h['steal_out'] = data.map {|i| i[15]}
h['bunt'] = data.map {|i| i[16]}
h['fly'] = data.map {|i| i[17]} 
h['walk'] = data.map {|i| i[18]}
h['intentional_walk'] = data.map {|i| i[19]}
# h['hit_by_pitch'] = data.map {|i| i[20]}
h['k'] = data.map {|i| i[21]}
h['double_play'] = data.map {|i| i[22]}
h['extra_hit_rate'] = data.map {|i| i[23]}
h['on_base_rate'] = data.map {|i| i[24]}

lr = Statsample::Regression.multiple(h.to_dataset, 'rate')
puts lr.summary

今回はプロ野球セリーグの打者データ上位 28 名のデータを学習データに使っています (前半部分)
出力したい情報を「打率 (rate)」とし、それ以外のデータを入力変数として使っています
打率なので単純な計算でも出せますがあえて学習させています

data に選手ごとの各打者成績が入っているので、それを項目ごとに変数に格納し直しています

h が最終的な学習させるデータでハッシュ形式でデータの部分を配列として定義します
そして Statsample::Regression.multiple に食わせる際に to_dataset をコールすることで学習データに変形します
第二引数の rate は算出したい値の項目になるので今回は打率を指定します

これで実行すると以下のような出力が得られます

  • bundle exec ruby learn
= Multiple reggresion of bat,bunt,double,double_play,extra_hit_rate,fly,game,hit,intentional_walk,k,on_base_rate,plate,rbi,run,shot,steal,steal_out,triple,walk on rate
  Engine: Statsample::Regression::Multiple::RubyEngine
  Cases(listwise)=28(28)
  R=1.000
  R^2=0.999
  R^2 Adj=0.998
  Std.Error R=0.001
  Equation=-0.039 + 0.001bat + 0.001bunt + -0.000double + -0.000double_play + 0.025extra_hit_rate + 0.001fly + 0.000game + 0.001hit + 0.000intentional_walk + 0.000k + 0.833on_base_rate + -0.001plate + 0.000rbi + 0.000run + -0.000shot + 0.000steal + -0.000steal_out + -0.001triple + -0.000walk
  == ANOVA
    ANOVA Table
+------------+-------+----+-------+---------+-------+
|   source   |  ss   | df |  ms   |    f    |   p   |
+------------+-------+----+-------+---------+-------+
| Regression | 0.020 | 19 | 0.001 | 606.688 | 0.000 |
| Error      | 0.000 | 8  | 0.000 |         |       |
| Total      | 0.020 | 27 | 0.001 |         |       |
+------------+-------+----+-------+---------+-------+

  Beta coefficients
+------------------+--------+--------+-------+--------+
|      coeff       |   b    |  beta  |  se   |   t    |
+------------------+--------+--------+-------+--------+
| Constant         | -0.039 | -      | 0.043 | -0.912 |
| bat              | 0.001  | 1.292  | 0.000 | 3.279  |
| bunt             | 0.001  | 0.177  | 0.000 | 4.850  |
| double           | -0.000 | -0.074 | 0.000 | -2.086 |
| double_play      | -0.000 | -0.009 | 0.000 | -0.551 |
| extra_hit_rate   | 0.025  | 0.067  | 0.057 | 0.438  |
| fly              | 0.001  | 0.064  | 0.000 | 2.394  |
| game             | 0.000  | 0.052  | 0.000 | 1.599  |
| hit              | 0.001  | 0.534  | 0.000 | 3.037  |
| intentional_walk | 0.000  | 0.004  | 0.000 | 0.192  |
| k                | 0.000  | 0.058  | 0.000 | 1.508  |
| on_base_rate     | 0.833  | 1.031  | 0.134 | 6.241  |
| plate            | -0.001 | -1.630 | 0.000 | -5.231 |
| rbi              | 0.000  | 0.067  | 0.000 | 1.577  |
| run              | 0.000  | 0.042  | 0.000 | 1.101  |
| shot             | -0.000 | -0.151 | 0.000 | -1.202 |
| steal            | 0.000  | 0.017  | 0.000 | 0.561  |
| steal_out        | -0.000 | -0.016 | 0.000 | -0.445 |
| triple           | -0.001 | -0.039 | 0.000 | -1.814 |
| walk             | -0.000 | -0.256 | 0.000 | -2.547 |
+------------------+--------+--------+-------+--------+

大事なのは Equation で得られた式になります
この式に当てはめると打率を機械学習的に求めることができます

また表を見ると b の欄で各変数の係数がわかります
ここで 0.000 になっている項目は計算に不必要な項目になるので例えば rbi (打点) は機械学習的には打率を算出するのには不必要な項目だと判断されています

Tips

今回「塁打 (total)」と「死球 (hit_by_pitch)」は学習に含められませんでした
含めようとすると以下のエラーとなり学習できません

Regressors are linearly dependent (Statsample::Regression::LinearDependency)

どうやら重回帰分析には学習できるデータに条件があるようです (詳細は不明)
Github に Issue があがっており「R でも同じデータでエラーになるよ、たぶんデータが悪い」と指摘がされていました
https://github.com/clbustos/statsample/issues/2

テスト用サンプルコード

では先ほどの式を使ってテストしてみましょう
テストデータはパリーグの選手の打席データを使ってみたいと思います

  • vim test.rb
# coding: utf-8
# 秋山翔吾 -> 0.322
bat = 575
bunt = 0
extra_hit_rate = 0.536
fly = 7
hit = 185
on_base_rate = 0.398
plate = 659
triple = 5
rate =- 0.039 + (0.001 * bat) + (0.001 * bunt) + (0.025 * extra_hit_rate) + (0.001 * fly) + (0.001 * hit) + (0.833 * on_base_rate) + (-0.001 * plate) + (-0.001 * triple)
puts rate

# 柳田悠岐 -> 0.310
bat = 448
bunt = 0
extra_hit_rate = 0.589
fly = 7
hit = 139
on_base_rate = 0.426
plate = 551
triple = 1
rate =- 0.039 + (0.001 * bat) + (0.001 * bunt) + (0.025 * extra_hit_rate) + (0.001 * fly) + (0.001 * hit) + (0.833 * on_base_rate) + (-0.001 * plate) + (-0.001 * triple)
puts rate

# 茂木栄五郎 -> 0.296
bat = 398
bunt = 4
extra_hit_rate = 0.497
fly = 1
hit = 118
on_base_rate = 0.370
plate = 450
triple = 2
rate =- 0.039 + (0.001 * bat) + (0.001 * bunt) + (0.025 * extra_hit_rate) + (0.001 * fly) + (0.001 * hit) + (0.833 * on_base_rate) + (-0.001 * plate) + (-0.001 * triple)
puts rate

# 西川遥輝 -> 0.296
bat = 541
bunt = 6
extra_hit_rate = 0.416
fly = 3
hit = 160
on_base_rate = 0.378
plate = 623
triple = 6
rate =- 0.039 + (0.001 * bat) + (0.001 * bunt) + (0.025 * extra_hit_rate) + (0.001 * fly) + (0.001 * hit) + (0.833 * on_base_rate) + (-0.001 * plate) + (-0.001 * triple)
puts rate

不要と判断させた項目は削除しています
これで算出すると上から

  • 秋山翔吾 -> 0.408934
  • 柳田悠岐 -> 0.3725829999999999
  • 茂木栄五郎 -> 0.350635
  • 西川遥輝 -> 0.367274

という感じになりました
今回算出できた式は成績が良い方向に計算されるようになったようです
もしくはパリーグの選手がもしセリーグで出場したら上記のような成績が残せたのかもしれません (たぶんそれはないが)

最後に

Ruby で重回帰分析を行ってみました
学習データが 28 件しかないのでかなり結果にズレが出ましたが、それっぽいモデルは作れました
使い方はこれでマスタできたかなと思います

あとはデータを増やして再度学習させればかなり近い値が出せるようになるかなと思います
今回は「打率」を算出するモデルを作りましたが打率は「打数」と「安打数」の計算で求められてしまうので本来であれば入力と出力は少し工夫したほうが良かったかもしれません

重回帰分析は「予測」や「推測」をするための手法だと思います
既存のデータからある規則 (式) を導き出し、今後起こり得る状況 (変数) に当てはめることで正解を探す方法かなと思います

何か予測したいデータがある場合に使ってみてはいかがでしょうか

0 件のコメント:

コメントを投稿