競争馬の血統情報(親馬毎の勝率/連帯率/複勝率)を特徴量に追加【LightGBM】

競馬AI

本記事の目的

本記事は、下記事項を目的としています。

目的
  • LightGBMで着順を予測し、100%を超える回収率を得る。

前回モデルの回収率

本ページでは、過去に作成したモデルを改良方法とその結果を紹介します。

過去モデルの詳細は「【競馬AI】スピード指数を特徴量に追加してLightGBMモデルを作成」で解説しているので、詳しく知りたい方は読んでみてください。

ちなみに過去データでシミュレーションした結果、回収率は80%後半でした。。。

前回モデルからの変更点

本記事では、「過去モデルを改善し、その結果回収率は向上するのか」という観点で解説を行っていきます。

今回は下記の変更を加えました。

前回からの変更点
  • 特徴量に「父・母・母父が同じ競走馬の過去成績」を追加

①特徴量に「父・母・母父が同じ競走馬の過去成績」を追加

過去モデルにおける各特徴量の重要度を見ると、上位に「ped_1(母の競走馬)」「ped_5(母父の競走馬)」があります。

競馬は血統が重要と言われており、特に影響があるのは

  • 母父(母方の祖父)

と言われています。

そこで今回は、同じ血統情報を持つ競走馬の勝率・連帯率・複勝率を、特徴量に追加します。

ソースコード解説

以降では、今回加えた変更点についてソースコードを交えて解説していきます。

「父・母・母父が同じ競走馬の過去成績」を特徴量に追加

父・母・母父がxxである競走馬の過去成績(勝率・連帯率・複勝率)の偏差値を計算します。

■用語説明■
  • 勝率:1着になる確率
  • 連帯率:2着以内になる確率
  • 複勝率:3着以内になる確率

①マスタ作成

学習データよりも過去のレースを対象に、父・母・母父がxxである競走馬の勝率・連帯率・複勝率を計算します。

アウトプットとしては、下記のように3つ(父・母・母父)のデータフレーム型変数を作成します。

    # 同じ親を持つ競走馬の成績を計算
    # groupbyで使用する関数を定義
    cnt1_fcn = lambda x: len(x[x == '1.0'])
    cnt2_fcn = lambda x: len(x[x <= '2.0'])
    cnt3_fcn = lambda x: len(x[x <= '3.0'])

    # 父がxxである競走馬
    horse_df['着順'] = horse_df['着順'].astype(str)
    ped0_df = horse_df.loc[horse_df['date'] < df['date'].min(), ['ped_0', '着順']]\
                 .groupby('ped_0').agg([cnt1_fcn, cnt2_fcn, cnt3_fcn, 'size'])
    ped0_df['win_prob'] = ped0_df.iloc[:, 0] / ped0_df.iloc[:, 3]
    ped0_df['second_prob'] = ped0_df.iloc[:, 1] / ped0_df.iloc[:, 3]
    ped0_df['third_prob'] = ped0_df.iloc[:, 2] / ped0_df.iloc[:, 3]

    # 母がxxである競走馬
    ped1_df = horse_df.loc[horse_df['date'] < df['date'].min(), ['ped_1', '着順']]\
                 .groupby('ped_1').agg([cnt1_fcn, cnt2_fcn, cnt3_fcn, 'size'])
    ped1_df['win_prob'] = ped1_df.iloc[:, 0] / ped1_df.iloc[:, 3]
    ped1_df['second_prob'] = ped1_df.iloc[:, 1] / ped1_df.iloc[:, 3]
    ped1_df['third_prob'] = ped1_df.iloc[:, 2] / ped1_df.iloc[:, 3]

    # 母父がxxである競走馬
    ped5_df = horse_df.loc[horse_df['date'] < df['date'].min(), ['ped_5', '着順']]\
                 .groupby('ped_5').agg([cnt1_fcn, cnt2_fcn, cnt3_fcn, 'size'])
    ped5_df['win_prob'] = ped5_df.iloc[:, 0] / ped5_df.iloc[:, 3]
    ped5_df['second_prob'] = ped5_df.iloc[:, 1] / ped5_df.iloc[:, 3]
    ped5_df['third_prob'] = ped5_df.iloc[:, 2] / ped5_df.iloc[:, 3]
