【LightGBM】走破タイムを予測する競馬AI(回帰モデル)を作成|Python

競馬AI

本記事の概要

本サイトでは、競馬AIの作成方法について解説しています。

そして、過去に作成した競馬AIでは直接「1着になる競走馬か否か」を予測しており、いずれも回収率は80%台に留まっています。

過去モデルの回収率

※横軸が「1着になる確率」、縦軸が「y_pred_probがある値を超えた競争馬に単勝を賭けた時の回収率」を表しています。

そこで今回は少し見方を変えて、

競走馬の走破タイム

を予測し、その結果をもとに着順および回収率を計算していきます。

特徴量の重要度や回収率について、どのような違いが見られるのかをチェックしていきます!

前提条件

続いて、競馬AI作成における前提条件を説明します。

前提条件
  • 特徴量
  • 使用する機械学習アルゴリズム

①特徴量

大きく分けて、「レース情報」「競争馬情報(絶対情報)」「競走馬情報(相対情報)」の3つを特徴量としています。

レース情報
  • レースID
  • 開催日
  • 回数
  • 日数
  • レース数
  • レース長
  • 天候
  • 馬場状態
  • レースクラス
  • 競走馬数
競争馬情報(絶対情報)
  • 競走馬ID
  • 性別
  • 年齢
  • 馬体重
  • 馬体重の増減
  • 馬番
  • 人気
  • 単勝オッズ
  • スピード指数
  • 騎手ID
  • 母馬ID
  • 父馬ID
  • 母父馬ID
  • 同じ母馬を持つ競争馬の勝率・連帯率・複勝率
  • 同じ父馬を持つ競走馬の勝率・連帯率・複勝率
  • 同じ母父馬を持つ競走馬の勝率・連帯率・複勝率
  • 過去5Rの通算賞金
  • 過去5Rの勝率・連帯率・複勝率
  • 過去5Rの第一コーナーから着順までの順位変化
  • 過去5Rの第一コーナーから最終コーナーまでの順位変化
  • 過去5Rの最終コーナーから着順までの順位変化
競走馬情報
(レース内の相対情報)
  • 過去5Rの通算賞金の偏差値
  • 過去5Rの勝率・連帯率・複勝率の偏差値
  • スピード指数の偏差値
  • 同じ母馬を持つ競走馬の勝率・連帯率・複勝率の偏差値
  • 同じ父馬を持つ競走馬の勝率・連帯率・複勝率の偏差値
  • 同じ母父馬を持つ競走馬の勝率・連帯率・複勝率の偏差値

特徴量の入手・加工方法

上記特徴量は、「netkeiba.com」から入手したデータをそのまま、もしくは加工しています。

入手・加工のソースコードについては、本記事で解説していないので過去記事を参照してください。

過去記事
カテゴリ 対象データ URL
データ入手
(スクレイピング)
レース結果のURL https://ryohei22.com/scraping-get_url-central/
レース結果 https://ryohei22.com/scraping-race_result/
競走馬の競争成績 https://ryohei22.com/scraping-horse_result/
データ加工 大部分の特徴量 https://ryohei22.com/scraping-edit_data/
血統情報 https://ryohei22.com/win-prediction4/
スピード指数 https://ryohei22.com/win-prediction3/

②使用する機械学習アルゴリズム

今回使用する機械学習アルゴリズムは、

LightGBM

です。

LightGBMとは、Microsoftが開発した分散型勾配ブースティングフレームワークのことで、詳細な解説は「最近流行りのLightGBMとは」を参照してください。

LightGBMには下記の特徴があり、実装の容易性と高い回収率が期待できると考え採用しました。

LightGBMの特徴
  • 予測精度が高く、計算時間が短い
  • 決定木をベースとするモデルのため、正規化等の前処理が必要ない
  • 特徴量の重要度を容易に確認できる

ソースコード解説

以降では、競馬AIの作成方法についてソースコードを交えて解説していきます。

①データ分割(訓練データ/検証データ/テストデータ・説明変数/目的変数)

この箇所のソースコードは下記のとおりです。

# パラメータ
date_split_train = 20200401 # テストデータの開始点(年+月+日)
date_split_valid = 20210801 # 訓練データと検証データの分岐点(年+月+日)
date_split_test = 20211101  # 検証データとテストデータの分岐点(年+月+日)

# 訓練データ・検証データ・テストデータに分割
train = df[df['date'] < date_split_valid]
valid = df[(df['date'] >= date_split_valid) & (df['date'] < date_split_test)]
test = df[df['date'] >= date_split_test]
odds_df_test = odds_df[odds_df['date'] >= date_split_test]

