【LightGBM】多クラス分類モデルで単勝予測|Optunaでパラメータチューニング

競馬AI

本記事の概要

以降では、LightGBMの多クラス分類モデルの作成方法を解説します。

本記事の特筆ポイントは

Optunaでハイパーパラメータを自動チューニングしている

という点です。

ハイパーパラメータとは?
ハイパーパラメータとは、機械学習アルゴリズムの挙動を設定するパラメータを指します。
基本的には、モデル構築が手動で設定する必要があります。

Optunaとは?
Preferred Networks社が開発しているハイパーパラメータの最適化を自動化するためのフレームワークです。

前提条件

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

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

①目的変数

今回モデルでは、

着順

を目的変数としています。

ただし、着順をそのまま使用しているのではなく

  • 1~3着 → 0
  • 4~10着 → 1
  • 11着以降 → 2

というような変換を行った結果を、目的変数としています。

②特徴量

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

レース情報
  • レースID
  • 開催日
  • 回数
  • 日数
  • レース数
  • レース長
  • 天候
  • 馬場状態
  • レースクラス
競争馬情報(絶対情報)
  • 競走馬ID
  • 性別
  • 年齢
  • 馬体重
  • 馬体重の増減
  • 馬番
  • 単勝オッズ
  • 騎手ID

特徴量の入手・加工方法

上記特徴量は、「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/

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

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

LightGBM

です。

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

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

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

ソースコード解説

以降では、上記の手順で競馬AIを作成します。

①前準備

まずは、データを学習可能な形に加工します。

""" 1. 前準備 """
# ライブラリ
import pandas as pd

# パラメータ
date_split_valid = 20210401 # 訓練データと検証データの分岐点(年+月+日)
date_split_test = 20210901 # 検証データとテストデータの分岐点(年+月+日)
file_directory = 'C:/Users/kouhe/OneDrive/01_Documents/02_機械学習/データ'
file_name = 'race_result.csv'

# データ読み込み ---------- (※1)
race_result = pd.read_csv(file_directory + '/' + file_name)

# データ加工 ---------- (※2)
df = edit_data(race_result)

# 訓練データ・テストデータに分割 ---------- (※3)
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]

# 競走馬ごとに1000m当たりの平均タイムを計算(訓練データを使用) ---------- (※4)
time_ave_list = []
horse_id_list = train['horse_ID']
for horse_id in horse_id_list:
    time_sum = train[train.horse_ID == horse_id]['time'].sum()
    race_sum = train[train.horse_ID == horse_id]['race_len'].sum()
    time_ave_list.append(time_sum * 1000 / race_sum)
train['time_ave'] = time_ave_list
train['have_hist'] = [1] * len(train)
train = train.drop('time', axis=1)

time_df = pd.DataFrame(data={'horse_ID': train['horse_ID'],
                             'time_ave': train['time_ave']})
time_df_dump = time_df.drop_duplicates(subset=['horse_ID'])

# 検証データ
time_ave_list = []
have_hist_list = []
for horse_id in valid['horse_ID']:
    if horse_id in time_df['horse_ID'].values:
        target_time = float(time_df_dump.loc[time_df['horse_ID']==horse_id, 'time_ave'])
        time_ave_list.append(target_time)
        have_hist_list.append(1)
    else:
        time_ave_list.append(0)
        have_hist_list.append(0)
valid['time_ave'] = time_ave_list
valid['have_hist'] = have_hist_list
valid = valid.drop('time', axis=1)
    
# テストデータ
time_ave_list = []
have_hist_list = []
for horse_id in test['horse_ID']:
    if horse_id in time_df['horse_ID'].values:
        target_time = float(time_df_dump.loc[time_df['horse_ID']==horse_id, 'time_ave'])
        time_ave_list.append(target_time)
        have_hist_list.append(1)
    else:
        time_ave_list.append(0)
        have_hist_list.append(0)
test['time_ave'] = time_ave_list
test['have_hist'] = have_hist_list
test = test.drop('time', axis=1)
test_rank = test['rank']
    
