競馬AIうまたんの技術ブログ

競馬AIうまたんの技術記事や競馬に関するデータの考察などを行っていきます!

機械学習で競馬の回収率100%超えを達成した話

この記事はQiitaに投稿された記事をちょこちょこ改変しながら再投稿したものです。 (https://qiita.com/Mshimia/items/6c54d82b3792925b8199)
今公開しているものは、ここからかなり色々と変更していますが、基本的な考え方は変わっていないので、競馬AIを作ってみたいという方の参考になれば嬉しいです。

はじめに

みなさん競馬はお好きでしょうか? 私は今年から始めた初心者なのですが、様々な情報をかき集めて予想して当てるのは本当に楽しいですね!

最初は予想するだけで楽しかったのですが、『負けたくない』という欲が溢れ出てきてしましました。

そこで、なんか勝てる美味しい方法はないかな〜とネットサーフィンしていたところ、機械学習を用いた競馬予想というのが面白そうだったので、勉強がてら挑戦してみることにしました。

目標

競馬の還元率は70~80%程度らしいので、適当に買っていれば回収率もこのへんに収束しそうです。

なのでとりあえず、出走前に得られるデータを使って、回収率100パーセント以上を目指したいと思います!

設定を決める

一概に競馬予測するといっても、単純に順位を予測するのか、はたまたオッズを考えて賭け方を最適化するのかなど色々とあると思います。また、買う馬券もいろいろな種類があります。

今回は競馬の順位を、3着以内・中位・下位の3グループに分けて、多クラス分類を行う方針でいきたいと思います。

そして予想結果で1位になった馬の単勝の馬券を買うことにします。理由としては、単勝式の還元率は三連単などの高額馬券が出やすいものなどと比べると高めに設定されているからです。(参考:ブエナの競馬ブログ〜馬券で負けないための知識)

また、人気やオッズの情報は特徴量に使わないで行こうと思います。 人気やオッズを特徴量に入れると、おそらくそちらに引っ張られAIの予想として面白くない結果になると思ったからです。

今回は東京競馬場でのレースにしぼって以下進めていきたいと思います。 なお競馬場を絞る理由ですが、アルゴリズムが貧弱なのとtime.sleepを1秒いれていることにより、レースデータのスクレイピングに時間がかかるためです。(2008~2019のデータを集めるのに50時間ぐらいかかりました...)

面倒ですが、時間さえかければ全ての競馬場についてデータを集められます。

手順

  1. レースデータをこちらのサイト(netkeiba.com)からスクレイピングして集める。
  2. データの前処理を行う。
  3. LightGBM で学習を行いモデルを作成する。
  4. 作成したモデルを使って、1年間の回収率がどうなっているかを確認する。

スクレイピング

こちらのサイト(netkeiba.com)からスクレイピングして集める。 スクレイピングを行うに当たってrobots.txt(そもそもなかった)や利用規約を読んだ限りでは、大丈夫そうだったので負荷のかけすぎに注意を払いながら行いました。 スクレイピングの方法については以下の記事を参考にさせていただきました。

データを集めた結果はこんなかんじです。

f:id:AI_umatan:20211006211130p:plain

スクレイピングをする際に、過去3レースの成績がない馬については情報を削除しています。 過去の情報がないものに対しては、未来を予測することはできないと考えるからです。

また、地方や海外で走った馬についてはタイム指数などが欠けている場合がありますが、その部分は平均値で埋めています。

特徴量

今回、特徴量として扱ったのは以下の項目です。 開催場所に関しては、今回は東京競馬場のデータのみしか扱わないことから、特徴量からは省いています。 当日のデータ

変数名 内容
month 何月開催か
kai 第何回目か
day 開催何日目か
race_num 何Rか
field 芝orダート
dist 距離
turn どっち回りか
weather 天気
field_cond 馬場状態
place 開催場所
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
)


動かしてみる

実際に動かしてみました。

f:id:AI_umatan:20211006211625p:plain

正答率は約54%ということになりました。半分以上は当てているということですね。 パラメータをいじってもあまりこの数値は変わらなかったので、今回はこのままいきます。

検証

2019年の1年間、東京競馬場で検証したデータを載せていきます。

ここで、条件として

  • 1レースにつき100円で単勝を買う。(当り = オッズ × 100 - 100  or  外れ = -100 )
  • 1レース中にデータが残っている馬数が出走頭数の半分以下の場合は買わない。(±0)

としています。 2つ目の条件がある理由ですが、これは2歳戦のような過去のレースデータが3戦以上ある馬が1頭だけといったレースを除外するためです。

以下が結果のグラフです。

f:id:AI_umatan:20211006211301p:plain


😊良い感じじゃん😊


