競馬をRで統計解析する初めの一歩

機械学習で競馬予測して勝てるのか?

人工知能や機械学習に興味があって、私にも何かできることはないかと探している時に、ふと「競馬予測はどうだろう」と思いついてウェブで調べました。すると、どんな分野にも先駆者がいるもので、stockedgeさんの記事、東工大生が作った競馬予測システムAlphaKeiba、中央競馬運営のJRA-VANのデータマイニングなどのページが見つかりました。

競馬の解析をガチでやったら回収率が100%を超えた件 - stockedge.jpの技術メモ

AlphaKeibaの穴馬マイニング - ニコニコチャンネル:スポーツ

データマイニング予測の仕組み - JRA-VAN

機械学習予測で賭け金より配当金が多い回収率1.0を超えることは、すでにやられているんですね。コンピュータ予想でそこまでできるんだと驚きました。

ただ、はじめは回収率>1.0さえ達成すればわっさわっさ金が儲かると思っていたのですが、はずれ馬券裁判で有名な馬券裁判 競馬で1億5000万円儲けた予想法の真実(2015年 卍著)- amazon.co.jpを読むと、どうもそう単純な話ではないようです。

しかし、卍さんの例や他にも競馬で収支プラスを稼いでいる人はいるので、コンピュータでデータ解析と予測をして競馬で長期的に勝つというのは、あながち夢物語ではなさそうです。なさそうというのは私が自分で確かめたわけではないからです。

だから、自分で計算して、計算結果で勝てそうだったら、実際に馬券を買ってみたいと思います。その進捗とアウトプットをこのブログに書いていきます。もしかしたら「機械学習で競馬予測して勝てるのか?」に自分なりの答えを出せるかもしれません。

といっても、私は競馬をやったことはないし、機械学習や統計学のプロではありません。プログラミングが少しできる程度です。できる確証はありませんが、気長に取り組んでいます。

とりあえず第一歩として、JRAの競馬データの簡単な統計解析をしてみました。

手っ取り早く解析してみたいけどデータはどうするか?

競馬のレース結果情報は、JRA-VANnetkeiba.comに情報がありますので、継続的に自分のデータベースを更新するなら、これらサイトから入手する仕組みを作らなければいけません。

正直、スクレイピングスクリプト書くか[1]、JRA-VANのソフトウェアを使ったりとちょう面倒くさいです。

ラッキーなことに、data_sciesotistさんが中央競馬のレース結果をcsvファイルにまとめてくれています。このページに中央競馬と地方競馬のレース結果をそれぞれまとめたcsvファイルのリンクがあり、そこからダウンロードできます。ウマナリティクスという競馬をサイエンスしようというイベントのページです。csvファイルがあって助かっています。ありがとうございます。

中央競馬のレース結果をまとまっている先のリンクからダウンロードしたjra_race_result.csvの中身は、次のようになっています。

# 行数 ワード数 ファイルサイズ を表示
$ wc jra_race_result.csv
  170216 546410 33831547 jra_race_result.csv
$ less jra_race_result.csv
開催日,競馬場,レース番号,レース名,コース,周回,距離,馬場状態,賞金,頭数,着順,枠番,馬番,馬名,性別,年齢,騎手,タイム,着差,通過順,上り3F,斤量,馬体重,増減,人気,オッズ,ブリンカー,調教師,調教コメント,調教評価
2013-01-05,中山,01,サラ系3歳未勝利,ダート,右,1200,良,500,16,1,6,12,リベルタドーレス,牡,3,丸田 恭介,73.6,,01-01,39.4,56.0,484,2,1,1.9,,宗像 義忠,好調持続,B
2013-01-05,中山,01,サラ系3歳未勝利,ダート,右,1200,良,200.0,16,2,2,4,アキノディフェンス,牝,3,石橋 脩,73.6,クビ,02-02,39.3,54.0,454,-2,7,44.1,,大和田 成,多少良化,C
...
2016-05-29,京都,12,與杼特別,ダート,右,1800,良,0,16,15,3,5,アースコネクター,牡,5,幸 英明,114.3,1 1/4馬身,01-01-01-02,41.6,57.0,502,0,2,5.2,B,和田 正道,動き良化,B
2016-05-29,京都,12,與杼特別,ダート,右,1800,良,0,16,16,8,15,ランドオザリール,牡,5,中谷 雄太,114.4,3/4馬身,05-05-05-07,41.2,57.0,538,10,11,59.3,,岡田 稲男,態勢万,A
(EOF)

ファイルサイズは33.8MB、列数は30列、行数は170216行です。2013年1月5日から2016年5月29日まで3年5ヶ月の間に開催された中央競馬レース結果の着順、タイム、オッズなどの情報が入っています。