# 説明変数
x_train = train.drop('rank', axis=1)
x_valid = valid.drop('rank', axis=1)
x_test = test.drop('rank', axis=1)

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

# 目的変数を「0 : 上位(1~3位)」「1: 中位(4~10位)」「2 : 下位(11位-)」に変換
# 上位
y_train.loc[y_train['rank'] <= 3] = 0
y_valid.loc[y_valid['rank'] <= 3] = 0
y_test.loc[y_test['rank'] <= 3] = 0

# 中位
y_train.loc[(y_train['rank'] >= 4) & (y_train['rank'] <= 10)] = 1
y_valid.loc[(y_valid['rank'] >= 4) & (y_valid['rank'] <= 10)] = 1
y_test.loc[(y_test['rank'] >= 4) & (y_test['rank'] <= 10)] = 1

# 下位
y_train.loc[y_train['rank'] >= 11] = 2
y_valid.loc[y_valid['rank'] >= 11] = 2
y_test.loc[y_test['rank'] >= 11] = 2

以降で、各要素の詳細を解説します。

データ読み込み(※1)

ローカルに保存しているCSVファイルを読み込みます。

使用するメソッドは、pandasの

メソッド

read_csv

で、フルパスを指定することで、CSVファイルを読み込んでいます。

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

# データ読み込み ---------- (※1)
race_result = pd.read_csv(file_directory + '/' + file_name)

また、読み込んだデータの全項目は下記のとおりです。

レース結果
  • 着順
  • タイム
  • コーナー通過順
レース情報
  • 馬番
  • 年月日
  • 競馬場ID
  • 回数
  • 日数
  • レース数
  • レース長
  • 天候
  • 馬場
  • レースクラス
競走馬情報
  • 馬名
  • 性齢
  • 斤量
  • 騎手
  • 厩舎
  • 馬体重(増減)
  • 競走馬ID
  • 騎手ID
  • 人気
  • 単勝オッズ

上記データの取得方法

このデータは「netkeiba.com」から取得しており、取得方法は「競馬AI作成:③-3」で解説しています。

データ加工(共通)(※2)

# データ加工 ---------- (※2)
df = edit_data(race_result)

読み込んだデータを、機械学習に使える形に加工します。

ここでの大まかな処理は、下記のとおりです。

  1. 不適切な値が含まれる行を削除
  2. 不要な列を削除
  3. 複数データが含まれる値を分割
  4. タイムを秒数に変換
  5. 文字列型の値を数値型に変換

メソッドについて

データ加工については、自作メソッドを使用しています。

この自作メソッドについては、「【スクレイピング】学習データ(netkeibaから取得したレースデータ)を加工」で詳細を解説しています。

訓練データ・検証データ・テストデータに分割(※3)

# パラメータ
date_split_valid = 20210401 # 訓練データと検証データの分岐点(年+月+日)
date_split_test = 20210901 # 検証データとテストデータの分岐点(年+月+日)

# 訓練データ・テストデータに分割 ---------- (※3)
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]

読み込んだデータを3つに分割します。

それぞれの用途は、

訓練データ
 →モデルを作成するために使用するデータ

検証データ
 →ハイパーパラメータを調整するために使用するデータ

テストデータ
 →モデルの汎用性を評価するために使用するデータ

となります。

本処理では、下記期間のデータをそれぞれ訓練データ・検証データ・テストデータとしています。

  • 訓練データ :2015/4/1 ~ 2021/4/1
  • 検証データ :2021/4/1 ~ 2021/9/1
  • テストデータ:2021/9/1 ~ 2022/4/1

データ加工(単勝予測用)(※4)

# データ加工(単勝予測用) ---------- (※4)
# 競走馬ごとに1000m当たりの平均タイムを計算(訓練データを使用)
time_ave_list = []
horse_id_list = train['horse_ID']
for horse_id in horse_id_list:
    time_sum = train[train.horse_ID == horse_id]['time'].sum()
    race_sum = train[train.horse_ID == horse_id]['race_len'].sum()
    time_ave_list.append(time_sum * 1000 / race_sum)
