【第5回】Python で競馬予想してみる 　~ LightGBM ~

前回までに、scikit-learn のロジスティック回帰を使って競馬予測のモデルを作ってみた。
説明変数などを工夫してみても馬券回収率（単勝）は 80％超程度までで上昇は見込めず

この回からは、流行りの決定木アルゴリズムに基づいた勾配ブースティング（Gradient Boosting）の機械学習フレームワーク __LightGBM__ を使って競馬予想してみるのだ

#データの取得
###ターゲットフロンティアのレース検索データを出力して使用
前回までと同様にターゲットフロンティアのレース検索で 2000年からのレースをすべて検索
前走読込み ボタンを押して前走のレースID等を取得して、今回はすべてのデータを CSV で出力した
全データを出力すると出力時間や pandas での読込みに時間がかかるのだが、使いたいデータがないときの出力し直しが面倒なのでとにかく全データを出力したのだ

参照：[【第1回】Python で競馬予想してみる　~ 環境設定 とデータの取得 ~](https://qiita.com/H58/items/eed0fa93ffc1a1ece289)

#データの前処理
###pandasでcsvファイル読み込み
###使用する 特徴量 を選択
予想するレース（直近レース）で使用する特徴量は下記の通り

```text:Out
Index(['レースID(新)', '前走レースID(新)', '性別', '年齢', '父タイプ名', '母父タイプ名', '場所', 'クラス名',
       '距離', '芝・ダ', 'ｺｰﾅｰ', '馬場状態', '出走頭数', '枠番', '馬番', '騎手コード', '斤量', 'キャリア',
       '間隔'],
      dtype='object')
```

上記の直近レースの 前走レースID(新) に紐づけで結合するための過去走で使用する特徴量は

```text:Out
Index(['レースID(新)', '前走レースID(新)', '場所', 'クラス名', '距離', '芝・ダ', 'ｺｰﾅｰ', '馬場状態',
       '出走頭数', '枠番', '馬番', '騎手コード', '斤量', 'キャリア', '間隔', '確定着順', '斤量体重比',
       '単勝オッズ', '人気', '着差', '上3F地点差', '脚質', 'Ave-3F', '上り3F', '上り3F順', '馬体重増減',
       '補正', '補9'],
      dtype='object')
```

###ダミー変数を作成

```Python:build.ipynb
# ダミー変数を生成
rd_last = pd.get_dummies(rd_last, columns=['場所', 'クラス名', '芝・ダ', '馬場状態', '性別', '父タイプ名', '母父タイプ名']) # 直近レース用
rd_past = pd.get_dummies(rd_past, columns=['場所', 'クラス名', '芝・ダ', '馬場状態', '脚質']) # 過去レース用
```

###直近レースデータに加工走のデータをマージ
今回は 5走前まで連結

###欠損値処理はしない
LightGBM は欠損値があっても学習・予測できるらしい
欠損値処理はいつでもできるので、とりあえず残しておくのだ

###目的変数の処理
目的変数は、出走馬の着順を 3着以内なら 1、4着以下なら 0 に変更した

```Python:build.ipynb
# 着順が3着以内かどうかのカラムを追加する
f_ranking = lambda x: 1 if x in [1, 2, 3] else 0
df['3着以内'] = df['確定着順'].map(f_ranking)
```

参照：[【第2回】Python で競馬予想してみる　~ データの前処理（CSVデータの読み込み～各種処理） ~](https://qiita.com/H58/items/6817fbb920ae55cd475a)

#モデルの学習
###アンダーサンプリングは必要 ?
アンダーサンプリングをした方が良いのか分からないので、今回はしない

```Python:build.ipynb
import lightgbm as lgb #LightGBM
# 説明変数をdataXに格納
dataX = df

# 目的変数をdataYに格納
dataY = df['3着以内']

# データの分割を行う（学習用データ 0.8 評価用データ 0.2） stratify:層化抽出
X_train, X_eval, y_train, y_eval = train_test_split(dataX, dataY, test_size=0.2, stratify=dataY)

# LightGBM用のデータセットに入れる
lgb_train = lgb.Dataset(X_train.values, y_train)
lgb_eval = lgb.Dataset(X_eval.values, y_eval)

# LightGBM parameters
params = {
    'task': 'train', 
    'objective': 'binary', 
    'metric': 'binary_logloss'
}

# モデルの学習
model = lgb.train(params, lgb_train, valid_sets=lgb_eval,
                  verbose_eval=50,  # 50イテレーション毎に学習結果出力
                  num_boost_round=1500,  # 最大イテレーション回数指定
                  early_stopping_rounds=100)

# テストデータの予測 (クラス1の予測確率(クラス1である確率)を返す)
y_pred_proba_train = model.predict(X_train)
y_pred_proba = model.predict(X_eval)
accuracy_train = roc_auc_score(y_train, y_pred_proba_train)
accuracy_eval = roc_auc_score(y_eval,y_pred_proba)

print("  Accuracy_train = {}".format(accuracy_train))
print("  Accuracy_eval = {}".format(accuracy_eval))
```

結果は
予想外に時間がかかるのだ

【追記】騎手コードをカテゴリ変数化するのを忘れたので、修正して再学習
良いのか悪いのかよく分からんのだ。

```text:Out
  roc_auc_score = 0.8105012696640244
  roc_auc_score = 0.7694469787133695
混同行列
 [[ 10234  33072]
 [  7232 154447]]
適合率 0.5859383945952136
再現率 0.23631829307717175
F値 0.336799842032515
```
feature	importance を見てみると
騎手や1走前（n_〇〇〇：n走前の〇〇〇データという意味）のデータが上位に。
下位にはレースのクラス名が勢ぞろい

```text:Out
	feature	importance
6	騎手コード	0.027750
75	1_単勝オッズ	0.018916
80	1_上り3F	0.017527
9	間隔	0.015901
79	1_Ave-3F	0.015689
77	1_着差	0.015336
69	1_騎手コード	0.013781
136	2_単勝オッズ	0.013616
83	1_補正	0.013380
84	1_補9	0.013263
140	2_Ave-3F	0.013239
197	3_単勝オッズ	0.012744
130	2_騎手コード	0.012697
191	3_騎手コード	0.012532
141	2_上り3F	0.012367
258	4_単勝オッズ	0.011896
319	5_単勝オッズ	0.011755
201	3_Ave-3F	0.011708
73	1_確定着順	0.011496
145	2_補9	0.010931
323	5_Ave-3F	0.010860
313	5_騎手コード	0.010813
206	3_補9	0.010671
262	4_Ave-3F	0.010530
252	4_騎手コード	0.010506
202	3_上り3F	0.010483
263	4_上り3F	0.010342
(中略)
294	4_クラス名_ＪＧ２	0.000000
159	2_クラス名_2勝	0.000000
285	4_クラス名_OP(L)	0.000000
172	2_クラス名_ＪＧ２	0.000000
167	2_クラス名_重賞	0.000000
171	2_クラス名_ＪＧ１	0.000000
346	5_クラス名_OP(L)	0.000000
293	4_クラス名_ＪＧ１	0.000000
343	5_クラス名_3勝	0.000000
350	5_クラス名_重賞	0.000000
```

今回はここまで:wave: