見出し画像

競馬データを機械学習(LightGBM)させ競馬予想をしてみた。

はじめに

遅くなりましたが新年あけましておめでとうございます。hagiと申します。
今年は学んだことをアウトプットしていこうと思いnoteを始めました!

簡単に自己紹介をさせていただきます。
関西生まれ、関西育ち、ラグビーが好きな29歳です。
趣味はスポーツ、サウナ、映画鑑賞、公園散歩、筋トレ。

現在は職業訓練制度を活用してデータサイエンスを5か月間(2022年8月~2022年12月)学び、今後も継続して学びつつ、その領域で仕事をしていくため活動をしています。

データサイエンスに興味を持ったきっかけはまたお話できたらと思うのですが、今回は学んだことを活かし、実際に機械学習を実装してみようということで少し前になりますが、昨年11月に競馬予測をしてみたのでアウトプットしたいと思います。

概要

目的

11月に行われたG1レース(マイルチャンピオンシップ)で3着以内に入る馬を機械学習により予想します。

仮説

該当レースに特化した条件(競馬場、距離、馬場状態)のデータを収集し、適切な方法で機械学習させれば高い精度で予測できるのでは?

検証方法

決定木モデルの中でも勾配ブースティングを使用したLightGBMを使用し、3着以内に入るかどうかの2値分類で予想します。
学習させるデータは競馬場、距離、馬場状態を該当レースに合わせた形でフィルターをかけ、30年分(1993年~2022年)集めます。
実際のレース結果と見比べ、どの程度予想できていたか検証します。

今回使用するLightGBMとは

モデリングに入る前にまずLightGBMについて簡単に解説させていただきます。LightGBM は、2016年に米マイクロソフト社が公開した機械学習手法で勾配ブースティングに基づく決定木分析(ディシジョンツリー)です。

データ分析コンペ「Kaggle」の上位6割以上が LightGBM を用いていると言われ、昨今多くの人に使われているようです。

LightGBM 公開前の勾配ブースティングは、「XGboost」が主流でしたが、LightGBMがどういう点で優れているのか、アンサンブル学習の説明から入り、その特徴を簡単に解説させていただきます(そもそも機械学習が何か、決定木分析が何かという説明は割愛させていただきます)。

アンサンブル学習について

決定木分析はそれ単独では精度も高くなく、過学習しやすいという欠点があります(弱学習器といいます)。しかし複数の決定木(弱学習器)を組み合わせ学習することによってその欠点を克服し、大幅に精度を改善することができます。これをアンサンブル学習といいます。

アンサンブル学習には主に3つの種類「バギング」「ブースティング」「スタッキング」があり、今回「バギング」「ブースティング」について取り上げます。

・バギングとは・・・
ブートストラップという方法で重複を許す形でランダムに複数にサンプルを抽出し、それぞれのデータ群で学習させることをバギングといいます。そして最終的な出力結果はその平均や多数決をとることが一般的です。そして並列的に処理を行うことが特徴です。


バギング

・ブースティングとは・・・
一方でブースティングは一つの学習モデルからうまく予測できなかった部分に重みづけをし、さらにモデル学習を繰り返すことで精度を上げていくことを言います。こうして複数のモデルを作成し、組み合わせて最終的な予測とします。この手法は直列的に処理をしていくことが特徴でバギングとの大きな違いになります。

さらに予測値と実績値の誤差を損失関数として定義し、「勾配降下法」により最小化するよう学習する方法が「勾配ブースティング」です。ブースティングと同様に、誤差に対する学習を繰り返すことで精度を高めていきます。そして今回使用した「LightGBM」はこの勾配ブースティング手法の一つです。他には「Xgboost」「Catboost」があります。


ブースティング

LightGBMの特徴

勾配ブースティングには、「予測精度は高いが、計算時間が長い」という特徴があります。そこで当時、勾配ブースティングの主流であった「XGboost」に対し、LightGBM は「予測精度を保ったまま計算時間を大きく削減できる」という特徴が注目を集め、急速に広まりました。

ではLightGBM では、どのようにして計算時間の削減を可能にしたのでしょうか。ここではXGboost と比較して、以下の2点を取り上げます。

① 「Level-wise」から「Leaf-wise」に変更
② 葉の分岐点を探す際にヒストグラムを採用している