# 説明変数
drop_list = ['time']
x_train = train.drop(drop_list, axis=1)
x_valid = valid.drop(drop_list, axis=1)
x_test = test.drop(drop_list, axis=1)

# 目的変数
y_train = train['time']
y_valid = valid['time']
y_test = test['time']
y_test_org = pd.DataFrame(test['rank'])

以降で、各要素について解説していきます。

訓練データ・検証データ・テストデータに分割

まずは、データを訓練データ・検証データ・テストデータの3つに分けます。

訓練データ
 →用途:モデルを作成するために使用するデータ
 →データ期間:2022/4/1 ~ 2021/8/1

検証データ
 →用途:ハイパーパラメータを調整するために使用するデータ
 →データ期間:2021/8/1 ~ 2021/11/1

テストデータ
 →用途:モデルの汎用性を評価するために使用するデータ
 →データ期間:2021/11/1 ~ 2022/4/1

# パラメータ
date_split_train = 20200401 # テストデータの開始点(年+月+日)
date_split_valid = 20210801 # 訓練データと検証データの分岐点(年+月+日)
date_split_test = 20211101  # 検証データとテストデータの分岐点(年+月+日)

# 訓練データ・検証データ・テストデータに分割
train = df[df['date'] < date_split_valid]
valid = df[(df['date'] >= date_split_valid) & (df['date'] < date_split_test)]
test = df[df['date'] >= date_split_test]
odds_df_test = odds_df[odds_df['date'] >= date_split_test]

変数「df」は、「①特徴量」のデータを格納したDataFrame型変数です。

説明変数と目的変数に分ける

説明変数は「①特徴量」のデータで、目的変数は「走破タイム」です。

# 説明変数
drop_list = ['time']
x_train = train.drop(drop_list, axis=1)
x_valid = valid.drop(drop_list, axis=1)
x_test = test.drop(drop_list, axis=1)

# 目的変数
y_train = train['time']
y_valid = valid['time']
y_test = test['time']
y_test_org = pd.DataFrame(test['rank'])

②モデル作成

この箇所のソースコードは下記のとおりです。

""" 2. モデル作成 """
# ライブラリ
import optuna.integration.lightgbm as lgb

# LightGBM用のデータセットに変換
lgb_train = lgb.Dataset(x_train, y_train)
lgb_valid = lgb.Dataset(x_valid, y_valid, reference=lgb_train)

# ハイパーパラメータ
params = {'task': 'train',
          'boosting_type': 'gbdt',
          'objective': 'regression',
          'metric': 'rmse',
          'learning_rate': 0.01,
          'random_state': 777}

# モデル作成
model = lgb.train(params,
                  lgb_train,
                  valid_sets=[lgb_train, lgb_valid],
                  early_stopping_rounds=50,
                  verbose_eval=100)

以降で、各要素について解説していきます。

学習用のデータセットを作成

今回作成するLightGBMのモデルは、Python APIを使用します。

そのため、「lgb.Dataset()」で専用のデータセットを事前に作成します。

# ライブラリ
import optuna.integration.lightgbm as lgb

# LightGBM用のデータセットに変換
lgb_train = lgb.Dataset(x_train, y_train)
lgb_valid = lgb.Dataset(x_valid, y_valid)

モデル作成

続いて、ハイパーパラメータを指定してモデルを作成します。

# ハイパーパラメータ
params = {'boosting_type': 'gbdt',
          'objective': 'regression',  # 回帰モデルを作成する場合は「regression」を指定する必要があります。
          'metric': 'rmse',
          'learning_rate': 0.01,
          'random_state': 777}

# モデル作成
model = lgb.train(params,
                  lgb_train,
                  valid_sets=[lgb_train, lgb_valid],
                  early_stopping_rounds=50,
                  verbose_eval=100)

今回は「Optuna」を利用して、LightGBMの学習モデルを作成しています。

Optunaを利用する理由

自動でパラメータチューニングしてくれるため

なお、自動チューニングしてくれるパラメータは下記の7つです。

  • lambda_la
  • lambda_l2
  • num_leaves
  • feature_fraction
  • bagging_fraction
  • bagging_freq
  • min_child_samples

また、上記以外のパラメータはこちらで設定する必要があるので、「params」という変数で指定しています。

以降で、今回設定したパラメータを解説していきます。

boosting_type

ブースティングのアルゴリズムを選択できます。

基本は勾配ブースティングを行うことが多いので、デフォルトの ‘gbdt’ でよいと思います。

objective

目的関数を選択できます。

今回は回帰モデルを作成するため、’regression’ を指定しています。

metric

評価指標を選択できます。

回帰モデルの場合は ‘rmse’、’auc’ が代表的ですが、今回は’rmse’を採用しています。

rmseとは?
参照元:AI Academy