正直こんなに簡単に回収率が100%を超えるとは思いませんでした。

的中率も26%程度とボチボチ当ててくれています。

2つ目の条件により、100レースほど賭けないレースがでてしまいましたが、およそ8割のレースに参加してこの回収率なら文句なしではないでしょうか?


せっかくなので他の馬券でも検証してみたいと思います。
なお3連単だけは3頭BOX買い(6通り)をする前提で行っています。

f:id:AI_umatan:20211006211229p:plain
f:id:AI_umatan:20211006211309p:plain

f:id:AI_umatan:20211006211312p:plain

f:id:AI_umatan:20211006211213p:plain

f:id:AI_umatan:20211006211226p:plain

結構良いですね...!馬単に関しては回収率200%近くと素晴らしい結果になっています。 ただ的中率が低い代わりにリターンが大きい馬券は、回収率のブレが大きくなるので参考程度に留めておきたいと思います。

不満点としては、3位以内に入るかどうかで評価しているにも関わらず、複勝の回収率が100%を切っている点ですかね。 この辺をどうにかしていきたいと思います。

さらに条件を付け加えてみる

今のところ馬券を買う条件は頭数のみでしたが、実用的な使い方では全レース買うというよりかは、予想数値の良し悪しで決めることになると思います。
そこで新たに次の条件を付け加えてみたいと思います。

・グループ0に分類される予測数値1位と2位の差が0.1以上の場合のみ買う。

つまりこういう場合のみ買い f:id:AI_umatan:20211006211232p:plain こういう時は買わない f:id:AI_umatan:20211006211258p:plain ということです。

この条件にする理由は 1. 3位以内に入るかどうかの予測数値は、強い馬が多かったり、出走頭数が少ないと大きくなりがちになるので、単なる数値の大小では予測しにくい。 2. 予測数値に開きがあれば、その馬が対象レースにおいてはかなり強いであろうことが予想される。 だからです。

以下、この条件で検証した結果です。 f:id:AI_umatan:20211006211248p:plain

😆めちゃくちゃ良い感じじゃん😆

的中率は26%→39%に、回収率は130%→168%へと大幅に改善されました。 対象レースはさらに250レース程減って年間100レースに絞られましたが、それでも1/4は参加していると考えると、この回収率は良いと思います。
他の馬券も一応みてみます。 f:id:AI_umatan:20211006211245p:plain f:id:AI_umatan:20211006211251p:plain f:id:AI_umatan:20211006211254p:plain f:id:AI_umatan:20211006211235p:plain f:id:AI_umatan:20211006211238p:plain

良いですね! 特筆すべきなのは複勝の的中率が70%を超え、回収率も100%を超えている点です。 1番人気の馬の複勝圏内率は約60~65%程度(参考:開発者ブログ | 株式会社AlphaImpact)らしいので、これは非常に良いと言えます。

特徴量について

モデルを作ったときの特徴量の重要度についてもみていくことにします。 f:id:AI_umatan:20211006211304p:plain

タイム指数がかなり重要な特徴量として扱われているのが分かります。当たり前ですが、過去のレースで良い走りをした馬が勝つ確率が高いということですね。

意外だったのは、前回のレースから何日経っているかが、上がりタイムや馬体重と同じくらい重要な特徴量として扱われている点です。近年の強い馬はローテを考え、あまり使い詰めていなかったりするので、そういう部分が効いてきているのかもしれませんね。

追記

実験的に、作成したプログラムでの予想をブログで公開しています。(無料です) 果たしてちゃんと当たるのかどうかは分かりませんが、興味のある方は覗いてみてください! https://ai-umatan.hatenablog.com ただ外れても私は何の責任も負いませんので、あくまでも馬券購入は自己責任でお願いします。🙇‍♂️

おわりに

昨今、競馬AIはサービスとして運営しているサイトもあったり、ドワンゴが主宰の電脳賞があったり(最近はやってないけど…)と、着々と盛り上がっているように思えます。そんな中で自分も機械学習を使った競馬予想を実践することができとても楽しかったです。

ただ未来を完璧に予想することはできないので、このモデルを使ったからといって確実に勝てるかと言えば、そういうわけではありません。今年、来年のレースでは惨敗してしまうこともあり得るでしょう。

こういうものに期待しすぎるのもよくないとは思いますが、競馬プログラムで稼いでいる人もいるので、夢はあるのかなと思います。

AIうまたんアップデートのお知らせ(2021年)

こちらの記事をはてなブログの方にも移管しました。 note.com