① 「Level-wise」から「Leaf-wise」に変更について

一般的な決定木分析のアルゴリズムでは「Level-wise」というやり方を使っています。これは一つの階層での分岐の計算がすべて終わってから次の階層に移っていくものになります。


Level-wise

一方で「Leaf-wise」では損失が小さくなるようなノードから優先的に分割していく方法をとります。これにより無駄な計算をすることなく、効率的に学習を進めることができ、処理速度が短縮されているのです。


Leaf-wise

② 「葉の分岐点を探す際にヒストグラムを採用している」について

通常ノードを分割する際には、あらかじめ特徴量をソートしておき、分割可能なすべての点を網羅的に評価し、最適な分割点を探します。これではデータ量×特徴量の分だけ計算量があり、またブースティングでは複数の決定木を作成するので膨大な計算になります。実際、計算の大部分はこの分岐点の探索に時間をかけているようです。
LightGBMではこの連続値の特徴量をヒストグラム化(離散化)し、いくつかの値を一つのbinとすることでbinの数だけ分岐点を評価すればよいので計算量が大幅に削減することができるのです。


ヒストグラム

他に計算量を削減するための工夫として

・Gradient-based One-Side Sampling (GOSS): 勾配が小さいデータはランダムサンプリングする
・Exclusive Feature Bundling (EFB): 複数の特徴量をbundleしてまとめて一つの特徴量のように扱う

などもありますが今回は割愛させていただきます。

またアンサンブル学習やLightGBMの詳しい解説については以下が分かりやすく参考にさせていただきました。

アンサンブル学習を超わかりやすく解説【機械学習入門30】
LightGBMを超わかりやすく解説(理論+実装)【機械学習入門33】

今回実施したモデリング

やっと本題に入りますが、どのようにして競馬データの分析・予測をしたか説明させていただきます。
今回は下記の2点を変更させながら予測結果への影響と比較を行いました。

①カテゴリ変数の扱い
②ハイパーパラメータチューニングの有無

①カテゴリ変数の扱いについて
LightGBMでは自動的にカテゴリ変数を認識し、処理してくれる機能があります。一方でパラメーターの一つに「Categorical Feature」というものがあります。ここでカテゴリ変数を明示的に指定することで最適な形で学習することができるようです。
ではどういった変数をカテゴリ変数として指定するがよいのでしょうか。

1,high-cardinalityな(=要素が多い)カテゴリ変数
high-cardinalityなものを扱う場合には木を深くする必要がありますが、「Categorical Feature」に指定することでうまく処理できるようです。

2,順序性のあるカテゴリ変数
カテゴリ変数をダミー変数化する際はLabel Encodingを使用するのですが、この場合、普通の数値型変数と同様に閾値との大小関係で判定されます。一方、カテゴリ変数として入力すると変数A (is or is not) category_xで判定されるようです。


カテゴリ変数における木の分岐

競馬データにおいてどの変数を順序性があると判断するかは微妙なラインのものもありますが、カテゴリ変数のうち順序性のあるものについてはLabel Encodingで数値型として処理し、順序性のないものについてはLabel Encodingしたあと、データ型をcategory型に変換してみたいと思います。

カテゴリ変数の扱いについては下記の記事を参考にさせていただきました。

LightGBMのCategorical Featureによって精度が向上するか?
lightgbm カテゴリカル変数と欠損値の扱いについて+α

②ハイパーパラメータチューニングの有無について
LightGBMではハイパーパラメータを多く持つため、チューニングにより精度がどれだけ向上するのか比較してみたいと思います。
チューニングの方法は主に3種類あります。

  1. グリッドサーチ

  2. ランダムサーチ

  3. ベイズ最適化

グリッドサーチやランダムサーチはパラメータの理解が深く、勘所がつかめている必要があり、計算量も膨大になる場合があります。
そこで今回はベイズ理論を用いて、効率的に探索できるベイズ最適化を使用します。
ベイズ最適化の理論や詳しい数式については理解できていないのですが(今後しっかり勉強し、理解したうえで使えるようになりたいと思います。。)、幸いOptunaというライブラリを使うことで簡単に実装することができます。中でもクロスバリデーションを行いながら、ハイパーパラメータの探索が可能なLightGBMTunerCVを使用します。

