はじめに
みなさん競馬はお好きでしょうか?
私は今年から始めた初心者なのですが、様々な情報をかき集めて予想して当てるのは本当に楽しいですね!
最初は予想するだけで楽しかったのですが、『負けたくない』という欲が溢れ出てきてしましました。
そこで、なんか勝てる美味しい方法はないかな〜とネットサーフィンしていたところ、機械学習を用いた競馬予想というのが面白そうだったので、勉強がてら挑戦してみることにしました。
目標
競馬の還元率は70~80%程度らしいので、適当に買っていれば回収率もこのへんに収束しそうです。
なのでとりあえず、出走前に得られるデータを使って、回収率100パーセント以上を目指したいと思います!
設定を決める
一概に競馬予測するといっても、単純に順位を予測するのか、はたまたオッズを考えて賭け方を最適化するのかなど色々とあると思います。また、買う馬券もいろいろな種類があります。
今回は競馬の順位を、3着以内・中位・下位の3グループに分けて、多クラス分類を行う方針でいきたいと思います。
そして予想結果で1位になった馬の単勝式の馬券を買うことにします。理由としては、単勝式の還元率は三連単などの高額馬券が出やすいものなどと比べると高めに設定されているからです。(参考:ブエナの競馬ブログ〜馬券で負けないための知識)
また、人気やオッズの情報は特徴量に使わないで行こうと思います。
これはレース直前まで決定されない情報を使うのはどうなの?ということと、あと単純に人気馬を買うという予想になると面白くないと思ったからです。
(レース発走の約50分前に決定される馬体重のデータは特徴量として扱っています)
今回は東京競馬場でのレースにしぼって以下進めていきたいと思います。
なお競馬場を絞る理由ですが、アルゴリズムが貧弱なのとtime.sleepを1秒いれていることにより、レースデータのスクレイピングに時間がかかるためです。(2008~2019のデータを集めるのに50時間ぐらいかかりました...)
面倒ですが、時間さえかければ全ての競馬場についてデータを集められます。
手順
- レースデータをこちらのサイト(netkeiba.com)からスクレイピングして集める。
- データの前処理を行う。
- LightGBM で学習を行いモデルを作成する。
- 作成したモデルを使って、1年間の回収率がどうなっているかを確認する。
スクレイピング
こちらのサイト(netkeiba.com)からスクレイピングして集める。
スクレイピングを行うに当たってrobots.txt(そもそもなかった)や利用規約を読んだ限りでは、大丈夫そうだったので負荷のかけすぎに注意を払いながら行いました。
スクレイピングの方法については以下の記事を参考にさせていただきました。
データを集めた結果はこんなかんじです。
スクレイピングをする際に、過去3レースの成績がない馬については情報を削除しています。
過去の情報がないものに対しては、未来を予測することはできないと考えるからです。
また、地方や海外で走った馬についてはタイム指数などが欠けている場合がありますが、その部分は平均値で埋めることにしています。
特徴量
今回、特徴量として扱ったのは以下の項目です。
当日のデータ
変数名 | 内容 |
---|---|
month | 何月開催か |
kai | 第何回目か |
day | 開催何日目か |
race_num | 何Rか |
field | 芝orダート |
dist | 距離 |
turn | どっち回りか |
weather | 天気 |
field_cond | 馬場状態 |
sum_num | 何頭立てか |
prize | 優勝賞金 |
horse_num | 馬番 |
sex | 性別 |
age | 年齢 |
weight_carry | 斤量 |
horse_weight | 馬体重 |
weight_change | 馬体重の変化量 |
l_days | 前走から何日経過したか |
過去3レースのデータ(01→前走、02→2レース前、03→3レース前)
変数名 | 内容 |
---|---|
p_place | 開催場所 |
p_weather | 天気 |
p_race_num | 何Rか |
p_sum_num | 何頭立てか |
p_horse_num | 馬番 |
p_rank | 順位 |
p_field | 芝orダート |
p_dist | 距離 |
p_condi | 馬場状態 |
p_condi_num | 馬場指数 |
p_time_num | タイム指数 |
p_last3F | 上がり3ハロンのタイム |
前処理
タイムを秒数表記にしたり、カテゴリ変数をラベルエンコードしただけです。
以下は例として天気をラベルエンコードするコードです。
num = df1.columns.get_loc('weather')
for i in range(df1['weather'].size):
copy = df1.iat[i, num]
if copy == '晴':
copy = '6'
elif copy == '雨':
copy = '1'
elif copy == '小雨':
copy = '2'
elif copy == '小雪':
copy = '3'
elif copy == '曇':
copy = '4'
elif copy == '雪':
copy = '5'
else:
copy = '0'
df1.iat[i, num] = int(copy)
df1['weather'] = df1['weather'].astype('int64')
このように各カテゴリ変数をラベルエンコードしていきます。
LabelEncoderを使えばもっと簡単にできそうだと思いましたが、複数のデータファイル間で、変換される数字と変数との互換を統一できなさそうだったので、使いませんでした。
また、今回用いる機械学習フレームワークのLightGBMは弱識別器に決定木を用いているらしいので、標準化をする必要はないです。(参考:LightGBM 徹底入門)
予測モデル
勾配ブースティングのフレームワークであるLightGBMでモデルを作ります。これを選んだ理由ですが、高速で、非deepでは一番強い(らしい)からです。
そして予測方法ですが、今回は3着以内、3着以内を除く中間1/3、下位1/3という3つのグループのどれかに分類する、多クラス分類にしました。例えば15頭立ての場合、1~3着がグループ0、4~8着がグループ1、9~15着がグループ2となります。
使い方は以下のサイトを参考にさせていただきました。
(参考:【初心者向け】LightGBM (多クラス分類編)【Python】【機械学習】)
学習用データと検証用データは以下のようにします。
学習用データ・検証用データ | テストデータ |
---|---|
Tokyo_2008_2018 | Tokyo_2019 |
学習用データをtrain_test_splitにて訓練用データとモデル評価データに分割します。
パラメータのチューニングは特におこなっていません。
train_set, test_set = train_test_split(keiba_data, test_size=0.2, random_state=0)
#訓練データを説明変数データ(X_train)と目的変数データ(y_train)に分割
X_train = train_set.drop('rank', axis=1)
y_train = train_set['rank']
#モデル評価用データを説明変数データ(X_test)と目的変数データ(y_test)に分割
X_test = test_set.drop('rank', axis=1)
y_test = test_set['rank']
# 学習に使用するデータを設定
lgb_train = lgb.Dataset(X_train, y_train)
lgb_eval = lgb.Dataset(X_test, y_test, reference=lgb_train)
params = {
'task': 'train',
'boosting_type': 'gbdt',
'objective': 'multiclassova',
'num_class': 3,
'metric': {'multi_error'},
}
model = lgb.train(params,
train_set=lgb_train, # トレーニングデータの指定
valid_sets=lgb_eval, # 検証データの指定
verbose_eval=10
)
動かしてみる
正答率は約54%ということになりました。半分以上は当てているということですね。
パラメータをいじってもあまりこの数値は変わらなかったので、今回はこのままいきます。
検証
2019年の1年間、東京競馬場で検証したデータを載せていきます。
ここで、条件として
- 1レースにつき100円で単勝を買う。(当り = オッズ × 100 - 100 or 外れ = -100 )
- 1レース中にデータが残っている馬数が出走頭数の半分以下の場合は買わない。(±0)
としています。
2つ目の条件がある理由ですが、これは2歳戦のような過去のレースデータが3戦以上ある馬が1頭だけといった、ほぼ確実に外れるレースを除外するためです。
以下が結果のグラフです。
良い感じじゃん
正直こんなに簡単に回収率が100%を超えるとは思いませんでした。
的中率も26%程度とボチボチ当ててくれています。
2つ目の条件により、100レースほど賭けないレースがでてしまいましたが、およそ8割のレースに参加してこの回収率なら文句なしではないでしょうか?
せっかくなので他の馬券でも検証してみたいと思います。
なお3連単だけは3頭BOX買い(6通り)をする前提で行っています。
結構良いですね...!馬単に関しては回収率200%近くと素晴らしい結果になっています。
ただ的中率が低い代わりにリターンが大きい馬券は、回収率のブレが大きくなるので参考程度に留めておきたいと思います。
不満点としては、3位以内に入るかどうかで評価しているにも関わらず、複勝の回収率が100%を切っている点ですかね。
この辺をどうにかしていきたいと思います。
さらに条件を付け加えてみる
今のところ馬券を買う条件は頭数のみでしたが、実用的な使い方では全レース買うというよりかは、予想数値の良し悪しで決めることになると思います。
そこで新たに次の条件を付け加えてみたいと思います。
・グループ0に分類される予測数値1位と2位の差が0.1以上の場合のみ買う。
つまりこういう場合のみ買い
こういう時は買わない
ということです。
この条件にする理由ですが
1. 3位以内に入るかどうかの予測数値は、強い馬が多かったり、出走頭数が少ないと大きくなりがちになるので、単なる数値の大小では予測しにくい。
2. 予測数値に開きがあれば、その馬が対象レースにおいてはかなり強いであろうことが予想される。
からです。
めちゃくちゃ良い感じじゃん
的中率は26%→39%に、回収率は130%→168%へと大幅に改善されました。
対象レースはさらに250レース程減って、年間100レースに絞られましたが、それでも1/4は参加していると考えると、この回収率は良いと思います。
他の馬券も一応みてみます。
良いですね!
特筆すべきなのは複勝の的中率が70%を超え、回収率も100%を超えている点です。
1番人気の馬の複勝圏内率は約60~65%程度(参考:開発者ブログ | 株式会社AlphaImpact)らしいので、これは非常に良いと言えます。
特徴量について
モデルを作ったときの特徴量の重要度についてもみていくことにします。
タイム指数がかなり重要な特徴量として扱われているのが分かります。当たり前ですが、過去のレースで良い走りをした馬が勝つ確率が高いということですね。
意外だったのは、前回のレースから何日経っているかが、上がりタイムや馬体重と同じくらい重要な特徴量として扱われている点です。競馬予想をしている人の中で、ローテーションを重視している人はあまり見かけないので驚きました。
また、これは中2週ローテを強行して負けたアーモンドアイと重なるところがあって面白いですね。まあ、空きが短いから指数が悪くなっているのかどうかは、全くわからないのですが笑
追記
実験的に、作成したプログラムでの予想をブログで公開しています。(無料です)
果たしてちゃんと当たるのかどうかは分かりませんが、興味のある方は覗いてみてください!
https://ai-umatan.hatenablog.com
ただ外れても私は何の責任も負いませんので、あくまでも馬券購入は自己責任でお願いします。
おわりに
昨今、競馬AIはサービスとして運営しているサイトもあったり、ドワンゴが主宰の電脳賞があったりと、着々と盛り上がっているように思えます。そんな中で自分も機械学習を使った競馬予想を実践することができとても楽しかったです。
ただ未来を完璧に予想することはできないので、このモデルを使ったからといって確実に勝てるかと言えば、そういうわけではありません。今年、来年のレースでは惨敗してしまうこともあり得るでしょう。
こういうものに期待しすぎるのもよくないとは思いますが、競馬プログラムで稼いでいる人もいるので、夢はあるのかなと思います。
コメント
@ArumetaMリンクをコピー このコメントを報告 7
@Mshimiaリンクをコピー このコメントを報告 2
@ArumetaMリンクをコピー このコメントを報告 2
@shin1007リンクをコピー このコメントを報告 2
@jaso0125リンクをコピー このコメントを報告 1
@Mshimiaリンクをコピー このコメントを報告 1
@Mshimiaリンクをコピー このコメントを報告 0
@Mshimiaリンクをコピー このコメントを報告 0
@Chishikiリンクをコピー このコメントを報告 2
@Mshimiaリンクをコピー このコメントを報告 0 サービス利用規約に基づき、このコメントは削除されました。
@TomoyaFujita2016リンクをコピー このコメントを報告 1
@Mshimiaリンクをコピー このコメントを報告 1
@toshikameリンクをコピー このコメントを報告 1
@Mshimiaリンクをコピー このコメントを報告 0
@kasshy1185リンクをコピー このコメントを報告 1
@Mshimiaリンクをコピー このコメントを報告 0
@kasshy1185リンクをコピー このコメントを報告 1
全体のデータに対して標準化は要らないですが,レース内での標準化は個人的に効くのかなって思ったりします.
今後東京以外にも集めるなら残しておいた方が楽だとは思いますけど,特徴量で開催場所のplaceは東京のみでしたらすべて東京なので不必要なものですね.
過去3レースのデータないものを除外しているように見受けられるのですが,LightGBMなら欠損値も可能なので除外しないで扱ってみてはどうでしょうか?
用語ですが機械学習では検証データは一般的にハイパーパラメータ調整のためのデータとして使われることが多いです.よって2008~2018学習データ,検証データ,2019テストデータの方がしっくりきます.記事内で使っている訓練データ=学習データ,モデル評価データ=検証データ,検証データ=テストデータで脳内置換して読んでいました.
train_test_splitではラベルの偏りなどを見ずに分割するので改善した方が良いかもしれません.引数にstratifyを指定するとデータの比率が保たれるようですね.
3連単のBOX買いですが,当たる条件は3連複と同じですよね?LightGBMなら各ラベルの確率を出力するのでレースごとに1位になる確率を並べることができるはずです.BOX買いではなくソートして1位,2位,3位を買うようにしたら的中率は下がりますが,回収率は3連複を上回るかもしれません.ただデータ総数が少ないので東京競馬場以外も増やさないと参考にならない数字かもしれないですね.
他の場所を集めるのが時間的に厳しいようでしたら交差検証してみてはどうでしょうか?年度ごとの交差検証ならテーストの量も12倍に増えるのでその平均を使えばより信頼性の高い結果になると思います.lightGBMならパラメーターチューニングをしたとしても大した時間がかかるわけでは無いので比較的短時間で実験できると思います.
Importanceで優勝賞金が上位に来ていることが個人的に驚きでした.同じレース内で優勝賞金はすべて同じ値になるかと思うので,モデルが優勝賞金の大きさによって分岐することを学んでいるのですかね.
直前データを使わないのであれば自動化しなくても金曜日の夜などにまとめて手動で購入して長い期間で実際に運用してみると面白いと思います.実験しましたという記事は多いですが,実際に利益が出ましたと言った記事はあまり見ないのでそちらの方が注目度も上がると思います.実際に成功してる人は口外しないことが見ない理由ではあると思いますが...
@ArumetaM さん
コメントとアドバイス有難うございます! 私自身ど素人なので本当に助かります!
データを集める際に各レース毎で、標準化を行うということでよろしいでしょうか?
試してみたいと思います。
最初は全競馬場のデータを集めるつもりで始めていたので残っていましたが、確かにそうですね笑
修正しておきます。
新馬戦などの情報はただのノイズになると思ったので除外するようにしていたのですが、試してみる価値はあるかもしれないですね。
これは訂正しておきます。ご指摘有難うございます!
全く知らなかったです。試してみたいと思います。
3連単は他の馬券と違って、あまり1点買いをするようなものではないと感じたのでbox買いをする設定にしたのですが、検証という意味では1点のみにするというのもありですね。ただ1年間で的中0になるかもしれないですが笑
そのような方法があるのですね。確かに今回の実験では信頼性が皆無なので、試してみたいと思います。
直前のデータは使わないと書いてしまいましたが、正確ではありませんでした
馬体重とその増減はレース発走の約50分前にならないと分からないので、前日などにまとめて予想することはできないです。ただ馬体重については前走のものを使うなどして代用できる可能性はあります。(増減はどうしても無理ですが...)
予想したものを実際に購入するのは面白そうなので、行っていきたいと思います。ただ信用に値する期間の長さがどれくらいなのか分からないので、そこも問題ですが...
各レース毎で、標準化を行うという認識で合っています.
馬体重が重い馬は強いという仮説と馬体重が次のような3つのレースがあったとします.
[48,45,47,50][51,52,49,53][52,53,54,55]
すべて4番目の馬が勝ったときに1レコードずつの学習では50,53,55の馬が上位に来るとしたいわけですが,3つ目のレースでは53の馬は負けているので,単純に1レコードの絶対値ではレースの中で重い馬かどうかが不明です.
そこでレース毎に標準化を行えば[0.28,-1.39,-0.28,1.39][-0.17,0.51,-1.52,1.18][-1.34,-0.45,0.45,1.34]となり,4番目の馬がすべて1以上なら強い馬だとモデルに分かる特徴量になるのではと思います.
LightGBMが標準化の必要がないというのは数値をヒストグラムに内部的に直して計算を高速化するという認識のため,このようにモデルに認識させやすい特徴量を作るという点では別問題だと思っています.
新馬戦などの情報はただのノイズというのは真かどうかは分からないですね.特徴量として新馬戦かどうかの0,1の特徴量を与えて上げれば決定木で分岐して新馬戦かそうじゃないかというところで学習をしてくれると思います.こちらがノイズと思っているだけでモデル側からはそうではないと判断するかもしれないですし.
3連単の的中率0は大いにあり得ますね...ここはテストデータを増やすしかないですね.
過去データを使わないようにするなら2008~2012学習,2013検証,2014テストという形で5年学習1年検証1年テストを1年ずつずらして2014~2019までテストをするか,交差検証のように2008から2019の中で2008年をテスト,それ以外を学習というのを2008~2019まですべて行うかのパターンがあると思います.後者は2008年をテストするのに未来のデータを使っているのでよろしくない気もするのですが,仮に今2022~2030のデータがあったからと言って2021のレースが予測できるかと言ったらそうではないと思うので,個人的にはありだと思っています.ただ後者に関しては自信が無いので前者の学習,検証,テストを1年ずつずらす方でまずはやってみてはいかかでしょうか?
仮に直前データを使うとしてもレース前に自動購入のプログラムさえかければ可能ですね.しかし機械学習とは別のところなのでそれはそれで勉強が必要ですね.
すごいですね!パドックでの動きの映像から機械学習かけてみようかと思ってたのを思い出しました!
全特徴量の項目数は当日データの18項目と前走レースデータの11項目×3レース分の計51項目で学習モデルを作成したという認識で合っていますでしょうか?
@ArumetaM さん
なるほど...確かにそうですね。特徴量の標準化も試す必要がありますね。的確なアドバイス有難うございます!
確かにせっかく機械学習を使っているのに、自分の想像でデータを削ってしまうのはもったいなかったですね。こちらも時間ができたら試してみたいと思います。
交差検証についてはすぐに実行できるので取り組みたいと思います。とりあえず提示していただいた前者の方法でやってみることにします。
自動投票については勉強してみて、可能であれば実行に移したいと思います。
様々なアドバイスや知見をいただけて本当に助かります!
@shin1007 さん
ありがとうございます!ただ、サンプル数が少ないのであまり当てにならない実験であったなと今更思っていますが...
その発想はありませんでした。パドックの映像解析は新しくて面白そうですね!
@jaso0125 さん
その認識で間違い無いです。
ただ書き忘れていましたが、何月に行われたかも特徴量に入れております。
なので、この1つを足した計52項目で学習モデルを作ったことになります。
とても楽しく読ませて頂きました!
自分は、特徴量をスポーツ選手(特にゴルフ)にあてて、選手の状態やゲームの流れから、個々の上達度と勝利の方程式を導き出す作業を行っています。なかなか進みは悪いですが。。
特徴量や理論の組み立て方が参考になりました。地道な作業ですが、ここがちゃんとしないとブレブレになりますね^^;。
@Chishiki さん
コメントありがとうございます!
面白そうなことされてるんですね!特徴量選びなどかなり難しそうですがぜひ頑張ってください…!
少しでも参考になれたのならよかったです
とても勉強になる記事をありがとうございます!
気になったのですが、https://www.netkeiba.com/ でのタイム指数の取得はプレミアム会員になって取得されたのですか?
差し支えなければ具体的にどのページをスクレイピングしたか教えていただけますでしょうか!
@TomoyaFujita2016 さん
コメントありがとうございます。
はい、タイム指数部分はプレミアム会員になって取得いたしました。
スクレイピングした部分は各馬のデータベースのページです。
少しでも参考になれば幸いです。
わかりやすい説明でとても勉強になりました。ありがとうございます。
一点教えていただきたいのですが、「モデルを作ったときの特徴量の重要度」の一覧の中に"P_last3F01"、"P_last3F02"、"P_last3F03"という特徴量があるのですが、これはどういう内容なのでしょうか。
それなりに重要な特徴量に見える一方、ページの上の方で紹介されている特徴量のリストの中には同じ名前のものがなかったため、とても気になり、質問させていただきました。
よろしくお願いします。
@toshikame さん
遅くなってしまいましたが、コメントありがとうございます。
特徴量リストから抜け落ちていたみたいで、すみません。
p_last3Fは過去のレースにおける上がり3ハロンのタイムになっています。
これは競馬予想において、末脚の良さを評価する時によく使われる値です。
とても分かりやすくウキウキする内容ですね。
早速、写経を始めたのですが、一つご教示願えないでしょうか。
X_test作成時に'rank'のみをdropしていますが、これではp_time_num等が未来に漏洩しないのでしょうか。
ど素人なので頓珍漢な事を言っているかもしれませんが、何卒よろしくお願いいたします。
@kasshy1185 さん
返信がめちゃくちゃ遅くなってすみません...
確かにp_time_numなどから情報が漏洩している可能性も考えられますが、2008~2018年のデータでトレーニングしたモデルの正答率と、このモデルを用いた2019年のテストの正答率との間に異常な差はなかったので、問題ないかと考えています。
(もしもp_time_num等からリークされており、そのデータを予測にかなり反映しているとしたら、2019年という未来の予測においては精度が著しく落ちると思われるため)
説明が分かりにくく、回答になっているか不安ですが、どうでしょうか。
ご回答ありがとうございます。
リークが無い事は検証済みとの事、承知しました。まだまだ勉強中なので参考にさせて頂きます。