train['time_ave'] = time_ave_list
train['have_hist'] = [1] * len(train)
train = train.drop('time', axis=1)

time_df = pd.DataFrame(data={'horse_ID': train['horse_ID'],
                             'time_ave': train['time_ave']})
time_df_dump = time_df.drop_duplicates(subset=['horse_ID'])

# 検証データ
time_ave_list = []
have_hist_list = []
for horse_id in valid['horse_ID']:
    if horse_id in time_df['horse_ID'].values:
        target_time = float(time_df_dump.loc[time_df['horse_ID']==horse_id, 'time_ave'])
        time_ave_list.append(target_time)
        have_hist_list.append(1)
    else:
        time_ave_list.append(0)
        have_hist_list.append(0)
valid['time_ave'] = time_ave_list
valid['have_hist'] = have_hist_list
valid = valid.drop('time', axis=1)
    
# テストデータ
time_ave_list = []
have_hist_list = []
for horse_id in test['horse_ID']:
    if horse_id in time_df['horse_ID'].values:
        target_time = float(time_df_dump.loc[time_df['horse_ID']==horse_id, 'time_ave'])
        time_ave_list.append(target_time)
        have_hist_list.append(1)
    else:
        time_ave_list.append(0)
        have_hist_list.append(0)
test['time_ave'] = time_ave_list
test['have_hist'] = have_hist_list
test = test.drop('time', axis=1)
test_rank = test['rank']
    
# 説明変数
x_train = train.drop('rank', axis=1)
x_valid = valid.drop('rank', axis=1)
x_test = test.drop('rank', axis=1)

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

# 目的変数を「0 : 上位(1~3位)」「1: 中位(4~10位)」「2 : 下位(11位-)」に変換
# 上位
y_train.loc[y_train['rank'] <= 3] = 0
y_valid.loc[y_valid['rank'] <= 3] = 0
y_test.loc[y_test['rank'] <= 3] = 0

# 中位
y_train.loc[(y_train['rank'] >= 4) & (y_train['rank'] <= 10)] = 1
y_valid.loc[(y_valid['rank'] >= 4) & (y_valid['rank'] <= 10)] = 1
y_test.loc[(y_test['rank'] >= 4) & (y_test['rank'] <= 10)] = 1

# 下位
y_train.loc[y_train['rank'] >= 11] = 2
y_valid.loc[y_valid['rank'] >= 11] = 2
y_test.loc[y_test['rank'] >= 11] = 2

続いて、単勝予測用にデータ加工を行います。

この箇所では、大きく2つの処理を行っています。

  • ①競争馬ごとに1000m当たりの平均タイムを計算
  • ②着順を「0:上位」「1:中位」「2:下位」に変換

①に関しては説明を割愛しますが、②に関しては「多クラス分類」という予測方法を採用するため、このような処理を行っています。

「多クラス分類」とは、データを複数のクラスに分類する分析のことです。

なお、データ加工後の全項目は下記のとおりです。

レース結果
(目的変数)
  • 着順
レース情報
(説明変数)
  • 馬番
  • 年月日
  • 競馬場ID
  • 回数
  • 日数
  • レース数
  • レース長
  • 天候
  • 馬場
  • レースクラス
競走馬情報
(説明変数)
  • 馬名
  • 年齢
  • 斤量
  • 騎手
  • 厩舎ID
  • 馬体重
  • 馬体重の増減
  • 競走馬ID
  • 騎手ID
  • 人気
  • 単勝オッズ
  • 1000m当たりの平均タイム

②モデル作成

続いて、加工したデータを使用してLightGBMモデルを作成します。

""" 2. モデル作成 """
# ライブラリ
from optuna.integration import lightgbm as lgb
import matplotlib.pyplot as plt
import os
import numpy as np
import pickle
import math


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

# ハイパーパラメータの設定 ----------- (※2)
params = {'task': 'train',
          'boosting_type': 'gbdt',
          'objective': 'multiclass',
          'metric': 'multi_logloss',
          'num_class': 3,
          'learning_rate': 0.02} 