モデル構築

データ取得

データはJRA-VANの競馬データベースソフト「TARGET frontier」から一か月無料お試しを活用し、取得しました。
※ホームページはこちら

こちらのソフトでは様々な条件を指定し、csvでのデータ取得が可能です。
今回は阪神競馬場で開催された30年分のレースを年齢や脚質など必要そうな変数を選択しながら取得しました。


データベースソフト画面

今回分析に使った説明変数は以下になります(前処理では別の変数も含みます)。

Sex・・・性別
Age・・・年齢
Jokky・・・騎手
Kinryo・・・斤量
Number・・・馬番
Popular・・・人気
Weight・・・馬体重
Trainer・・・調教師
Belong・・・所属
Color・・・毛色
Odds・・・単勝オッズ
PCI・・・ペースチェンジ指数。PCI =Ave-3F÷上がりタイム*100-50
Zougen・・・馬体重の前レースからの増減
Foot_type・・・脚質。「差し」「逃げ」など。
Father・・・父の血統タイプ
MotherFather・・・母父の血統タイプ

目的変数は3着以内を0か1で表したものになります。

前処理

まずは必要なライブラリをインポートします。

import pandas as pd
import numpy as np
from sklearn.preprocessing import LabelEncoder
import seaborn as sns
import lightgbm as lgb
from sklearn.metrics import log_loss
from sklearn.model_selection import train_test_split
from sklearn.model_selection import StratifiedKFold
import optuna.integration.lightgbm as lgb

分析にするにあたり、主に以下の前処理を行いました。

  • 距離を1600mのレースに絞る

  • 馬場状態を「良」のレースに絞る

  • 着順が0になっている行を削除する(出馬表には入っているが実際に出走しなかった馬などが該当)

  • データを絞るのに使い不要になった列を削除する

  • カテゴリ変数のダミー変数化

# 1600メートルのレースに絞る
df = df.query('距離 == 1600')

# 馬場状態が「良」のレースに絞る
df = df.query('馬場状態 == "良"')

# 着順が0になっている行を削除
drop_index = df.index[df['確定着順'] == 0]
df = df.drop(drop_index ,axis = 0)

# 不要になった列を削除
drop_col = ["距離","馬場状態","確定着順"]
df = df.drop(drop_col ,axis = 1)

# ダミー変数化
category = ['Sex','Jokky','Trainer','Belong','Color','Foot_type','Father','MotherFather']
for c in category:
  le = LabelEncoder()
  le.fit(df[c])
  df[c] = le.transform(df[c])

前処理を終えたデータはこんな感じです。

前処理後

データ概観

機械学習させる前に基本統計量を見たり、変数内でどのような種類のデータが何個あるか見たり、分析対象となるデータを簡単に把握するため、いろいろと試します。その中で何個か行ったものを取り上げます。

・目的変数との相関係数を見てみます。

corr = df.corr()["3着以内"]
corr

[出力結果]
Sex             0.040879
Age            -0.046755
Jokky           0.005607
Kinryo          0.051783
Number         -0.056678
Popular        -0.416781
Weight          0.057235
Trainer         0.019034
Belong          0.006642
Color           0.011224
Odds           -0.273955
PCI             0.182607
Zougen          0.005526
Foot_type      -0.127078
Father          0.052913
MotherFather   -0.005760
3着以内            1.000000

今回、ダミー変数化したものはについては順序性のない名義尺度になるためあまり参考になりませんが、人気やPCIが3着以内に入るために重要になっていそうです。

・変数内の種類ごとの勝率を見てみます。今回、目的変数は0か1の値をとるのでそれぞれのデータごとに目的変数の平均値をとることで勝率が見ることができます。

df[["Age","3着以内"]].groupby("Age").mean()

「出力結果」
	3着以内
Age	
2	0.228883
3	0.206931
4	0.243437
5	0.229978
6	0.140985
7	0.109863
8	0.061728
9	0.000000
10	0.000000
11	0.000000

馬齢ごとの勝率を見ると1600mのレースでは4歳馬の勝率が一番高く、9歳以上では勝利の実績がなく、とても厳しい戦いになります。

df[["Number","3着以内"]].groupby("Number").mean()

「出力結果」
	3着以内