今までは過去3レースの戦績がない馬については予想から除外してきましたが、この度のアップデートでこれらの馬についても予想をすることにしました。なお注意点は以下の通りです。

  • 過去のデータで欠落しているものはNone(欠損値)とします。

  • 上の条件により、過去のレース戦績が揃っていない馬は、もしかすると評価が低く(高く)なってしまうかもしれないです。(検証できていないので評価が不当に高くなるか、低くなるかは不明)

  • 新馬戦は今まで通り配信しません。

  • 1/11~の投稿分では、過去の戦績が足りない馬には*印を馬名の後ろにつけたいと思います。*印は足りないレース分つけます。
    (例)過去の戦績が1レースしかない場合、3-1=2で2個の*印がつき、馬名**のようになります。

以上です。これからもどうぞよろしくお願いします。

競馬の格言を統計学で検証 !『夏は牝馬』は本当?

始めに

こんにちは!うまたんです。 いつもは自作の競馬AIで中央競馬の予想をしています。

ai-umatan.hatenablog.com

今回は競馬の格言が本当なのかどうかを統計学を用いて検証していきたいと思います。 古くからの競馬ファンはもちろん、最近競馬始めたよって方でも一度くらいは聞いたことがあるかもしれない格言に 『夏は牝馬 というものがあります。

これに関しては、牝馬の方が暑さに強いからだとかコースが平坦なローカルで行われるからだとか色々言われています。 これは本当なのでしょうか?

ちょっと検索すれば、夏の牝馬の勝率や回収率などでこの格言を検証しているブログ等がたくさん出てきます。しかし数値だけをなんとなく比較して、この格言は正しいとか間違っていると結論づけられているので、信頼性は少し欠けるような気がします。

そこで今回はちゃんと統計学を使って検証してみたいと思います。(とは言っても私は統計学の初学者なので間違っている点もあると思います。詳しい方は是非指摘してください🙇)

理論

今回は仮説検定の一種であるカイ二乗検定(χ2検定 )を使うことにします。夏に好走することと牝馬であることの、独立性検定となります。

カイ二乗検定とは?

帰無仮説が正しければ検定統計量が漸近的にカイ二乗分布に従うような統計的検定法である。(wikipediaより) うーん…分からないですねwもう少し説明します。
まず独立性の検定とはある2つの属性AとBの間に関連があるかどうかを調べることを指します。ではどうやって調べるのかというと、立証したい意見と反対の仮説を立てて、それを棄却(否定)することによって行います。

帰無仮説と対立仮説

棄却するために用意する仮説を帰無仮説と言い、その反対の仮説(自分が示したいもの)を対立仮説と言います。
帰無仮説 H_0 :2つの変数は独立である
対立仮説 H_1 :2つの変数は独立ではない
となります。今回の場合だと
帰無仮説 H_0 :夏に好走すること牝馬であることは独立である
対立仮説 H_1 :夏に好走すること牝馬であることは独立ではない
となりますね。「独立である」とは関係性がないということなので、帰無仮説では「夏は牝馬という格言は意味がない」ということになります。

期待度数を求める

帰無仮説の下で期待度数というものを求めます。期待度数というのは理論値みたいなものだと思ってもらえば大丈夫です。つまり二つの変数の間に何も関連がなかったと仮定するとこうなるだろうという値です。i行j列における期待度数  E_{ij} は次の式で求めることができます。

\displaystyle{
E_{ij} = \frac{n_{i.}n_{.j}}{N}
}

なぜこの式で求められるのか気になる方はこの記事を参考にしてみてください。
(参考 : 独立性のカイ二乗検定 例題を用いてわかりやすく解説 | AVILEN AI Trend)

もし、帰無仮説が正しければ期待度数と実測値との差は小さくなると予想できます。

検定統計量を求める

続いて検定統計量 \chi^{2}  を求めます。検定統計量は期待度数と実測値のズレを2乗したものを期待度数で割ったものになります。すなわち

\displaystyle{
\chi^{2} = \sum_{n = 1}^{2}  \sum_{n = 1}^{2}  \frac{(n_{ij}-E_{ij})^2}{E_{ij}}
}

となります。

カイ二乗分布表からp値を求め、帰無仮説を棄却するか考える

求めた検定統計量とカイ二乗分布表からp値を求めます。p値は期待度数と実測値のズレがどのくらいの確率で起きるものなのかを示す値だと思ってもらえれば大丈夫です。p値が有意水準より小さければ帰無仮説は棄却されることになります。ここで有意水準というのは、帰無仮説を棄却する基準となる確率のことです。
例えば有意水準が5%で、p値が0.05未満であった時、95%の確率で帰無仮説を棄却するのは正しいということになります。統計では100%間違っているとか正しいとかは断言できないので、確率によって表します。したがって有意水準は5%や1%といった小さな値を用います。

実際に検定しよう!