# ハイパーパラメータの設定 ----------- (※3)
model = lgb.train(params,
                  lgb_train,
                  num_boost_round=1000,
                  valid_names=['train', 'valid'],
                  valid_sets=[lgb_train, lgb_valid],
                  early_stopping_rounds=100,
                  verbose_eval=False)

以降で、各要素の詳細を解説します。

LightGBM用のデータセットに変換(※1)

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

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

そのため、予め専用のデータセットを用意する必要があります。

学習(※2)

# 学習 ----------- (※2)
params = {'task': 'train',
          'boosting_type': 'gbdt',
          'objective': 'multiclass',
          'metric': 'multi_logloss',
          'num_class': 3,
          'learning_rate': 0.02} 

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

本処理では、「Optuna」を利用してLightGBMの学習モデルを作成します。
 ※約7年分のレースデータ学習させるのにかかった時間は、約40分

Optunaを利用する理由は、自動でパラメータをチューニングしてくれるからです。

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

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

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

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

boosting_type

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

objective

目的関数を選択できます。
今回は多クラス分類を行うため、‘multiclass’ を指定しています。

metric

誤差関数の測定方法を選択できます。
多クラス分類を選択した際の選択肢としては、“multi_logloss(softmax関数)” と “mulit_error(正答率)” の2種類があります。

num_class ※多クラス分類の場合

分類したいクラスの数を指定できます。
今回は、上位・中位・下位を分類したいので、3を指定しています。

learning_rate

学習率を指定できます。
値を大きくするほど処理時間は短くなりますが、制度は落ちます。
一般的に、0.01を設定することが多いです。

③予測

続いて、作成したLightGBMモデルでテストデータを予測します。

""" 3. 予測 """
# テスト結果を予測 ------------- (※1)
y_pred_prob = model.predict(x_test)

# 同レースの中で上位に入る確率が高い馬を1位とする ------------- (※2)
df_y_pred = pd.DataFrame(data={'race_ID': x_test['race_ID'], 'y_pred': y_pred_prob[:, 0]})
race_id_list = x_test['race_ID'].unique()
df_pred = pd.DataFrame()
for race_id in race_id_list:
    df_part = df_y_pred.loc[df_y_pred['race_ID']==race_id, ]
    max_prob = df_part['y_pred'].max()
    df_part.loc[df_part['y_pred']!=max_prob, 'y_pred_class'] = 0
    df_part.loc[df_part['y_pred']==max_prob, 'y_pred_class'] = 1
    df_pred = df_pred.append(df_part)

以降で、各要素の詳細を解説します。

テスト結果を予測(※1)

# テスト結果を予測 ------------- (※1)
y_pred_prob = model.predict(x_test)

まずは、作成したモデルを用いて各カテゴリ(上位・中位・下位)に入る確率を計算します。

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

ちなみに出力結果は、下記のようになります。(一部のみを記載しています。)

競走馬 0 (上位に入る確率) 1 (中位に入る確率) 2 (下位に入る確率)
0 0.635704 0.304167 0.0601289
1 0.428402 0.445569 0.126028
2 0.473814 0.407631 0.118555

同レースの中で上記に入る確率が最も高い競走馬を1着とする(※2)

# 同レースの中で上位に入る確率が高い馬を1位とする ------------- (※2)
df_y_pred = pd.DataFrame(data={'race_ID': x_test['race_ID'], 'y_pred': y_pred_prob[:, 0]})
race_id_list = x_test['race_ID'].unique()
df_pred = pd.DataFrame()
for race_id in race_id_list:
    df_part = df_y_pred.loc[df_y_pred['race_ID']==race_id, ]
    max_prob = df_part['y_pred'].max()
    df_part.loc[df_part['y_pred']!=max_prob, 'y_pred_class'] = 0
    df_part.loc[df_part['y_pred']==max_prob, 'y_pred_class'] = 1
    df_pred = df_pred.append(df_part)

今回予測したいことは、「どの競走馬が1着になるのか?」ということなので、先ほどの確率から順位に変換する必要があります。