Number	
1	0.235756
2	0.232261
3	0.234833
4	0.233402
5	0.226112
6	0.230358
7	0.218287
8	0.238273
9	0.216113
10	0.218279
11	0.198379
12	0.179834
13	0.206444
14	0.182885
15	0.161398
16	0.136760
17	0.124549
18	0.175556

出走枠では内枠が有利になりそうです。

df[["Foot_type","3着以内","PCI"]].groupby(["3着以内","Foot_type"]).mean()


勝敗を分ける脚質ごとのPCI

ここでは勝ち馬がどのようなペースで走ったか見ることができます。最初から飛ばし首位を守る走り方をする「逃げ」タイプでもPCIは50を超えないと厳しく、最後までスピードを落とさないスタミナが必要そうです。
ちなみに脚質はそれぞれ以下の区分でダミー変数化されています。
0 先行
1 差し
2 追込
3 逃げ
4 マクリ

学習

それでは実際に機械学習をさせていきたいと思います。
まずは、学習データと検証データに分割します。

col = ["Sex","Age","Jokky","Kinryo","Number","Popular","Weight","Trainer","Belong","Color","Odds","PCI","Foot_type","Zougen","Father","MotherFather"]
x = df[col]
t = df['3着以内']
tr_x,va_x,tr_y,va_y = train_test_split(x,t,test_size = 0.2, random_state = 0)

LightGBM用にデータセットを作成し、パラメータを指定し、学習を行っていきます。2値分類問題のため評価関数はlogloss(対数損失/交差エントロピー)を指定。

lgb_train = lgb.Dataset(tr_x,tr_y)
lgb_eval = lgb.Dataset(va_x,va_y)
params = {'objective':'binary', 'seed':71, 'verbose':0, 'metrics':'binary_logloss'}
num_round = 10000
early_stopping_round = 100
model = lgb.train(params, lgb_train, num_boost_round=num_round,
                  early_stopping_rounds=early_stopping_round,
                 valid_names=['train', 'valid'], valid_sets=[lgb_train, lgb_eval]
                 )
「出力結果」
[133]	train's binary_logloss: 0.302039	valid's binary_logloss: 0.385102
[134]	train's binary_logloss: 0.301565	valid's binary_logloss: 0.385075
[135]	train's binary_logloss: 0.30103	valid's binary_logloss: 0.385008
[136]	train's binary_logloss: 0.300487	valid's binary_logloss: 0.385092
[137]	train's binary_logloss: 0.300121	valid's binary_logloss: 0.385067
[138]	train's binary_logloss: 0.299527	valid's binary_logloss: 0.385212
[139]	train's binary_logloss: 0.299307	valid's binary_logloss: 0.385258
[140]	train's binary_logloss: 0.29894	valid's binary_logloss: 0.385254
[141]	train's binary_logloss: 0.298431	valid's binary_logloss: 0.38527
[142]	train's binary_logloss: 0.297913	valid's binary_logloss: 0.385545
[143]	train's binary_logloss: 0.29747	valid's binary_logloss: 0.38565
[144]	train's binary_logloss: 0.297101	valid's binary_logloss: 0.385843
[145]	train's binary_logloss: 0.296606	valid's binary_logloss: 0.385871
[146]	train's binary_logloss: 0.296262	valid's binary_logloss: 0.385977
[147]	train's binary_logloss: 0.295764	valid's binary_logloss: 0.386049
Early stopping, best iteration is:
[47]	train's binary_logloss: 0.343913	valid's binary_logloss: 0.379727

各イテレーションでの評価は前半省略していますが上記のようになりました。アーリーストッピングを100回に設定しているので、47回目以降精度が向上せず、147回目で学習が終了しています。

結果

結果を見ていきます。
まずは特徴量の重要度です。

lgb.plot_importance(model)


特徴量重要度

どのような走りをするのかや事前の人気が結果に与える影響としては大きいようです。

精度は先ほども確認しましたが以下の数値になりました。

va_pred = model.predict(va_x)
score = log_loss(va_y, va_pred)
print(f'logloss: {score:.4f}')

「出力結果」
logloss: 0.3797

予測

それでは実際レースに出走する馬のデータを読み込ませ、それぞれの馬が3着以内に入る確率を出していきたいと思います。