RMSEとは二乗平均平方根誤差のことで、上記式で表されます。
この値が小さいほど、誤差の小さいモデルであると言えます。

learning_rate

学習率を指定できます。

値を大きくするほど処理時間は短くなりますが、精度は落ちます。

一般的に、0.01を設定することが多いです。

③モデル評価

この箇所のソースコートは下記のとおりです。

""" 3. モデルの評価 """
# テストデータの走破タイムを予測
y_pred_time = model.predict(x_test)

# 予測した走破タイムから着順を算出
df_pred = pd.DataFrame(data={'race_ID': x_test['race_ID'],
                             'horse_ID': x_test['horse_ID'],
                             'win_odds': odds_df_test['win_odds'],
                             'actual_time': y_test,
                             'pred_time': y_pred_time,
                             'actual_rank': y_test_org['rank']
                             })
df_pred['pred_rank'] = df_pred[['race_ID', 'pred_time']].groupby('race_ID').rank(ascending=True)

# 的中率を計算
df_pred['hit_mark'] = df_pred.apply(lambda x: 1 if (x['actual_rank'] == 1 and\
                                                    x['actual_rank'] == x['pred_rank'])\
                                                else 0, axis=1)
acc_rate = df_pred['hit_mark'].sum() / len(df_pred['race_ID'].unique()) * 100
    
# 回収率を計算
return_rate =  df_pred.loc[df_pred['hit_mark'] == 1, 'win_odds'].sum() / len(df_pred['race_ID'].unique()) * 100

以降で、各要素について解説していきます。

走破タイムを予測

まずは、作成したモデルを用いてテストデータの走破タイムを予測します。

「モデル.predect()」を記述するだけで、簡単に予測できますよ。

# テストデータの走破タイムを予測
y_pred_time = model.predict(x_test)

予測した走破タイムから着順を算出

予測結果と実際の結果(走破タイム・着順)を比較しやすいように、DataFrameを作成します。

下記のように記述することで、列名を指定しながらDataFrameを作成できます。

pd.DataFrame( data = {‘列名’: 値, ・・・} )

その後、「groupby()」と「rank()」を使用してレース内の順位を算出しています。

# 予測した走破タイムから着順を算出
df_pred = pd.DataFrame(data={'race_ID': x_test['race_ID'],
                             'horse_ID': x_test['horse_ID'],
                             'win_odds': odds_df_test['win_odds'],
                             'actual_time': y_test,
                             'pred_time': y_pred_time,
                             'actual_rank': y_test_org['rank']
                             })
df_pred['pred_rank'] = df_pred[['race_ID', 'pred_time']].groupby('race_ID').rank(ascending=True)

回収率を計算

まずは、下記の2条件を満足する行にマーク(1:的中、0:外れ)をつけます。

  • actual_rankの値が1
     →実際の着順が1位
  • actual_rankとpred_rankの値が等しい
     →予測が的中

その際に「apply()」を使用して、DataFrameの行ごとに判定処理を行っています。

その後、下記の計算式で回収率を計算しています。

的中率 = 的中数 / レース数 × 100

# 回収率を計算
df_pred['hit_mark'] = df_pred.apply(lambda x: 1 if (x['actual_rank'] == 1 and\
                                                    x['actual_rank'] == x['pred_rank'])\
                                                else 0, axis=1)
return_rate =  df_pred.loc[df_pred['hit_mark'] == 1, 'win_odds'].sum() / len(df_pred['race_ID'].unique()) * 100

# 回収率を出力
print(acc_rate)

>>> 80.50925925925927

回収率の計算結果は約80.5%でした。

過去に作成した2値分類モデルの回収率は、最高で88%だったので若干性能は劣るようです。

過去モデルの回収率

しかしながら、今回の賭け方は「1位と予測された全ての競走馬に賭ける」という単純なロジックを採用したので、賭け方を改善することで回収率は改善できそうです。

④特徴量の重要度

今回作成したモデルについて、各特徴量がどの程度影響及ぼしているかを確認します。

""" 4. 特徴量の重要度 """
lgb.plot_importance(model, max_num_features=20)
特徴量の重要度(今回モデル)

特に重要度の高い特徴量は、下記の4つでした。

  • race_len
     → レースの距離
  • jockey_ID
     → 騎手ID
  • race_ID
     → レースID
  • ped_0
     → 父馬ID

全体ソースコード

最後に、全体ソースコードを掲載します。

""" 走破タイムを予測 """

# ライブラリ
import pandas as pd
import datetime

# パラメータ
date_split_train = 20200401 # テストデータの開始点(年+月+日)
date_split_valid = 20210801 # 訓練データと検証データの分岐点(年+月+日)
date_split_test = 20211101  # 検証データとテストデータの分岐点(年+月+日)