そこで本処理では、上位に入る確率が最も高い競争馬1着になる、という想定で確率→順位に変換しています。

④モデル評価

続いて、モデルの評価を行っていきます。

""" 4. モデル評価 """
# 正答率を計算 -------------- (※1)
# トータルレース数、正答数を5%刻みで取得
df_cnt = calc_accuracy(y_test_org['rank'], df_pred)

acc_rate = df_cnt['acc_cnt'].sum() / df_cnt['total_cnt'].sum()
print('1位の正答数 : ', df_cnt['acc_cnt'].sum())
print('全レース数 : ', df_cnt['total_cnt'].sum())
print('1位の正答率 : ', str(round(acc_rate * 100)) + '%')

acc_rate_list = df_cnt['acc_cnt'] / df_cnt['total_cnt']

# 正答率を描画 -------------- (※2)
x = np.arange(0.00, 1.00, 0.05)
y1 = acc_rate_list * 100
y2 = df_cnt['total_cnt']

fig, ax1 = plt.subplots(1, 1, figsize=(10, 8))
plt.title('Accuracy Rate', fontsize=20)
ax2 = ax1.twinx()
ax1.bar(x, y1, width=0.025, color='lightblue', label='Accuracy Rate')
ax2.plot(x, y2, linestyle='solid', color='k', marker='^', label='race_cnt')

# 軸ラベルを追加
ax1.set_xlabel('y_pred_prob', fontsize=15)
ax1.set_ylabel('accuracy_rate', fontsize=15)
ax2.set_ylabel('total_race_cnt', fontsize=15)

# y軸設定
ax1.set_ylim(0, 100)
ax2.set_ylim(0, max(y2) + 50)

# 棒グラフに値ラベルを追加
for i, k in zip(x, y1):
    if math.isnan(k):
        continue
    plt.text(i, k, str(round(k)) + '%', ha='center', va='bottom')
    
    
# 特徴量の重要度をプロット -------------- (※3)
lgb.plot_importance(model)

以降で、各要素の詳細を解説します。

正答率を計算(※1)

# 正答率を計算 -------------- (※1)
# トータルレース数、正答数を5%刻みで取得
df_cnt = calc_accuracy(y_test_org['rank'], df_pred)

acc_rate = df_cnt['acc_cnt'].sum() / df_cnt['total_cnt'].sum()
print('1位の正答数 : ', df_cnt['acc_cnt'].sum())
print('全レース数 : ', df_cnt['total_cnt'].sum())
print('1位の正答率 : ', str(round(acc_rate * 100)) + '%')

acc_rate_list = df_cnt['acc_cnt'] / df_cnt['total_cnt']

本処理では、「1位の正答数 / 全レース数」といった全体の正答率だけでなく、

「1位の正答数(予測確率が0%~5%)/ 該当レース数」
「1位の正答数(予測確率が5%~10%)/ 該当レース数」
・・・
「1位の正答数(予測確率が95%~100%)/ 該当レース数」

といったように、予測確率毎の正答数も計算しています。

これによって、「予測確率が何%以上の時に購入するべきなのか?」が分かるようになります。

使用メソッドの解説

本処理では、トータルレース数・正答数を5%刻みで取得するために、”calc_accuracy” というメソッドを使用しています。

これは自作メソッドであり、ソースは下記のとおりです。

(ライン数は多いのですが、単純な処理のため解説は割愛します。)