これらのデータは手打ちで作成しましたがけっこう骨の折れる作業でした。。騎手や調教師の番号をダミー変数化したものと照らし合わせながら入力したり、PCIについては前走のレースや条件の合うレースから平均を出したり、予想して入力したので恣意性が否めないものになっています。。

test_x = pd.read_csv('mairuCS.csv',encoding='shift_jis')
test_x

[出力結果]
	Sex	Age	Jokky	Kinryo	Number	Popular	Weight	Trainer	Belong	Color	Odds	PCI	Zougen	Foot_type	Father	MotherFather
0	2	3	318	56	1	10	484	230	2	7	53.9	54.90	-2	2	7	5
1	2	5	100	57	2	9	508	480	3	1	26.0	55.25	-6	0	7	5
2	2	4	148	57	3	8	530	148	2	6	26.0	54.70	4	0	7	4
3	2	4	93	57	4	1	494	216	3	6	3.6	58.30	4	1	4	4
4	2	5	85	57	5	3	530	120	3	1	6.2	57.80	-4	1	7	4
5	2	4	173	55	6	2	480	448	2	2	4.4	53.85	2	0	4	5
6	2	4	385	57	7	7	498	147	2	6	18.9	57.73	0	2	7	5
7	2	5	254	55	8	15	482	422	2	6	108.0	56.00	-4	1	7	4
8	2	3	463	56	9	12	470	144	2	7	57.1	51.00	4	3	7	4
9	2	3	94	56	10	6	486	13	2	1	9.2	59.00	-4	2	7	3
10	2	4	289	57	11	5	504	302	2	7	7.7	55.80	4	2	5	7
11	2	4	186	57	12	14	520	361	2	1	84.1	51.05	0	3	5	7
12	2	5	332	57	13	11	500	384	2	6	54.1	55.20	4	2	5	4
13	2	10	414	57	14	17	526	150	2	1	441.0	46.00	4	3	7	7
14	2	3	262	56	15	4	460	148	2	6	7.3	53.50	-2	1	5	4
15	2	6	259	57	16	16	450	241	2	6	435.6	53.95	4	2	5	7
16	2	5	337	57	17	13	474	474	2	6	76.1	51.83	8	0	7	5
pred = model.predict(test_x)
for c in pred:
    print('{:.2%}'.format(c))

[出力結果]
23.90%
24.06%
26.47%
65.65%
57.38%
38.11%
28.83%
15.47%
24.70%
51.72%
56.70%
6.56%
30.31%
1.43%
48.70%
5.52%
14.92%

馬番が1番のものから順に3着以内に入る確率を出すことができました。確率の高い上位5頭は以下になります。
4番 1番人気 シュネルマイスター 65.65%
5番 3番人気 サリオス 57.38%
11番 5番人気 ソウルラッシュ 56.7%
10番 6番人気 セリフォス 51.72%
15番 4番人気 ダノンスコーピオン 48.7%

ハイパーパラメータチューニング(ベイズ最適化(Optuna))

次はハイパーパラメータをベイズ最適化によりチューニングし、同様に3着以内に入る確率を出していきたいと思います。

OptunaのLightGBMTunerCVを使っていきます。
これは前述のとおり、クロスバリデーションを行いながら、より良いハイパーパラメータを探索してくれるのが特徴です。

今回は一応3着以内に入るデータのほうが少ないので不均衡データとしてStratifiedKFoldで分割を行います(評価関数でloglossを採用したときもそうですが、データの分割でも何を採用するかなどはもう少しそれぞれの方式の理解を深め、精査する必要はありそうです)。

trainval = lgb.Dataset(tr_x,tr_y)
params = {'objective': 'binary',
          'metric': 'binary_logloss',
          'random_seed':0}
tuner = lgb.LightGBMTunerCV(params, trainval, verbose_eval=10000, early_stopping_rounds=100, folds=StratifiedKFold(n_splits=5))
tuner.run()
best_params = tuner.best_params
print("  Params: ")
for key, value in best_params.items():
    print("    {}: {}".format(key, value))

[出力結果]
  Params: 
    objective: binary
    metric: binary_logloss
    random_seed: 0
    feature_pre_filter: False
    lambda_l1: 0.18543043594906852
    lambda_l2: 0.00023271515174781978
    num_leaves: 2
    feature_fraction: 0.5
    bagging_fraction: 0.9048791552682229
    bagging_freq: 3
    min_child_samples: 20

