LoginSignup
9
3

More than 1 year has passed since last update.

XGBoostでチャンピオンズカップを予想してみたという話

Last updated at Posted at 2022-12-08

はじめに

MYJLab Advent Calendar 2022の9日目の記事です。

Pythonで機械学習(XGBoostによる分類)を行って競馬の予想してみようという内容になっています。
先にネタバレしますと何とも言えない結果となりました。

環境

データ取得はTARGET frontier JVというアプリケーション(2022年12月8日時点での最新版)を使用します。
(注:JRA-VAN データラボの契約が必要です)

機械学習・モデル作成はGoogle Colaboratoryを使用します。

データ取得

2022年12月4日のチャンピオンズカップを予想したいので、同条件である中京ダート1800mのレース結果等のデータを集めます。
TARGET frontier JVから、2012年1月1日~2022年11月31日の中京ダート1800mのデータを一走前のデータも含めてCSV形式でダウンロードします。これを学習データにしてモデルを構築します。

2022年12月4日のチャンピオンズカップの、一走前のデータも含めた出馬表のデータもCSV形式でダウンロードします。構築したモデルを使って、このデータで予想します。

ダウンロードしたCSVファイルはGoogle Colaboratoryで使用できるようにGoogle Driveにアップロードしておきます。

前処理

まずはデータの前処理を行って機械学習が出来るように準備します。

Google DriveからCSVファイルを使えるようにします。
このとき,文字化け防止のためにencoding="Shift-JIS"で文字コードを指定してエンコードします。

from google.colab import drive
drive.mount('/content/drive')

df = pd.read_csv('/content/drive/MyDrive/〇〇/〇〇.csv', encoding="Shift-JIS")

前走のデータがない場合のデータの処理を行います。前走馬体重は該当レースの馬体重にし、上り3Fタイムや通過順はNaNにします。

scikit-learnが文字列対応していないのでOne-Hotエンコーディングで性別と前走が芝かダートかどうかについてのダミー変数を作成します。

また、乗り替わりの有無も説明変数とし用いたいので、df['騎手名']df['前走騎手名']が一致した場合は1を、そうでない場合には0を返す関数を定義し、それを用いてdf['乗り替わり']という列を新たに作ります。

# 前走が地方や海外の場合、前走馬体重が0になってしまうため、前走馬体重を(今回の)馬体重と同じにする
df.loc[df['前走馬体重'] == 0,'前走馬体重'] =df.loc[df['前走馬体重'] == 0,'馬体重']

# 前走が地方や海外の場合データが無いため、該当部分をNaNにする
for element in ['前走上がり3Fタイム','前走通過順1', '前走通過順2','前走通過順3', '前走通過順4']:
  df.loc[df[element] == 0,element] = np.NaN

# 性別のone-hot化
one_hot_vector = pd.get_dummies(df[['性別']],drop_first=False)
df = pd.concat([df, one_hot_vector], axis=1)

# 前走芝・ダのone-hot
one_hot_vector = pd.get_dummies(df[['前走芝・ダ']],drop_first=False)
df = pd.concat([df, one_hot_vector], axis=1)

# 乗り替わりの有無
def categorize(x):
  if x['騎手名'] == x['前走騎手名']:
    return 0
  else:
    return 1

# '乗り替わり'という列を新たに作る
df['乗り替わり'] = df.apply(categorize, axis=1)

データをシャッフル、サンプリングしてから、hold-out法で訓練用とテスト用のデータに分割します。
df.loc[:,~df.columns.isin(['xxx','yyy'])]と書くことで、'xxx','yyy'の列を取り除くことが出来ます。これで不要な列を取り除きます。

# データをシャッフルして半分をサンプリング
df = df.sample(frac=0.5, random_state=0).reset_index(drop=True)

# 説明変数を取り出す
x_data = df.loc[:,~df.columns.isin(['入線着順','騎手名','前走騎手名','異常コード','前走異常コード','前走芝・ダ','性別'])]
# 目的変数は'入線着順'
y_data = df.loc[:,['入線着順']]

from sklearn.model_selection import train_test_split

x_train, x_test, y_train, y_test = train_test_split(x_data, y_data, train_size = 0.8)

学習

学習する前にグリッドサーチでパラメータ探索して、最適なパラメータを見つけていきます。

from sklearn.model_selection import RepeatedKFold
cv = RepeatedKFold(n_splits=5, n_repeats=3, random_state=0)