この表は2010~2020年の7,8月におけるレースの結果(牝馬限定戦を除く)から作成しました。 帰無仮説と対立仮説は
帰無仮説 H_0 :夏に好走すること牝馬であることは独立である。
対立仮説 H_1 :夏に好走すること牝馬であることは独立ではない。
とします。ここで好走=3着以内と考えることにします。有意水準 \alpha = 0.05  で独立性検定を行います。
期待度数を求めると次のようになります。

この表から \chi^{2}  を計算すると \chi^{2}  = 2.647713 となります。そしてp値を求めると p = 0.103699  となります。p値が有意水準を超えてしまっているので、帰無仮説は棄却できませんでした。
すなわち、夏に好走すること牝馬であることに関連があるかは分かりません。(ここで注意なのですが、帰無仮説が棄却できなかったからといって帰無仮説が正しいとは言えません)

まとめ

長々と書きましたが、結局のところ「夏は牝馬」という格言の正しさを示すことはできませんでした。残念😥
関連性が示せれば来年の夏に使える!と思って始めたのですが、そう上手くはいかないものですね…
他にも競馬の格言はたくさんあるので、今後もいろいろ試してみたいと思います。こんな格言があるよ!っていうのがあればコメント等してもらえると嬉しいです😆 また、検定の方法や説明に不備があればご指摘していただけると幸いです。

このブログについて

はじめに

このブログでは競馬AIうまたんに関して技術的な考察、および競馬に関するデータの考察などを書いていきます。

競馬予想に関してはAIうまたんの競馬予想ブログで引き続き公開していますので、AI予想が見たい方はそちらに移動してください。

プライバシーポリシー

 

こんにちは、管理人のうまたんです。下記にプライバシーポリシーを掲載いたしましたので、ご一読願います。

 

 

当サイトに掲載されている広告について

当サイトでは、第三者配信の広告サービス(もしもアフィリエイト, A8.net)を利用しています。
このような広告配信事業者は、ユーザーの興味に応じた商品やサービスの広告を表示するため、当サイトや他サイトへのアクセスに関する情報 『Cookie』(氏名、住所、メール アドレス、電話番号は含まれません) を使用することがあります。
またGoogleアドセンスに関して、このプロセスの詳細やこのような情報が広告配信事業者に使用されないようにする方法については、こちらをクリックしてください。

 

当サイトが使用しているアクセス解析ツールについて

当サイトでは、Googleによるアクセス解析ツール「Googleアナリティクス」を利用しています。
このGoogleアナリティクスはトラフィックデータの収集のためにCookieを使用しています。
このトラフィックデータは匿名で収集されており、個人を特定するものではありません。

この機能はCookieを無効にすることで収集を拒否することが出来ますので、お使いのブラウザの設定をご確認ください。
この規約に関して、詳しくはこちら、またはこちらをクリックしてください。

当サイトへのコメントについて

当サイトでは、スパム・荒らしへの対応として、コメントの際に使用されたIPアドレスを記録しています。

これはブログの標準機能としてサポートされている機能で、スパム・荒らしへの対応以外にこのIPアドレスを使用することはありません。

また、メールアドレスとURLの入力に関しては、任意となっております。
全てのコメントは管理人である私が事前にその内容を確認し、承認した上での掲載となりますことをあらかじめご了承下さい。

加えて、次の各号に掲げる内容を含むコメントは管理人の裁量によって承認せず、削除する事があります。

  • 特定の自然人または法人を誹謗し、中傷するもの。
  • 極度にわいせつな内容を含むもの。
  • 禁制品の取引に関するものや、他者を害する行為の依頼など、法律によって禁止されている物品、行為の依頼や斡旋などに関するもの。
  • その他、公序良俗に反し、または管理人によって承認すべきでないと認められるもの。

免責事項

当サイトで掲載している画像の著作権・肖像権等は各権利所有者に帰属致します。権利を侵害する目的ではございません。記事の内容や掲載画像等に問題がございましたら、各権利所有者様本人が直接メールでご連絡下さい。確認後、対応させて頂きます。

当サイトからリンクやバナーなどによって他のサイトに移動された場合、移動先サイトで提供される情報、サービス等について一切の責任を負いません。

当サイトのコンテンツ・情報につきまして、可能な限り正確な情報を掲載するよう努めておりますが、誤情報が入り込んだり、情報が古くなっていることもございます。

当サイトに掲載された内容によって生じた損害等の一切の責任を負いかねますのでご了承ください。

プライバシーポリシーの変更について

 

当サイトは、個人情報に関して適用される日本の法令を遵守するとともに、本ポリシーの内容を適宜見直しその改善に努めます。

修正された最新のプライバシーポリシーは常に本ページにて開示されます。

 

管理人:うまたん