Rに中央競馬のレース結果のデータをロードする

もっと詳しくデータを知るためにRで解析するためにRにjra_race_result.csvをロードしました。

# データフレーム変数dataにcsvデータを代入せよ 先頭1行はラベルなのでスキップ ヘッダーはなし
data <- read.csv("jra_race_result.csv", skip = 1, header = F)

# 列名を変更せよ
colnames(data) <- c("date","venue","raceNumber","raceName","track","runDirection","distance","trackCondition","purse","headsCount","finishOrder","postPosition","horseNumber","horseName","horseSex","horseAge","jockeyName","time","margin","waypointOrder","time3F","loadWeight","horseWeight","dhorseWeight","oddsOrder","odds","isBlinkers","trainerName","commentsByTrainer","evaluationByTrainer")

パラメータの定義はこのようにしました。

変数名 csvのラベル名 説明
date 開催日 yyyy-mm-dd
venue 競馬場
raceNumber レース番号
raceName レース名
track コース ダート, 芝, 障害のいずれか
runDirection 周回 ダートか芝の場合、右回りなら「右」、左回りなら「左」
distance 距離 [m]
trackCondition 馬場状態 良(りょう), 稍重(ややおも), 重(おも), 不良(ふりょう)の4段階
purse 賞金 [万円]
headsCount 頭数
finishOrder 着順
postPosition 枠番
horseNumber 馬番
horseName 馬名
horseSex 性別
horseAge 年齢
jockeyName 騎手
time タイム [s]
margin 着差 前着の馬との差のこと クビ, ハナ, アタマ, ?馬身, 大差
waypointOrder 通過順
time3F 上り3F ラスト600mのタイム [s]
loadWeight 斥量 [kg]
horseWeight 馬体重 [kg]
dhorseWeight 増減 前レースからの馬体重変化 [kg]
oddsOrder 人気 oddsの降順の番号
odds オッズ
isBlinkers ブリンカー ブリンカー(目隠し)ありの場合、「B」
trainerName 調教師
commentsByTrainer 調教コメント
evaluationByTrainer 調教評価

いつどこでレースが行われたか、その時のレース結果の着順、オッズなど基本的な情報は入っていますが、これでもパラメータは少ないと思います。このデータには、馬柱に記載されているような、馬の親馬、脚質などの情報が入っていません。

まぁ、しかし、始めて統計解析をするには情報量には申し分ありません。

Rで基本的な競馬の事柄を調べる

私は競馬についてほとんど知りませんので、開催地はどこがあるのか、トラックは何があるのか、といった基本的な疑問の答えを統計解析で調べました。

まず、View(data)で表を眺めてみると、同じレースの行がいくつもあるのがわかります。

jra_race_result.csv 01

jra_race_result.csv 02

jra_race_result.csv 03

「2013-01-05 中山 第1レース」の結果が、1着から16着まで1行ずつあります。1着は馬番12のリベルタドーレスで1番人気オッズ1.9です。

1つのレースで複数行あるので、全体が約17万行と多いことがわかります。行数170,215 = 2013-01-05から2016-05-29までに行われた全レース数 ではありません。

データを眺めて、1つのレースは、開催日、開催地、レース番号の3つの情報の組み合わせで決定することがわかりました。SQLなどのリレーショナル・データベースでいうところの主キーです。

2013-01-05から2016-05-29の間にレースが何回あったのか知りたいです。Rで次のように計算できます。

# 開催日.開催地.レース番号 で1つのレースが決まる
head(interaction(data$date, data$venue, data$raceNumber))
## [1] 2013-01-05.中山.1 2013-01-05.中山.1 2013-01-05.中山.1 2013-01-05.中山.1
## [5] 2013-01-05.中山.1 2013-01-05.中山.1
## 44880 Levels: 2013-01-05.中京.1 2013-01-06.中京.1 ... 2016-05-29.阪神.12
# 重複を除く
head(unique(interaction(data$date, data$venue, data$raceNumber)))
## [1] 2013-01-05.中山.1 2013-01-05.中山.2 2013-01-05.中山.3 2013-01-05.中山.4
## [5] 2013-01-05.中山.5 2013-01-05.中山.6
## 44880 Levels: 2013-01-05.中京.1 2013-01-06.中京.1 ... 2016-05-29.阪神.12
# 重複を除いたベクトルの長さ(全レース数)を得る  
length(unique(interaction(data$date, data$venue, data$raceNumber)))
## [1] 11795

レースは全部で11,795レースあったことがわかります。上の2つのコマンドは説明のためで、最後のコマンド1つで計算できます。

この11,795レースの開催地ごとの内訳はどうなっているのか調べました。