""" メソッド : 正答率を0.05刻みで計算 """
def calc_accuracy(true_list, df_pred):
    
    # 初期値
    acc_cnt_list = [0] * 20
    total_cnt_list = [0] * 20
    y_class_list = df_pred['y_pred_class']
    y_prob_list = df_pred['y_pred']

    for cnt in range(0, len(df_pred)):
        pred = y_class_list[cnt]
        if pred == 0:
            continue
        elif pred == 1:
            prob = y_prob_list[cnt]
            
            # 0 - 0.0.5
            if prob < 0.05:
                total_cnt_list[0] += 1
                true = true_list[cnt]
                if pred == true:
                    acc_cnt_list[0] += 1
                else:
                    continue
            
            # 0.05 - 0.1
            elif prob <= 0.05 and prob < 0.1:
                total_cnt_list[1] += 1
                true = true_list[cnt]
                if pred == true:
                    acc_cnt_list[1] += 1
                else:
                    continue
                
            # 0.1 - 0.15
            elif prob >= 0.1 and prob < 0.15:
                total_cnt_list[2] += 1
                true = true_list[cnt]
                if pred == true:
                    acc_cnt_list[2] += 1
                else:
                    continue
        
            # 0.15 - 0.2
            elif prob >= 0.15 and prob < 0.2:
                total_cnt_list[3] += 1
                true = true_list[cnt]
                if pred == true:
                    acc_cnt_list[3] += 1
                else:
                    continue
                
            # 0.20 - 0.25
            elif prob >= 0.20 and prob < 0.25:
                total_cnt_list[4] += 1
                true = true_list[cnt]
                if pred == true:
                    acc_cnt_list[4] += 1
                else:
                    continue

            # 0.25 - 0.30
            elif prob >= 0.20 and prob < 0.30:
                total_cnt_list[5] += 1
                true = true_list[cnt]
                if pred == true:
                    acc_cnt_list[5] += 1
                else:
                    continue

            # 0.30 - 0.35
            elif prob >= 0.30 and prob < 0.35:
                total_cnt_list[6] += 1
                true = true_list[cnt]
                if pred == true:
                    acc_cnt_list[6] += 1
                else:
                    continue

            # 0.35 - 0.40
            elif prob >= 0.35 and prob < 0.40:
                total_cnt_list[7] += 1
                true = true_list[cnt]
                if pred == true:
                    acc_cnt_list[7] += 1
                else:
                    continue

            # 0.40 - 0.45
            elif prob >= 0.40 and prob < 0.45:
                total_cnt_list[8] += 1
                true = true_list[cnt]
                if pred == true:
                    acc_cnt_list[8] += 1
                else:
                    continue

            # 0.45 - 0.50
            elif prob >= 0.45 and prob < 0.50:
                total_cnt_list[9] += 1
                true = true_list[cnt]
                if pred == true:
                    acc_cnt_list[9] += 1
                else:
                    continue
                
            # 0.50 - 0.55
            elif prob >= 0.50 and prob < 0.55:
                total_cnt_list[10] += 1
                true = true_list[cnt]
                if pred == true:
                    acc_cnt_list[10] += 1
                else:
                    continue

            # 0.55 - 0.60
            elif prob >= 0.55 and prob < 0.60:
                total_cnt_list[11] += 1
                true = true_list[cnt]
                if pred == true:
                    acc_cnt_list[11] += 1
                else:
                    continue

            # 0.60 - 0.65
            elif prob >= 0.60 and prob < 0.65:
                total_cnt_list[12] += 1
                true = true_list[cnt]
                if pred == true:
                    acc_cnt_list[12] += 1
                else:
                    continue

            # 0.65 - 0.70
            elif prob >= 0.65 and prob < 0.70:
                total_cnt_list[13] += 1
                true = true_list[cnt]
                if pred == true:
                    acc_cnt_list[13] += 1
                else:
                    continue

            # 0.70 - 0.75
            elif prob >= 0.70 and prob < 0.75:
                total_cnt_list[14] += 1
                true = true_list[cnt]
                if pred == true:
                    acc_cnt_list[14] += 1
                else:
                    continue

            # 0.75 - 0.80
            elif prob >= 0.75 and prob < 0.80:
                total_cnt_list[15] += 1
                true = true_list[cnt]
                if pred == true:
                    acc_cnt_list[15] += 1
                else:
                    continue

            # 0.80 - 0.85
            elif prob >= 0.80 and prob < 0.85:
                total_cnt_list[16] += 1
                true = true_list[cnt]
                if pred == true:
                    acc_cnt_list[16] += 1
                else:
                    continue

            # 0.85 - 0.90
            elif prob >= 0.85 and prob < 0.90:
                total_cnt_list[17] += 1
                true = true_list[cnt]
                if pred == true:
                    acc_cnt_list[17] += 1
                else:
                    continue
                
            # 0.90 - 0.95
            elif prob >= 0.90 and prob < 0.95:
                total_cnt_list[18] += 1
                true = true_list[cnt]
                if pred == true:
                    acc_cnt_list[18] += 1
                else:
                    continue
                                
            # 0.95 - 1.00
            elif prob >= 0.95 and prob < 1.00:
                total_cnt_list[19] += 1
                true = true_list[cnt]
                if pred == true:
                    acc_cnt_list[19] += 1
                else:
                    continue

    # 2つのリストを結合
    df_cnt = pd.DataFrame(data={'total_cnt': total_cnt_list, 'acc_cnt': acc_cnt_list})

    return df_cnt