import xgboost as xgb
model = xgb.XGBClassifier()

from sklearn.model_selection import GridSearchCV

params = {
    'booster': ['gbtree'], 'n_estimators': [10, 30, 50],'max_depth': [3,5,7],  'learning_rate': [0.3, 0.5, 0.7], 
    'colsample_bytree': [0.3, 0.5, 0.7],'random_state': [0], "objective":["multi:softmax"],"num_class": [16] 
}

gs = GridSearchCV(estimator=model, param_grid=params, cv=cv, scoring = 'f1_micro')
gs.fit(x_train, y_train)

print('best score: {:0.3f}'.format(gs.score(x_test, y_test)))
print('best params: {}'.format(gs.best_params_))
print('best val score:  {:0.3f}'.format(gs.best_score_))
出力
best score: 0.090
best params: {'booster': 'gbtree', 'colsample_bytree': 0.5, 'learning_rate': 0.5, 'max_depth': 3, 'n_estimators': 50, 'num_class': 16, 'objective': 'multi:softmax', 'random_state': 0}
best val score:  0.102

これを元にパラメータを設定し、学習を行いモデルを構築していきます。

model=xgb.XGBClassifier(
    booster= 'gbtree', colsample_bytree= 0.5, learning_rate= 0.25,max_depth= 3, n_estimators= 50, 
    num_class= 16, objective='multi:softmax', random_state= 0
)

model.fit(x_train, y_train)

精度確認

sklearn.metricsclassification_reportで分類の精度を確認していきます。

y_test_pred = model.predict(x_test)

from sklearn.metrics import classification_report
print(classification_report(y_test, y_test_pred))
出力
              precision    recall  f1-score   support

           1       0.12      0.22      0.16        41
           2       0.13      0.10      0.11        52
           3       0.15      0.12      0.13        48
           4       0.11      0.12      0.12        41
           5       0.04      0.10      0.06        20
           6       0.03      0.03      0.03        39
           7       0.02      0.02      0.02        46
           8       0.03      0.03      0.03        36
           9       0.23      0.13      0.17        38
          10       0.13      0.11      0.12        38
          11       0.09      0.12      0.10        32
          12       0.00      0.00      0.00        22
          13       0.07      0.05      0.05        22
          14       0.09      0.04      0.05        27
          15       0.07      0.06      0.06        16
          16       0.18      0.15      0.17        13

    accuracy                           0.09       531
   macro avg       0.09      0.09      0.09       531
weighted avg       0.10      0.09      0.09       531

めちゃくちゃ精度が悪い
とはいえ、機械学習等で競馬の予想をする場合、そのほとんどが馬券を当てるということがメインになっています。なので、投資した金額を配当額が上回ったかどうかという、回収率という評価指数を用いる必要がありそうです。

いよいよチャンピオンズカップの予想をしてみる

df_test = pd.read_csv('/content/drive/MyDrive/〇〇/〇〇.csv', encoding="Shift-JIS")

学習データと同じように前処理を行います。
カラム名が学習データと違っている場合はdf.rename(columns={'xxx':'XXX'})などでカラム名の変更を行い、学習データのカラム名と一致させる必要があります。

test_pred = model.predict(test_data)
pred_df = pd.Series(test_pred, index=df_test['馬名'])
print(pred_df)

出力結果が下になります。右側の数字は着順です。

出力
グロリアムンディ     16
サンライズホープ      1
ハピ            3
スマッシングハーツ     3
ジュンライトボルト     5
レッドガラン       12
オーヴェルニュ      11
サンライズノヴァ      8
ノットゥルノ       11
クラウンプライド      1
バーデンヴァイラー    15
テーオーケインズ     12
シャマル          3
タガノビューティー     1
サクラアリュール      9
レッドソルダード      1

結果

実際のチャンピオンズカップの結果は、5着と予想したジュンライトボルトが優勝し、1着と予想したクラウンプライドが2着に、3着と予想したハピが3着に来るという結果でした。
これだけだったら結構よさそうな感じに見えるのですが、他に1着だと予想したサンライズホープは6着、タガノビューティーは10着、レッドソルダードは16着という散々な結果になりました。

XGBoost以外の分析手法を用いたり、前走だけではなく、2走前、3走前のデータを用いたりすることで精度を改善できそうです。また、データがない場合の処理を見直す必要もありそうです。

競馬で大儲けするのはまだまだ先になりそうです。

参考文献

9
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
3