ソースコード解説
  1. 1着になるレース数・2着以内になるレース数・3着以内になるレース数をカウントする関数を定義(groupbyで使用)
  2. カウントするために、変数 “horse_df” の「着順」カラムの型を文字列型に変更

    ——————— 以降の処理は、父・母・母父毎に実行 ———————–
  3. 変数 “horse_df” のうち、「日付」カラムが、変数 “df” の最も古い日時よりも古いデータを対象に、競走馬(親)毎に下記のレース数をカウントする
     ・1着になるレース数
     ・2着以内になるレース数
     ・3着以内になるレース数
  4. データフレーム型変数に、勝率・連帯率・複勝率を計算する

変数解説

horse_df
df

②レース内における単勝率・連帯率・複勝率の偏差値を計算

レース内での相対的な能力を表現するために、偏差値を計算します。

偏差値 = (値 – 平均値) / 標準偏差 + 50

今回は、上位計算式の「+50」を排除した値を特徴量に追加します。

    for race_id in tqdm(df['race_ID'].unique()):
        date = int(df.loc[df['race_ID'] == race_id, 'date'].head(1))

        # 父・母・母父がxxである競走馬の過去成績(勝率・連帯率・複勝率の偏差値)
        ped0_list = list(df.loc[df['race_ID'] == race_id, 'ped_0'])
        ped1_list = list(df.loc[df['race_ID'] == race_id, 'ped_1'])
        ped5_list = list(df.loc[df['race_ID'] == race_id, 'ped_5'])

        ped0_win = [float(ped0_df.loc[ped0_df.index == x, 'win_prob'])\
                    if x in ped0_df.index else 0 for x in ped0_list]
        ped0_second = [float(ped0_df.loc[ped0_df.index == x, 'second_prob'])\
                       if x in ped0_df.index else 0 for x in ped0_list]
        ped0_third = [float(ped0_df.loc[ped0_df.index == x, 'third_prob'])\
                      if x in ped0_df.index else 0 for x in ped0_list]
        ped1_win = [float(ped1_df.loc[ped1_df.index == x, 'win_prob'])\
                    if x in ped1_df.index else 0 for x in ped1_list]
        ped1_second = [float(ped1_df.loc[ped1_df.index == x, 'second_prob'])\
                       if x in ped1_df.index else 0 for x in ped1_list]
        ped1_third = [float(ped1_df.loc[ped1_df.index == x, 'third_prob'])\
                      if x in ped1_df.index else 0 for x in ped1_list]
        ped5_win = [float(ped5_df.loc[ped5_df.index == x, 'win_prob'])\
                    if x in ped5_df.index else 0 for x in ped5_list]
        ped5_second = [float(ped5_df.loc[ped5_df.index == x, 'second_prob'])\
                       if x in ped5_df.index else 0 for x in ped5_list]
        ped5_third = [float(ped5_df.loc[ped5_df.index == x, 'third_prob'])\
                      if x in ped5_df.index else 0 for x in ped5_list]

        # 偏差値
        ped0_win_dev += [(x - np.mean(ped0_win)) / np.std(ped0_win) for x in ped0_win]
        ped0_second_dev += [(x - np.mean(ped0_second)) / np.std(ped0_second) for x in ped0_second]
        ped0_third_dev += [(x - np.mean(ped0_third)) / np.std(ped0_third) for x in ped0_third]
        ped1_win_dev += [(x - np.mean(ped1_win)) / np.std(ped1_win) for x in ped1_win]
        ped1_second_dev += [(x - np.mean(ped1_second)) / np.std(ped1_second) for x in ped1_second]
        ped1_third_dev += [(x - np.mean(ped1_third)) / np.std(ped1_third) for x in ped1_third]
        ped5_win_dev += [(x - np.mean(ped5_win)) / np.std(ped5_win) for x in ped5_win]
        ped5_second_dev += [(x - np.mean(ped5_second)) / np.std(ped5_second) for x in ped5_second]
        ped5_third_dev += [(x - np.mean(ped5_third)) / np.std(ped5_third) for x in ped5_third]
ソースコード解説
  1. 同レースの競走馬の父・母・母父の競走馬リストを作成する
  2. ①で作成したマスタから、馬名が一致する単勝率・連帯率・複勝率を取得する
  3. 同レース内における偏差値を計算