# レースが重複しているか(TRUE)、重複していないか(FALSE)
head(duplicated(interaction(data$date, data$venue, data$raceNumber)))
## [1] FALSE  TRUE  TRUE  TRUE  TRUE  TRUE
# 重複しているレースを除いて開催地のベクトルを作る
venueForCount <- data$venue[!duplicated(interaction(data$date, data$venue, data$raceNumber))]
head(venueForCount)
## [1] 中山 中山 中山 中山 中山 中山
## Levels: 中京 中山 京都 函館 小倉 新潟 札幌 東京 福島 阪神
# レースの開催地の内訳
summary(venueForCount)
## 中京  中山  京都  函館  小倉  新潟  札幌  東京  福島  阪神 
## 1032 1677 1990 576  816  1008  311 1879  864 1642 

11,795レースの開催地ごとの内訳がわかります。中央競馬の競馬場は全部で10ヶ所あって、最もレース数が少ないのは札幌競馬場で311レース、最もレース数が多いのは京都競馬場で1990レースです。

トラックはどうなのか気になります。レースのトラックごとの内訳を調べました。計算方法は開催地の内訳と同じです。

trackForCount <- data$track[!duplicated(interaction(data$date, data$venue, data$raceNumber))]
summary(trackForCount)
## ダート    芝   障害 
## 5746   5614   435 

トラックはダート、芝、障害の3種類です。11,795レースの内、少しだけダートの方がと芝よりもレース数が多いです。障害はダートと芝に比べればレース数は少ないことがわかります。

次はトラックがダート、芝、障害それぞれの開催地ごとの内訳です。

# ダートのみのデータフレームを作る
dirt <- subset(data, track == "ダート")

# 重複を除いた開催地のベクトルを作る
dirtForCount <- dirt$venue[!duplicated(interaction(dirt$date, dirt$venue, dirt$raceNumber))]

# ダートの全5,746レースの開催地ごとの内訳を得る
summary(dirtForCount)
## 中京 中山 京都 函館 小倉 新潟 札幌 東京 福島 阪神 
## 504  939 1045 268  286  411 137 944  351  861
# 芝の全5,614レースの開催地ごとの内訳を得る
turf <- subset(data, track == "芝")
turfForCount <- turf$venue[!duplicated(interaction(turf$date, turf$venue, turf$raceNumber))]
summary(turfForCount)
## 中京 中山 京都 函館 小倉 新潟 札幌 東京 福島 阪神 
## 463  681  887 308  507 531  174  883 454  726 
# 障害の全435レースの開催地ごとの内訳を得る
hurdle <- subset(data, track == "障害")
hurdleForCount <- hurdle$venue[!duplicated(interaction(hurdle$date, hurdle$venue, hurdle$raceNumber))]
summary(hurdleForCount)
## 中京 中山 京都 函館 小倉 新潟 札幌 東京 福島 阪神 
## 65   57   58    0   23  66    0  52   59  55 

ダートと芝は開催地ごとに多いレースと少ないレースがあっても極端な偏りはないことがわかります。障害レースは函館競馬場と札幌競馬場にはないんですね。

おわりに

以上、data_sciesotistさん提供の中央競馬のデータを使って、Rで簡単な統計解析をしました。

2017-11-25 競馬データとその後について

この後、自分で競馬レースの過去30年分のデータベース(を作るソフトウェア)を作りました。

詳しい説明をJVData To SQLiteに書きました。

今は、このソフトウェアJVData To SQLiteで作ったデータベースを使ってデータ解析をしています。

JVData To SQLiteは、JRA-VANデータラボ会員の方が使うことができます。オープンソースです(MITライセンス)。JRA-VANデータラボは1ヶ月間の無料トライアルがあるので、過去30年分の競馬データを無料で手に入れることができます。

データ解析の進捗はタグ競馬データ解析で投稿しています。JVData To SQLiteが作るデータベースの使い方、SQLスクリプト、Rスクリプトを公開しています。


  1. netkeiba.comをスクレイピングしてレース結果をSQLiteに保存するscalaスクリプトをstockedgeさんが公開しているんですが、私の環境 Ubuntu14.04, Java1.7.0_101だとエラーが出て動きませんでした。このscalaスクリプトはcollecturl, scrapehtml, extract, genfeatureの4コマンド構成でextractで正規表現とリターンのエラーが出ます。ダウンロードしたhtmlファイルにはEUC-JPエンコードと書かれているのに実際はUTF-8エンコードになっていることが関係しているかもしれません。 ↩︎

Kosuke Maeda / まえだこうすけ

「機械学習で競馬予想して勝てるのか?」をテーマに活動中! QiitaにはR、VBAなどのTipsを投稿しています。