最適なハイパーパラメータは上記のようになりました。こちらのハイパーパラメータを使用し、再度学習させていきます。

lgb_train = lgb.Dataset(tr_x, tr_y,free_raw_data=False)
lgb_eval = lgb.Dataset(va_x, va_y,free_raw_data=False)
model = lgb.train(best_params, lgb_train, num_boost_round=num_round,
                  early_stopping_rounds=early_stopping_round,
                 valid_names=['train', 'valid'], valid_sets=[lgb_train, lgb_eval]
                 )

[出力結果]
[610]	train's binary_logloss: 0.364248	valid's binary_logloss: 0.377627
[611]	train's binary_logloss: 0.364244	valid's binary_logloss: 0.377665
[612]	train's binary_logloss: 0.364244	valid's binary_logloss: 0.377694
[613]	train's binary_logloss: 0.364237	valid's binary_logloss: 0.377681
[614]	train's binary_logloss: 0.364232	valid's binary_logloss: 0.377704
[615]	train's binary_logloss: 0.364229	valid's binary_logloss: 0.3777
[616]	train's binary_logloss: 0.364226	valid's binary_logloss: 0.377727
[617]	train's binary_logloss: 0.364226	valid's binary_logloss: 0.377744
[618]	train's binary_logloss: 0.364225	valid's binary_logloss: 0.377758
Early stopping, best iteration is:
[518]	train's binary_logloss: 0.364879	valid's binary_logloss: 0.377283

今回は618回目で学習が終了し、518回目のスコア0.3773が最も良くなりました。
先ほどは0.3797という数値だったのでわずかですが精度が向上しました。

特徴量重要度や3着以内に入る確率は下記のようになりました。


特徴量重要度2
pred = model.predict(test_x)
for c in pred:
    print('{:.2%}'.format(c))

[出力結果]
22.33%
29.97%
29.80%
72.84%
60.69%
35.85%
32.72%
13.06%
16.91%
51.92%
45.84%
8.48%
21.82%
0.44%
55.51%
1.61%
8.62%

特徴量の重要度は脚質が1位になりました。
勝率の高い馬上位5頭は以下になります。

4番 1番人気 シュネルマイスター 72.84%
5番 3番人気 サリオス 60.69%
15番 4番人気 ダノンスコーピオン 55.51%
10番 6番人気 セリフォス 51.92%
11番 5番人気 ソウルラッシュ 45.84%

その他パターン

その他のパターンとしてハイパーパラメータのチューニングをした状態で

①high-cardinalityな(=要素が多い)カテゴリ変数を明示的に指定
②順序性のない変数をカテゴリー型にデータ型を変換

①high-cardinalityな(=要素が多い)カテゴリ変数を明示的に指定

trainval = lgb.Dataset(tr_x,tr_y)
cat_list = ["Jokky","Trainer"]
params = {'objective': 'binary',
          'metric': 'binary_logloss',
          'random_seed':0}
tuner = lgb.LightGBMTunerCV(params, trainval, verbose_eval=10000, early_stopping_rounds=100, categorical_feature = cat_list, folds=StratifiedKFold(n_splits=5))
tuner.run()
best_params = tuner.best_params

lgb_train = lgb.Dataset(tr_x, tr_y,free_raw_data=False)
lgb_eval = lgb.Dataset(va_x, va_y,free_raw_data=False)
cat_list = ["Jokky","Trainer"]
model = lgb.train(best_params, lgb_train, num_boost_round=num_round,
                  early_stopping_rounds=early_stopping_round,
                 valid_names=['train', 'valid'], valid_sets=[lgb_train, lgb_eval],
                 categorical_feature = cat_list,
                 )

精度はlogloss: 0.3821となり少し悪化しました。勝率上位5頭は順番の前後はありましたが顔ぶれは変わりませんでした。

②順序性のない変数をカテゴリー型にデータ型を変換

cat_list = ["Sex","Jokky","Growther","Belong","Color","Foot_type","Father","MotherFather"]

for i in cat_list:
    df[i] = df[i].astype("category")

df.dtypes