回収率

今回の変化によって回収率がどの程度になるか、確認します。

回収率の計算

「予測確率がxx%を超えたら、その競争馬に単勝を賭ける」というロジックで、回収率を計算します。

本処理では、上記xxの値を0%~500%の間で10%刻みで計算しています。

# 予測結果を整理
df_pred = pd.DataFrame(data={'race_ID': x_test['race_ID'],
                             'y_pred_prob': y_pred_prob,
                             'y_test': y_test[0],
                             'win_odds': odds_df_test['win_odds']})

# 回収率を計算
step_list = [x * 0.01 for x in range(0, 31)]
return_rate_list = []

for cnt in range(0, len(step_list)):
    step = step_list[cnt]
    df_pred['y_pred_class'] = [1 if x >= step else 0 for x in df_pred['y_pred_prob']] 
    input_amount = len(df_pred.loc[df_pred['y_pred_class'] == 1, 'y_pred_prob']) 
    hit_odds_list = list(df_pred.loc[(df_pred['y_pred_class'] == df_pred['y_test']) &\
                                     (df_pred['y_pred_class'] == 1),
                                     'win_odds'])
    return_amount = sum(hit_odds_list)
    if input_amount > 0:
        return_rate_list.append(return_amount / input_amount)
    elif input_amount == 0:
        return_rate_list.append(float('nan'))
ソースコード解説
  1. レースID、予測確率、実際の着順、オッズを1つのデータフレーム型変数に格納
  2. 0-0.3を0.05刻みするリストを作成

    ——————– 2のリストでループ ——————-
  3. 予測確率がxxを超えた場合「y_pred_class」カラムの値を1にし、下回った場合は0を挿入する
  4. 3の値が1の数をカウントする(投入金額)
  5. 「y_pred_class」と「y_test」の値が両方1となる行数をカウントする(回収金額)
  6. 4と5の値を割って、回収率を計算する

回収率を描画

計算した回収率をグラフ化します。

# 回収率を描画
x = step_list
y = return_rate_list
fig, ax = plt.subplots(1, 1, figsize=(10, 8))
ax.plot(x, y, linestyle='solid', color='k', marker='^', label='race_cnt')
plt.title('Return_Rate', fontsize=20)

# 軸ラベルを追加
ax.set_xlabel('y_pred_prob', fontsize=15)
ax.set_ylabel('return_rate', fontsize=15)

# y軸設定
ax.set_ylim(0.5, 1.5)

# 値ラベルを追加
for i, k in zip(x, y):
    if math.isnan(k):
        continue
    elif k < 0.5 :
        continue
    else:
        plt.text(i, k + 0.05, str(round(k * 100)) + '%', fontsize=10)  

回収率の結果は以下のようになりました。

全体的に回収率が70%と、あまり良い結果になりませんでした。。。

以前のモデルは回収率が80%後半だったので、逆に悪くなっていますね。

以前のモデルの回収率

この原因としては、以前よりも学習データ数を増やしたため、傾向が変わったことが考えられます。

以降で紹介する各特徴量の重要度を見ると、傾向が変わっていることが一目瞭然です。

特徴量の重要度

次に、特徴量の重要度を確認していきます。

lgb.plot_importance(model, max_num_features=20)

今回は新たに、

  • ped_win_dev(同じ親馬を持つ競争馬の単勝率)
  • ped_second_dev(同じ親馬を持つ競走馬の連帯率)
  • ped_third_dev(同じ親馬を持つ競走馬の複勝率)

の3項目(父・母・母父の3パターンがあるので、本当は9項目です。)を新たに追加しましたが、それぞれが上位に食い込んでいないので、あまり影響のない項目でした。。。

ちなみに、以前のモデルの特徴量の重要度を見ると、大きく傾向が変わっているのですが、これは学習データ数を大きく増やしたことが原因と考えられます。(2年→5年分のレースを学習させました。)

これまではスピード指数の重要度が高かったのですが、今回は競争馬IDや親馬ID等のカテゴリ変数が上位に食い込んでいます。

今後はこれらに関する特徴量を追加すると、効果がありそうですね。

コメント

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