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

## はじめに

[MYJLab Advent Calendar 2022](https://qiita.com/advent-calendar/2022/myjlab)の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"`で文字コードを指定してエンコードします。

```python
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['乗り替わり']`という列を新たに作ります。

```python
# 前走が地方や海外の場合、前走馬体重が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'`の列を取り除くことが出来ます。これで不要な列を取り除きます。

```python
# データをシャッフルして半分をサンプリング
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)
```

## 学習

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

```python
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_))
```

```console:出力
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
```

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

```python
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.metrics`の`classification_report`で分類の精度を確認していきます。

```python
y_test_pred = model.predict(x_test)

from sklearn.metrics import classification_report
print(classification_report(y_test, y_test_pred))
```

```console:出力
              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

```

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


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

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

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

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

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

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

## 結果

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

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

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

## 参考文献

https://qiita.com/c60evaporator/items/a9a049c3469f6b4872c6