[出力結果]
Sex             category
Age                int64
Jokky           category
Kinryo           float64
Number             int32
Popular          float64
Weight           float64
Growther        category
Belong          category
Color           category
Odds             float64
PCI              float64
Zougen           float64
Foot_type       category
Father          category
MotherFather    category
3着以内               int64
dtype: object

こちらはカテゴリ変数を明示的に指定せず、ハイパーパラメータチューニングをして学習させました。しかし精度はlogloss: 0.3815となり、最も良いスコアをただき出すことはできませんでした。

レース結果

そして気になるレース結果ですが、以下のようになりました。

レース結果

3着以内に入る馬は予測上位5頭の中でセリフォスだけ的中することができました。ソウルラッシュ、シュネルマイスターはあと一歩というところでした。
予測精度はなんとも言えないですが、ポジティブに考えると予測上位5頭に単勝100円ずつかけていれば920円の払い戻しなので儲けが出るという形になります笑

感想や考察

今回の分析を通じて感じたことを記していきます。

・ドメイン知識の重要性
どのような変数を使ってモデルを構築するかはその分野の知識があることが重要だと感じました。今回の競馬では数えきれないほどの変数があり、しかもそれらが相互に影響を及ぼしあっています。また生物である以上、前走からの成長やその日のコンディションなど時系列的に考えたりして、当日のパフォーマンスを予測できるようにしないといけません。そのためドメイン知識がないとよい仮説が立てることができず、モデル構築も難しいと感じました。

・ハイパーパラメータのチューニングについて
データ数が豊富にある場合はそこまで精度を格段に上げるものではありませんでした。むしろそれより特徴量の選択やその扱いのほうが精度向上には重要と感じました。データ分析コンペにおいて上位に入るため、わずかでも精度を上げたい場合に力を発揮するのかなと思いましたが、実際のビジネスでは分析から打ち手をどうするかが重要であり、小さな精度を追求することにでどれほど意味があるのかは疑問が残りました。

・カテゴリ変数の扱いについて
こちらもあまり気にしないでいい部分だと感じました。むしろLightGBM自身の自動適用によるものが最も精度の高いものとなりました。

・PCIについて
今回、予測させるデータにおけるPCIについては恣意性がありました。一方で特徴量としての重要度は高く、当日のPCIをしっかり予測させることが大事だと感じました。実際、レース後に当日のPCIを使って、再度予測させたところ予測における勝率上位5頭の顔ぶれは変わり、的中率も高まりました。

今後やってみたいこと

今回の分析を通じて分かったことや感じたことも踏まえ、今後やってみたいことを記します。

・回収率を最大化する馬券の買い方
やはり分析だけでは付加価値はうまれません。そのあとどういう行動をするべきか意思決定に繋がるものが求められます。競馬でいうと実際に馬券を買い、儲けを出すことが目的になり、どのように馬券を買えばよいか分かることが重要です。今後はそれぞれの順位に入る確率やオッズから払戻額を期待値的に算出し、数理最適化を用いながら回収率が最大化する馬券の買い方を導いてみたいと思います。

続編に向けて・・・

この後、年末に行われた有馬記念を含めいくつかG1レースの分析を追加で行いました。
主に以下のような改良を行いました。

・新しい特徴量の作成
PCIは出走するほかの馬の影響を受けるようです。「逃げ」や「先行」タイプの馬が多ければ、全体としてペースが上がります。すると「差し」を武器とする馬も普段よりPCIが下がる可能性もあります。そこでレースにおけるPCIの平均値を出し、それに対して各馬がどれぐらいのPCIだったかを新たな特徴量として作成しました。

・特徴量の削減
特徴量として重要度の低いものがありましたのでそれらを省いて学習させてみました。そうすることで精度を向上させることができました。

最終的な目標は回収率を最大化させる馬券の買い方を出すことで、こちらはこれからになりますが、上記の改良はすでにやってみたので続編を書く機会があれば、またアウトプットしてみたいと思います。

最後になりましたが、ここまで読んでいただきありがとうございました!
省略できるところは省略したつもりでしたが、思いのほかボリューミーになってしまいました。。
これからもアウトプットは続けたいと思いますので、勉強のためにも今回の記事含めフィードバックなどいただけましたら嬉しく思います!



いいなと思ったら応援しよう!