正答率を描画(※2)

# 正答率を描画 -------------- (※2)
x = np.arange(0.00, 1.00, 0.05)
y1 = acc_rate_list * 100
y2 = df_cnt['total_cnt']

fig, ax1 = plt.subplots(1, 1, figsize=(10, 8))
plt.title('Accuracy Rate', fontsize=20)
ax2 = ax1.twinx()
ax1.bar(x, y1, width=0.025, color='lightblue', label='Accuracy Rate')
ax2.plot(x, y2, linestyle='solid', color='k', marker='^', label='race_cnt')

# 軸ラベルを追加
ax1.set_xlabel('y_pred_prob', fontsize=15)
ax1.set_ylabel('accuracy_rate', fontsize=15)
ax2.set_ylabel('total_race_cnt', fontsize=15)

# y軸設定
ax1.set_ylim(0, 100)
ax2.set_ylim(0, max(y2) + 50)

# 棒グラフに値ラベルを追加
for i, k in zip(x, y1):
    if math.isnan(k):
        continue
    plt.text(i, k, str(round(k)) + '%', ha='center', va='bottom')


先ほど計算した結果を、棒グラフと折れ線グラフの複合グラフ表します。

凡例を記載するのを忘れていたのですが、

  • 棒グラフ:accurace_rate(正答率)
  • 折れ線グラフ:total_race_cnt(レース数)

となります。

特徴量の重要度をプロット(※3)

# 特徴量の重要度をプロット -------------- (※3)
lgb.plot_importance(model)

続いて、特徴量の重要度をプロットしますが、上記のように1行で簡単に出力できます。

出力結果を見ると、

  • win_odds(単勝オッズ)
  • time_ave(競走馬毎の1000m当たりの平均タイム)

の2つの特徴量が群を抜いて高いことが分かります。

その他の特徴量の影響度が低すぎるので、学習データには改善の余地がありそうですね。

⑤回収率を計算

""" 4. 回収率を計算 """
odds = x_test['win_odds']
step_list = np.arange(0.00, 1.00, 0.05)
return_rate_list = []
for cnt in range(0, len(step_list)):
    step = step_list[cnt]
    input_amount = df_cnt['total_cnt'][cnt:].sum() * 100
    hit_odds_list = x_test.loc[(df_pred['y_pred_class'] == y_test_org['rank']) & 
                               (df_pred['y_pred_class'] == 1) &
                               (df_pred['y_pred'] >= step),
                               'win_odds']
    return_amount = sum(hit_odds_list) * 100
    return_rate_list.append(return_amount / input_amount)

最後に、回収率を計算します。

ここでも正答率を計算した時と同様に、5%刻みで回収率を計算しています。

「予測確率が0%以上のレースを購入した際の回収率」
「予測確率が5%以上のレースを購入した際の回収率」
・・・
「予測確率が100%以上のレースを購入した際の回収率」

回収率を計算した結果は、下記のとおりです。

どのポイントで購入しても、回収率は100%に届きませんでした。。。

特徴量の重要度をプロット(※3)」でも述べましたが、やはり学習データを考え直す必要がありそうですね。

コメント

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