file_directory = 'C:/Users/kouhe/OneDrive/01_Documents/02_機械学習/データ'
file_name = 'race_result.csv'

# データ読み込み
race_result_total = pd.read_csv(file_directory + '/race_result.csv')
horse_result = pd.read_csv(file_directory + '/horse_result.csv', index_col=0)
horse_ped = pd.read_csv(file_directory + '/horse_peds.csv', index_col=0)
return_table = pd.read_csv(file_directory + '/return_table.csv', index_col=0)

race_result = race_result_total[race_result_total['date'] >= date_split_train]

# データ加工
df_edit = edit_data(race_result, horse_result, horse_ped)

# オッズを取得
odds_df = pd.DataFrame(data={'date': df_edit['date'],
                             'win_odds': df_edit['win_odds']})
odds_test = odds_df.loc[odds_df['date'] >= date_split_test, 'win_odds']

# 不要な行を削除
drop_list = ['rank']
df = df_edit.drop(drop_list, axis=1)

# 訓練データ・テストデータに分割
train = df[df['date'] < date_split_valid]
valid = df[(df['date'] >= date_split_valid) & (df['date'] < date_split_test)]
test = df[df['date'] >= date_split_test]
odds_df_test = odds_df[odds_df['date'] >= date_split_test]

# 説明変数
drop_list = ['time']
x_train = train.drop(drop_list, axis=1)
x_valid = valid.drop(drop_list, axis=1)
x_test = test.drop(drop_list, axis=1)

# 目的変数
y_train = train['time']
y_valid = valid['time']
y_test = test['time']
y_test_org = pd.DataFrame(test['rank'])


""" 2. モデル作成 """
# ライブラリ
import optuna.integration.lightgbm as lgb

# LightGBM用のデータセットに変換
lgb_train = lgb.Dataset(x_train, y_train)
lgb_valid = lgb.Dataset(x_valid, y_valid, reference=lgb_train)

# ハイパーパラメータ
params = {'task': 'train',
          'boosting_type': 'gbdt',
          'objective': 'regression',
          'metric': 'rmse',
          'learning_rate': 0.01,
          'random_state': 777}

# モデル作成
model = lgb.train(params,
                  lgb_train,
                  valid_sets=[lgb_train, lgb_valid],
                  early_stopping_rounds=50,
                  verbose_eval=100)


""" 3. モデルの評価 """
# テストデータの走破タイムを予測
y_pred_time = model.predict(x_test)

# 予測した走破タイムから着順を算出
df_pred = pd.DataFrame(data={'race_ID': x_test['race_ID'],
                             'horse_ID': x_test['horse_ID'],
                             'win_odds': odds_df_test['win_odds'],
                             'actual_time': y_test,
                             'pred_time': y_pred_time,
                             'actual_rank': y_test_org['rank']
                             })
df_pred['pred_rank'] = df_pred[['race_ID', 'pred_time']].groupby('race_ID').rank(ascending=True)

# 回収率
df_pred['hit_mark'] = df_pred.apply(lambda x: 1 if (x['actual_rank'] == 1 and\
                                                    x['actual_rank'] == x['pred_rank'])\
                                                else 0, axis=1)
return_rate =  df_pred.loc[df_pred['hit_mark'] == 1, 'win_odds'].sum() / len(df_pred['race_ID'].unique()) * 100


""" 4.特徴量の重要度 """
lgb.plot_importance(model, max_num_features=20)

考察(特徴量の重要度を受けて)

今回のモデルは走破タイムを目的変数に設定したので、「race_len」「race_ID」の重要度が大きくなっているのは納得の結果でした。

一方で、「jockey_ID」「ped_0」の重要度が大きくなっているのは意外でした。

特徴量の重要度(今回モデル)

というのも、前回モデルにおける特徴量の重要度を見ると、「jockey_ID」や「ped_0」はあまり大きな重要度ではなかったからです。

特徴量の重要度(前回モデル)

前回モデルでは「着順が1位か否か」を予測しました。

今回モデルで影響が大きかった「jockey_ID」と「ped_0」はどちらもカテゴリ変数で、その他にもいくつかカテゴリ変数を使用しているので、

CatBoost

を採用した方が回収率が上がるかもしれませんね。

CatBoostとは?

CatBoostとは「Category Boosting」の略称で、LightGBMと同様に勾配ブースティングの一種です。

その名の通り、カテゴリ変数の多いデータに強い特徴があります。

その代わりに処理速度は遅くなりそうですが、、、

下記の記事によると、LightGBMと同様にCatBoostでも「Oputuna」でパラメータチューニングができそうなので、これは嬉しいですね。

コメント

タイトルとURLをコピーしました