PR

脚質と持ちタイムからレース展開を予測

データサイエンス
この記事は約93分で読めます。
スポンサーリンク
スポンサーリンク

はじめに¶

私は競馬予想AIの開発をしています。動画で制作過程の解説をしています。良ければ見ていってください。

競馬予想AI #1 アクセス回数を1000回減らす意外な方法とは?【ゆっくりと作る競馬予想プログラムソフト開発 part1】
競馬予想プログラムソフトの開発過程を投稿していきます。単勝回収率100%超えを目指して、競馬予想AIを機械学習で作成します。今回の内容は、以下のサイトで詳細を解説しています。ソースを参考にされたい方はそちらを閲覧ください。

8.過去成績からレース展開の予測

前回は脚質の分析とその予測の可能性を見てきた。
今回の話では、各出走馬の過去成績による脚質の変遷と持ちタイムからレース展開の予測を試みる。

レース展開を知りたい理由は、戦法の一つである脚質がレース展開によって勝ちやすい負けやすいといった話が一般的にあり、そういった影響も勝ち馬を絞り込む上では重要な要素となる可能性が十分ありうるからである。
そのため、レース展開の予測ができるかどうかを調べることは必要であると考える。

スポンサーリンク

8-1.話の内容

  1. 持ちタイムの分析
  2. レース展開の分析
  3. レース展開の予測
スポンサーリンク

8-2.前提の話: レース展開(ペース情報)って?¶

GPTに聞いてみた結果が以下

競馬のレース展開にはさまざまなパターンがある。以下はいくつかの典型的な例である。馬の特徴やペース、コースの形状などによって、展開が異なることがある。

1. 逃げ切り(逃げ馬有利)¶

逃げ馬が最初からリードを取り、そのままペースを維持してゴールまで先頭を守る展開である。特に前残りの馬場や短距離戦でよく見られる。逃げ馬にとってはスタミナよりもスピードと持続力が重要である。

  • 例: スプリントレース(1200m前後) では、スタートから勢いよく飛び出して、最後まで先頭をキープすることが多い。

2. 差し馬の台頭¶

レースの中盤から後方でじっと我慢していた馬が、直線に入ってから加速し、先行馬を次々と交わしていく展開である。差し馬に有利な展開は、前半のペースが速く、逃げや先行馬がバテやすい状況でよく見られる。

  • 例: 中距離レース(1800m – 2400m) では、ペースが落ち着くことが多く、終盤の差し馬が活躍する場面が増える。

3. 追い込み(追い込み馬の豪脚)¶

レースの後方で待機していた馬が、最後の直線で猛烈なスピードを見せて一気に追い込み、ゴール直前で逆転する展開である。これは特に長距離戦やペースが速く、前がバテた展開で見られる。

  • 例: 長距離レース(2400m以上) では、追い込み馬が最後に爆発的な脚を使って一気に追い上げることがしばしばある。

4. ペースメーカーの存在による展開¶

時にはレースのペースを乱すために、あえて逃げ馬や先行馬にペースを上げさせる「ペースメーカー」として出走する馬が存在する。このような馬の存在によって、他の馬が早めに仕掛けざるを得なくなり、後ろから差す馬が有利になる展開である。

5. 団子状態のレース¶

全体的にペースが遅く、馬群が固まったまま直線に入る展開である。この場合、どの馬もチャンスがあり、抜け出すタイミングが重要になる。位置取りや騎手の判断力が勝敗を分けることが多い。

6. ハイペースからのバテ合い¶

レース序盤から中盤にかけてペースが速すぎる場合、逃げや先行馬がバテてしまい、最終的には後方待機馬が有利になる展開である。特に前に行った馬たちが早いペースに巻き込まれたとき、終盤の粘りがなくなり、後続の馬たちが一気に追い込む展開がよくある。

これらの展開のパターンは、馬場状態、天候、出走馬の脚質、距離などに影響されることも多い。競馬ファンとしては、これらの展開を読みながら予想を楽しむことが、競馬の醍醐味の一つである。

上記のレース展開例から以下の5ケースのレース展開がありうるのではと考える

  • 速いペース × 逃げが勝ち切り
  • 速いペース × 逃げが負ける
  • 遅いペース × 逃げが勝ち切り
  • 遅いペース × 逃げが負ける
  • 脚質の影響がないケース

以上の5ケースを知るためにもう少し話を嚙み砕いて、レース展開を予想する上では以下2つに着目すると良いのではと考えた。

一つ目に、逃げ馬や人気馬の影響によってペースの速い遅いが決まるのではないか
(逃げ馬が多いレースはどの馬も前に行きたがるので、ペースが速くなるのでは)
二つ目に、先頭集団と後方集団とのパワーバランスによって逃げの勝ち負けが決まるのではないか
(ペースの速い遅いに関わらず先頭集団の能力が高ければ、後方が差せない場合があり、逆のパターンもあるのでは)

つまり、逃げ馬と他競走馬の関係を知ることが重要だと考えた。

上記の話から、まずはレースのペースを知る必要がありそう。
そのためには、レース出走時点での各馬のペース情報と脚質が鍵になってきそう。

なので、まず最初に持ちタイムの分析から始めていく。

スポンサーリンク

8-3.前準備¶

ソースの一部は有料のものを使ってます。
同じように分析したい方は、以下の記事から入手ください。

ゼロから作る競馬予想モデル・機械学習入門

In [1]:
import pathlib
import warnings
import lightgbm as lgbm
import pandas as pd
import tqdm
import datetime
import matplotlib.pyplot as plt
import japanize_matplotlib
import seaborn as sns
import numpy as np

import sys
sys.path.append(".")
sys.path.append("..")
from src.data_manager.preprocess_tools import DataPreProcessor  # noqa
from src.data_manager.data_loader import DataLoader  # noqa

warnings.filterwarnings("ignore")

root_dir = pathlib.Path(".").absolute().parent
dbpath = root_dir / "data" / "keibadata.db"
start_year = 2000  # DBが持つ最古の年を指定
split_year = 2014  # 学習対象期間の開始年を指定
target_year = 2019  # テスト対象期間の開始年を指定
end_year = 2023  # テスト対象期間の終了年を指定 (当然DBに対象年のデータがあること)

# 各種インスタンスの作成
data_loader = DataLoader(
    start_year,
    end_year,
    dbpath=dbpath  # dbpathは各種環境に合わせてパスを指定してください。絶対パス推奨
)

dataPreP = DataPreProcessor()

df = data_loader.load_racedata()
dfblood = data_loader.load_horseblood()

df = dataPreP.exec_pipeline(
    df, dfblood, ["s", "b", "bs", "bbs", "ss", "sss", "ssss", "bbbs"])
2024-10-06 10:54:16.785 | INFO     | src.data_manager.data_loader:load_racedata:23 - Get Year Range: 2000 -> 2023.
2024-10-06 10:54:16.785 | INFO     | src.data_manager.data_loader:load_racedata:24 - Loading Race Info ...
2024-10-06 10:54:17.452 | INFO     | src.data_manager.data_loader:load_racedata:26 - Loading Race Data ...
2024-10-06 10:54:32.502 | INFO     | src.data_manager.data_loader:load_racedata:28 - Merging Race Info and Race Data ...
2024-10-06 10:54:34.667 | INFO     | src.data_manager.data_loader:load_horseblood:45 - Loading Horse Blood ...
2024-10-06 10:55:00.085 | INFO     | src.data_manager.preprocess_tools:__0_check_use_save_checkpoints:100 - Start PreProcess #0 ...
2024-10-06 10:55:00.085 | INFO     | src.data_manager.preprocess_tools:__1_exec_all_sub_prep1:103 - Start PreProcess #1 ...
2024-10-06 10:55:06.252 | INFO     | src.data_manager.preprocess_tools:__2_exec_all_sub_prep2:105 - Start PreProcess #2 ...
2024-10-06 10:55:19.085 | INFO     | src.data_manager.preprocess_tools:__3_convert_type_str_to_number:107 - Start PreProcess #3 ...
2024-10-06 10:55:22.835 | INFO     | src.data_manager.preprocess_tools:__4_drop_or_fillin_none_data:109 - Start PreProcess #4 ...
2024-10-06 10:55:26.401 | INFO     | src.data_manager.preprocess_tools:__5_exec_all_sub_prep5:111 - Start PreProcess #5 ...
2024-10-06 10:55:44.069 | INFO     | src.data_manager.preprocess_tools:__6_convert_label_to_rate_info:113 - Start PreProcess #6 ...
2024-10-06 10:55:54.685 | INFO     | src.data_manager.preprocess_tools:__7_convert_distance_to_smile:115 - Start PreProcess #7 ...
2024-10-06 10:55:54.919 | INFO     | src.data_manager.preprocess_tools:__8_category_encoding:117 - Start PreProcess #8 ...
2024-10-06 10:55:59.702 | INFO     | src.data_manager.preprocess_tools:__9_convert_raceClass_to_grade:119 - Start PreProcess #9 ...
2024-10-06 10:56:07.069 | INFO     | src.data_manager.preprocess_tools:__10_add_bloods_info:123 - Start PreProcess #10 ...
スポンサーリンク

8-4.持ちタイムの分析¶

8-4-0.持ちタイムとはなにかを決める

ここではまず出走馬たちの持ちタイムとレースのペース情報との関係を見てみる

ということで、持ちタイムの情報になりそうなデータがどのカラムに入っているのか思い出す

In [2]:
df.columns
Out[2]:
Index(['raceId', 'place', 'raceName', 'raceDetail', 'raceDate', 'startTime',
       'distance', 'weather', 'field', 'condition', 'direction', 'inoutside',
       'rapTime', 'rapSumTime', 'f3Ftol3F', 'remarks', 'number', 'boxNum',
       'label', 'odds', 'favorite', 'horseName', 'horseId', 'sex', 'age',
       'jweight', 'jockey', 'jockeyId', 'time', 'passLabel', 'last3F',
       'weight', 'gl', 'teacher', 'teacherId', 'boss', 'race_span', 'horseNum',
       'rough_horse', 'rough_race', 'f3F', 'l3F', 'f3F_vel', 'l3F_vel',
       'rapTime_vel', 'rapSumTime_vel', 'velocity', 'last3F_vel', 'toL3F_vel',
       'label_1C', 'label_2C', 'label_3C', 'label_4C', 'label_rate',
       'label_lastC', 'label_diff', 'dist_cat', 'place_en', 'field_en',
       'sex_en', 'condition_en', 'jockeyId_en', 'teacherId_en', 'horseId_en',
       'dist_cat_en', 'raceGrade', 'stallionId', 'stallionName', 'breedId',
       'breedName', 'bStallionId', 'bStallionName', 'b2StallionId',
       'b2StallionName', 'sStallionId', 'sStallionName', 's2StallionId',
       's2StallionName', 's3StallionId', 's3StallionName', 'b3StallionId',
       'b3StallionName'],
      dtype='object')

タイム関連情報は以下のカラムになりそう

  • time: 出走タイム
  • last3F: 上り3Fタイム
  • velocity: 60 × レース距離 / 出走タイム
  • last3F_vel: 600 / 上り3Fタイム
  • toL3F_vel: 上り3Fに到達するまでの速度

そもそも持ちタイムとは「同じ距離のレースの中で最も速かったタイムのこと」と考えられることが多い。
しかし、レース距離や条件などで色々変わったりすることも考慮が必要であると考える。
また、最後の上り3Fはこれまでのペースに関わらず全速力で走ることが要求されると思われるので、上り3Fに至るまでの速度を持ちタイムと見た方がペースを予測する上では重要ではないかと考える。

よってここでは、持ちタイムを以下に定義する。

持ちタイム
馬場×レース距離×馬齢ごとの最速のtoL3F_velの値とする
ただし、欠損値は前出のデータで埋めるとする。
つまり、4歳になって初めて出走した
芝のマイル距離レースの持ちタイムは、
3歳までに出走していた同条件のレースの
持ちタイムで埋め合わせる

8-4-1.持ちタイムの求め方

DataFrameのgroupbyを活用しつつ、shiftメソッドとrollingメソッドを使って求める。
horseId×field×distance×ageでgroupbyして、
最大過去100レースに対するtoL3F_velカラムの最大値を計算

欠損値をなるべく減らすために、馬齢が変わって初めて出走するレース条件では、
一つ前の馬齢での同条件の持ちタイムで埋め合わせる
それでも欠損が出る場合は、以下の優先度の特徴量ごとに
過去成績の最小値を出して埋める(悲観的評価を採用する)

  1. horseId×field×distance
  2. horseId×field×dist_cat
  3. horseId×field
In [3]:
targetCol = "toL3F_vel"

idf = df.copy()
idf = idf[~idf["horseId"].isin(
    idf[idf["horseId"].str[:4] < "1998"]["horseId"].unique())]
idf["mochiTime_org"] = idf.groupby(['horseId', "field", "distance", "age"])[
    targetCol].shift()
idf["mochiTime"] = idf.groupby(['horseId', "field", "distance", "age"])["mochiTime_org"].rolling(
    1000, min_periods=1).max().reset_index(level=[0, 1, 2, 3], drop=True)
idf["mochiTime_org"] = idf.groupby(['horseId', "field", "distance"])[
    targetCol].shift()
idf["mochiTime"].fillna(idf.groupby(['horseId', "field", "distance"])["mochiTime_org"].rolling(
    1000, min_periods=1).min().reset_index(level=[0, 1, 2], drop=True), inplace=True)
idf["mochiTime_org"] = idf.groupby(['horseId', "field", "dist_cat"])[
    targetCol].shift()
idf["mochiTime"].fillna(idf.groupby(['horseId', "field", "dist_cat"])["mochiTime_org"].rolling(
    1000, min_periods=1).min().reset_index(level=[0, 1, 2], drop=True), inplace=True)
idf["mochiTime_org"] = idf.groupby(['horseId', "field",])[
    targetCol].shift()
idf["mochiTime"].fillna(idf.groupby(['horseId', "field"])["mochiTime_org"].rolling(
    1000, min_periods=1).min().reset_index(level=[0, 1], drop=True), inplace=True)
In [4]:
targetCol = "last3F_vel"

idf = idf[~idf["horseId"].isin(
    idf[idf["horseId"].str[:4] < "1998"]["horseId"].unique())]
idf["mochiTime_org"] = idf.groupby(['horseId', "field", "distance", "age"])[
    targetCol].shift()
idf["mochiTime3F"] = idf.groupby(['horseId', "field", "distance", "age"])["mochiTime_org"].rolling(
    1000, min_periods=1).max().reset_index(level=[0, 1, 2, 3], drop=True)
idf["mochiTime_org"] = idf.groupby(['horseId', "field", "distance"])[
    targetCol].shift()
idf["mochiTime3F"].fillna(idf.groupby(['horseId', "field", "distance"])["mochiTime_org"].rolling(
    1000, min_periods=1).min().reset_index(level=[0, 1, 2], drop=True), inplace=True)
idf["mochiTime_org"] = idf.groupby(['horseId', "field", "dist_cat"])[
    targetCol].shift()
idf["mochiTime3F"].fillna(idf.groupby(['horseId', "field", "dist_cat"])["mochiTime_org"].rolling(
    1000, min_periods=1).min().reset_index(level=[0, 1, 2], drop=True), inplace=True)
idf["mochiTime_org"] = idf.groupby(['horseId', "field",])[
    targetCol].shift()
idf["mochiTime3F"].fillna(idf.groupby(['horseId', "field"])["mochiTime_org"].rolling(
    1000, min_periods=1).min().reset_index(level=[0, 1], drop=True), inplace=True)

8-4-2.持ちタイムと走破タイムの関連

出走前に算出した持ちタイムと走破タイムに関連があるかの確認

簡単に散布図でみる

In [5]:
idf2 = idf.groupby("raceId")[["velocity", "mochiTime"]].mean()
idf2.plot.scatter("velocity", "mochiTime")
plt.grid(ls=":")
plt.show()

当たり前かもだが、恐ろしく相関がある
つまり、持ちタイムを見れば走破タイムを予想可能である可能性が高い

8-4-3.持ちタイムと着順の関係

それなら、レースごとに持ちタイムが最も速い競走馬が1着になるのか?
ということなので、持ちタイムが最速の競走馬の着順分布と、
それ以外の競走馬の着順分布を確認する

出走数の違いもあると思うので、それぞれ割合でみる

In [6]:
idf["mochiRank"] = idf.groupby("raceId")["mochiTime"].rank(ascending=False)
idfdes = pd.concat(
    [
        idf[idf["mochiRank"] < 2]["label"].value_counts().rename("top1") /
        len(idf[idf["mochiRank"] < 2]),
        idf[idf["mochiRank"] >= 2]["label"].value_counts().rename(
            "other")/len(idf[idf["mochiRank"] >= 2]),
    ],
    axis=1
).T
display(idfdes)
idfdes.T.plot()
plt.grid(ls=":")
plt.show()
label 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
top1 0.103424 0.095936 0.085057 0.077845 0.072571 0.068903 0.065941 0.063436 0.060585 0.057124 0.052363 0.048183 0.044625 0.038507 0.031545 0.024472 0.005232 0.004166 0.000083
other 0.070553 0.071583 0.072375 0.072711 0.072522 0.072375 0.072003 0.070725 0.068994 0.066014 0.062204 0.057287 0.050714 0.044464 0.037245 0.026911 0.006482 0.004785 0.000053

もともと出走する競走馬の数もレースごとに違うので、10着以降の分布は割合の数字に意味はない。

結果を見るに、持ちタイム最速は、そうでないものとで1着になりやすい

もう少し細かく見たい
持ちタイムが1位, 2位, 3位, 4位, 5位とそれ以外でみる

In [7]:
idf["mochiRank"] = idf.groupby("raceId")["mochiTime"].rank(ascending=False)
idf["mochiRank"] = idf["mochiRank"].apply(
    lambda d: d if pd.isna(d) else int(d))

idfdes = pd.concat(
    [
        idf[idf["mochiRank"].isin([i])]["label"].value_counts().rename(f"top{i}") /
        len(idf[idf["mochiRank"].isin([i])])
        for i in range(1, 6)
    ] + [
        idf[idf["mochiRank"] >= 6]["label"].value_counts().rename(
            "other")/len(idf[idf["mochiRank"] >= 6])
    ],
    axis=1
).T
display(idfdes)
idfdes.T.plot()
plt.grid(ls=":")
plt.show()
label 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
top1 0.103424 0.095936 0.085057 0.077845 0.072571 0.068903 0.065941 0.063436 0.060585 0.057124 0.052363 0.048183 0.044625 0.038507 0.031545 0.024472 0.005232 0.004166 0.000083
top2 0.096955 0.094527 0.086480 0.084236 0.078067 0.073126 0.071065 0.065192 0.062891 0.056270 0.051484 0.045711 0.041617 0.035433 0.029053 0.019848 0.004701 0.003289 0.000056
top3 0.088213 0.089997 0.086029 0.083459 0.081661 0.078792 0.075223 0.068144 0.064432 0.058808 0.053027 0.046333 0.039039 0.033472 0.026906 0.019255 0.003982 0.003155 0.000071
top4 0.084357 0.084227 0.085364 0.081060 0.081852 0.077749 0.076713 0.071415 0.066636 0.062116 0.053723 0.047130 0.041444 0.033469 0.026847 0.018800 0.004031 0.002951 0.000115
top5 0.079614 0.081569 0.081131 0.080096 0.078754 0.079716 0.074612 0.074218 0.069070 0.062784 0.055828 0.050709 0.041287 0.035075 0.027826 0.020141 0.004229 0.003311 0.000029
other 0.061703 0.063128 0.065845 0.067697 0.068541 0.069770 0.070747 0.071264 0.070713 0.069200 0.066789 0.062467 0.055912 0.049783 0.042291 0.030808 0.007664 0.005632 0.000045

1着から2着あたりまで序列通りであるが、3着以降になると逆転している

心なしか曲線もtop5に行くにつれて、otherと同じ曲がり方(上に凸という)になる。
top1のみ下に凸な曲がり方をしている

8-4-4.持ちタイムと人気の関係

無論ここまで関係しているのであれば、
持ちタイムが最速であれば自ずと人気も集中しているだろうという推測がたつ

さっきと同じような分布を人気版として出す

In [8]:
idf["mochiRank"] = idf.groupby("raceId")["mochiTime"].rank(ascending=False)
idf["mochiRank"] = idf["mochiRank"].apply(
    lambda d: d if pd.isna(d) else int(d))

idfdes = pd.concat(
    [
        idf[idf["mochiRank"].isin([i])]["favorite"].value_counts().rename(f"top{i}") /
        len(idf[idf["mochiRank"].isin([i])])
        for i in range(1, 6)
    ] + [
        idf[idf["mochiRank"] >= 6]["favorite"].value_counts().rename(
            "other")/len(idf[idf["mochiRank"] >= 6])
    ],
    axis=1
).T
display(idfdes)
idfdes.T.plot()
plt.grid(ls=":")
plt.show()
favorite 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
top1 0.126055 0.102663 0.093846 0.084974 0.078385 0.071713 0.066896 0.064377 0.058868 0.053581 0.046826 0.041013 0.035559 0.027531 0.023365 0.017344 0.003959 0.003045
top2 0.112766 0.100329 0.093722 0.086297 0.081624 0.076090 0.070895 0.065884 0.059827 0.054576 0.046303 0.041066 0.034671 0.029504 0.023420 0.017138 0.003219 0.002668
top3 0.101987 0.092809 0.088056 0.086000 0.081204 0.079006 0.074167 0.069171 0.063904 0.055040 0.049702 0.042793 0.036684 0.030989 0.024879 0.017029 0.003854 0.002726
top4 0.090921 0.089064 0.086746 0.081305 0.081060 0.078311 0.075604 0.071703 0.065384 0.059870 0.052831 0.045806 0.039112 0.032145 0.024789 0.018296 0.003887 0.003167
top5 0.082065 0.084777 0.079556 0.081948 0.079439 0.077558 0.075502 0.073343 0.068049 0.061311 0.055988 0.048755 0.041754 0.034652 0.027812 0.020170 0.004186 0.003136
other 0.056539 0.059631 0.062192 0.063402 0.065622 0.067127 0.068434 0.069010 0.069587 0.069357 0.067886 0.064933 0.059584 0.054730 0.048191 0.038383 0.008444 0.006948

見立て通り持ちタイム最速の競走馬の1番人気が多い。
しかし、中には5番人気以降になっているデータも少なからずある。
これは欠損値の埋合せなどが影響しているか、別の原因でそう判断されているのか定かではない

要するに足の速い競走馬は当然ながら成績を残すし人気も集まりやすい
ただし、それ以外の不安要素が見つかるとその限りではない
という一般的な考え方に当てはまる結果であると考える。

8-4-5.ペース情報の分析¶

持ちタイムの速さとレースのペース情報の前半と後半のタイムから関係を見る

ペース情報に関するカラムは「rapTime」が該当する

In [9]:
idfpace = idf.drop_duplicates(
    "raceId", ignore_index=True)  # 処理しやすいように重複を削除しておく

idfpace[["raceId", "rapTime"]]
Out[9]:
raceId rapTime
0 200002010105 [12.3, 10.8, 12.0, 12.4, 12.8]
1 200002010106 [12.5, 11.1, 11.4, 11.6, 11.5]
2 200002010205 [12.4, 11.0, 11.4, 11.9, 12.3, 12.1]
3 200002010405 [12.5, 11.6, 11.6, 11.5, 12.2]
4 200002010406 [12.3, 10.3, 11.2, 12.4, 12.6, 12.3]
76137 202309050908 [12.3, 10.9, 12.0, 12.6, 12.3, 12.1, 12.9]
76138 202309050909 [12.7, 10.5, 13.3, 12.4, 12.5, 12.7, 12.6, 13….
76139 202309050910 [12.7, 11.3, 12.7, 12.3, 12.0, 11.6, 11.6, 11….
76140 202309050911 [12.8, 11.5, 12.8, 12.1, 12.5, 13.2, 12.2, 12….
76141 202309050912 [12.3, 11.1, 11.3, 11.2, 11.4, 11.5]

76142 rows × 2 columns

これは200mごとのラップタイムになっているため、上り3Fを除いた前半と後半の200mの平均ラップタイムを割り出すとレースのペースが見えてくるのではと考える
レース距離が奇数の場合は、最初の100mのラップタイムを取るので注意。(1150mのレースもあるので注意)

In [10]:
idfpace["rapTime2"] = idfpace[["rapTime", "distance"]].apply(
    lambda row: row["rapTime"] if row["distance"] % 200 == 0
    else [round(row["rapTime"][0]*200/(row["distance"] % 200), 1)] + row["rapTime"][1:], axis=1)
idfpace[idfpace["distance"].isin([1150])][["rapTime", "rapTime2"]]
Out[10]:
rapTime rapTime2
12334 [9.4, 11.0, 11.1, 12.3, 12.5, 13.1] [12.5, 11.0, 11.1, 12.3, 12.5, 13.1]
12337 [9.3, 10.7, 11.1, 12.0, 12.7, 13.0] [12.4, 10.7, 11.1, 12.0, 12.7, 13.0]
12368 [9.5, 10.7, 11.3, 12.2, 12.5, 13.7] [12.7, 10.7, 11.3, 12.2, 12.5, 13.7]
12372 [9.1, 10.8, 11.0, 11.9, 12.7, 13.5] [12.1, 10.8, 11.0, 11.9, 12.7, 13.5]
12405 [9.3, 10.8, 11.4, 12.0, 12.8, 13.2] [12.4, 10.8, 11.4, 12.0, 12.8, 13.2]
75722 [9.6, 10.7, 10.8, 11.8, 12.2, 13.0] [12.8, 10.7, 10.8, 11.8, 12.2, 13.0]
75747 [9.5, 10.8, 11.3, 11.7, 11.9, 12.4] [12.7, 10.8, 11.3, 11.7, 11.9, 12.4]
75754 [9.5, 10.2, 10.9, 11.8, 12.3, 12.7] [12.7, 10.2, 10.9, 11.8, 12.3, 12.7]
75781 [9.8, 10.8, 11.3, 12.2, 12.0, 12.7] [13.1, 10.8, 11.3, 12.2, 12.0, 12.7]
75786 [9.5, 10.5, 11.0, 11.9, 12.4, 13.0] [12.7, 10.5, 11.0, 11.9, 12.4, 13.0]

641 rows × 2 columns

In [11]:
idfpace["prePace"] = idfpace["rapTime2"].apply(
    lambda lst: np.mean(lst[:(len(lst)-3)//2]))
idfpace["pastPace"] = idfpace["rapTime2"].apply(
    lambda lst: np.mean(lst[(len(lst)-3)//2:-3]))
idfpace["prePace3F"] = idfpace["rapTime2"].apply(
    lambda lst: np.mean(lst[:-3]))
idfpace["pastPace3F"] = idfpace["rapTime2"].apply(
    lambda lst: np.mean(lst[-3:]))
idfpace[["raceId", "rapTime2", "prePace", "pastPace"]]
Out[11]:
raceId rapTime2 prePace pastPace
0 200002010105 [12.3, 10.8, 12.0, 12.4, 12.8] 12.300000 10.800000
1 200002010106 [12.5, 11.1, 11.4, 11.6, 11.5] 12.500000 11.100000
2 200002010205 [12.4, 11.0, 11.4, 11.9, 12.3, 12.1] 12.400000 11.200000
3 200002010405 [12.5, 11.6, 11.6, 11.5, 12.2] 12.500000 11.600000
4 200002010406 [12.3, 10.3, 11.2, 12.4, 12.6, 12.3] 12.300000 10.750000
76137 202309050908 [12.3, 10.9, 12.0, 12.6, 12.3, 12.1, 12.9] 11.600000 12.300000
76138 202309050909 [12.7, 10.5, 13.3, 12.4, 12.5, 12.7, 12.6, 13…. 12.166667 12.533333
76139 202309050910 [12.7, 11.3, 12.7, 12.3, 12.0, 11.6, 11.6, 11…. 12.233333 11.875000
76140 202309050911 [12.8, 11.5, 12.8, 12.1, 12.5, 13.2, 12.2, 12…. 12.366667 12.600000
76141 202309050912 [12.3, 11.1, 11.3, 11.2, 11.4, 11.5] 12.300000 11.200000

76142 rows × 4 columns

In [12]:
xcol = "prePace3F"
ycol = "pastPace3F"

plt.figure(figsize=(20, 8))
for n, f in enumerate(["芝", "ダ"], start=0):
    for m, d in enumerate("SMILE", start=1):
        idfp = idfpace[idfpace["field"].isin(
            [f]) & idfpace["dist_cat"].isin([d]) & ~idfpace["raceId"].str[:4].isin(["2024", "2023", "2022"])]
        if len(idfp) == 0:
            continue
        # region plot
        minV = min(idfp[ycol].min(), idfp[xcol].min())
        plt.subplot(2, 5, int(5*n+m))
        sns.scatterplot(
            idfp,
            x=xcol,
            y=ycol,
            label=f"{f}-{d}",
            edgecolors="black", facecolors="none"
        )
        plt.hlines(
            idfp[ycol].median(),
            xmin=minV-0.25,
            xmax=idfp[xcol].max()+0.25,
            colors="red",
            linestyles=":"
        )
        plt.xlim(
            left=minV-0.25,
            right=idfp[xcol].max()+0.25
        )
        plt.vlines(
            idfp[xcol].median(),
            ymin=minV-0.25,
            ymax=idfp[ycol].max()+0.25,
            colors="red",
            linestyles=":"
        )
        plt.ylim(
            bottom=minV-0.25,
            top=idfp[ycol].max()+0.25
        )
        plt.grid(ls=":")
        # endregion
plt.tight_layout()
plt.show()
In [13]:
# 出力結果は省略する
if 0:
    for d in ['函館', '福島', '新潟', '東京', '中山', '中京', '京都', '阪神', '小倉', '札幌', ]:
        print("競馬場:", d)
        plt.figure(figsize=(15, 6))
        for n, f in enumerate(["芝", "ダ"], start=0):
            for m, dist in enumerate("SMILE", start=1):
                idfp = idfpace[idfpace["field"].isin(
                    [f]) & idfpace["place"].isin([d]) & ~idfpace["raceId"].str[:4].isin(
                        ["2024", "2023", "2022"]) & idfpace["dist_cat"].isin([dist])
                ]
                if len(idfp) == 0:
                    continue
                # region plot
                minV = min(idfp[ycol].min(), idfp[xcol].min())
                plt.subplot(2, 5, int(5*n+m))
                sns.scatterplot(
                    idfp,
                    x=xcol,
                    y=ycol,
                    label=f"{f}-{dist}",
                    edgecolors="black", facecolors="none"
                )
                plt.hlines(
                    idfp[ycol].median(),
                    xmin=minV-0.25,
                    xmax=idfp[xcol].max()+0.25,
                    colors="red",
                    linestyles=":"
                )
                plt.xlim(
                    left=minV-0.25,
                    right=idfp[xcol].max()+0.25
                )
                plt.vlines(
                    idfp[xcol].median(),
                    ymin=minV-0.25,
                    ymax=idfp[ycol].max()+0.25,
                    colors="red",
                    linestyles=":"
                )
                plt.ylim(
                    bottom=minV-0.25,
                    top=idfp[ycol].max()+0.25
                )
                plt.grid(ls=":")
                plt.legend(loc="best")
                # endregion
        plt.tight_layout()
        plt.show()

8-4-6.持ちタイムとペース情報の関係

レースごとに出走馬のlast3F_vel, toL3F_vel, velocity, mochiTimeの平均値を計算
上記の平均値の情報とレースごとのペース情報の相関を確認する。

In [14]:
idfvel = idf.groupby("raceId")[
    ["last3F_vel", "toL3F_vel", "velocity", "mochiTime", "mochiTime3F"]].mean().rename(columns=lambda x: f"{x}_mean").reset_index()
idfvel = pd.merge(idfpace[[
                  "raceId", "prePace", "prePace3F", "pastPace", "pastPace3F",]], idfvel, on="raceId")

idfvel.corr().loc[["mochiTime_mean", "mochiTime3F_mean"]]
Out[14]:
raceId prePace prePace3F pastPace pastPace3F last3F_vel_mean toL3F_vel_mean velocity_mean mochiTime_mean mochiTime3F_mean
mochiTime_mean -0.047494 -0.291776 -0.840335 -0.837238 -0.417676 0.465754 0.839774 0.769325 1.000000 0.493266
mochiTime3F_mean 0.123575 -0.315756 -0.404680 -0.364354 -0.807430 0.838541 0.446989 0.760387 0.493266 1.000000

結果から出走馬の持ちタイムの平均値であるmochiTime_meanに対してpastPace3F, pastPace, toL3F_vel_meanで0.8の相関がある。
つまり、出走馬の持ちタイム(速度指標)が速くなると後半のペース情報であるpastPaceが早くなり、出走馬の上り3Fに至るまでの速度の平均であるtoL3F_vel_meanが速くなることを意味する。

先では出走馬の平均情報とペース情報の関係をみた。
つぎは、各馬の持ちタイムと前走でのvelocity, last3F_vel, toL3F_velとレース結果のvelocity, last3F_vel, toL3F_velとの相関を確認する。

In [15]:
idf["vel_lag1"] = idf.groupby("horseId")["velocity"].shift()
idf["l3Fvel_lag1"] = idf.groupby("horseId")["last3F_vel"].shift()
idf["t3Fvel_lag1"] = idf.groupby("horseId")["toL3F_vel"].shift()
idf[["velocity", "last3F_vel", "toL3F_vel", "vel_lag1",
     "l3Fvel_lag1", "t3Fvel_lag1", "mochiTime", "mochiTime3F"]].corr().loc[
    ["vel_lag1", "l3Fvel_lag1", "t3Fvel_lag1", "mochiTime", "mochiTime3F"]][["velocity", "last3F_vel", "toL3F_vel"]]
Out[15]:
velocity last3F_vel toL3F_vel
vel_lag1 0.615464 0.482257 0.529577
l3Fvel_lag1 0.470675 0.518547 0.252701
t3Fvel_lag1 0.525013 0.268416 0.613931
mochiTime 0.596624 0.323186 0.670378
mochiTime3F 0.615503 0.660305 0.341653

持ちタイムでみると上り3F到達までの速度であるtoL3F_velと0.67の相関がある。
これは前走の同指標であるt3Fvel_lag1(前走のtoL3F_vel)toL3F_velの相関0.61よりも高い。

前走のtoL3F_velの情報よりも、持ちタイムの方が上り3Fに到達するまでの情報と関係がありそうということである。

In [16]:
idfp = pd.merge(
    idf[["raceId",]+list(set(idf.columns) - set(idfvel.columns))],
    idfvel[["raceId", "last3F_vel_mean", "toL3F_vel_mean",
            "mochiTime_mean", "mochiTime3F_mean"]],
    on="raceId"
)
idfp
Out[16]:
raceId inoutside label_2C rough_horse rapSumTime_vel t3Fvel_lag1 distance jweight toL3F_vel b2StallionName dist_cat jockeyId_en sStallionId horseName sex sStallionName last3F_vel_mean toL3F_vel_mean mochiTime_mean mochiTime3F_mean
0 200002010105 7 0 [975.6097560975609, 1038.9610389610389, 1025.6… NaN 1000 53.0 952.380952 インターメゾ S 74 000a0012bf サニーサマリン Halo 950.671834 973.029890 NaN NaN
1 200002010105 2 0 [975.6097560975609, 1038.9610389610389, 1025.6… NaN 1000 53.0 1030.042918 Danzig S 101 000a000dfe タシロスプリング Nijinsky 950.671834 973.029890 NaN NaN
2 200002010105 11 0 [975.6097560975609, 1038.9610389610389, 1025.6… NaN 1000 53.0 909.090909 フロリバンダ S 65 000a001b4d プラントラッキー Rainbow Quest 950.671834 973.029890 NaN NaN
3 200002010105 4 0 [975.6097560975609, 1038.9610389610389, 1025.6… NaN 1000 53.0 1008.403361 Seattle Slew S 4 000a00033a マイネルボルテクス サンデーサイレンス 950.671834 973.029890 NaN NaN
4 200002010105 11 0 [975.6097560975609, 1038.9610389610389, 1025.6… NaN 1000 52.0 905.660377 ティエポロ S 25 000a001205 グッドマイチョイス Lyphard 950.671834 973.029890 NaN NaN
1054022 202309050912 14 0 [975.6097560975609, 1025.6410256410256, 1037.4… 1005.025126 1200 58.0 1011.235955 Cape Cross S 12 000a002328 スーサンアッシャー Pivotal 1055.689283 1022.114517 1052.631703 1055.708732
1054023 202309050912 8 0 [975.6097560975609, 1025.6410256410256, 1037.4… 1057.268722 1200 56.0 1025.641026 Starborough S 827 2002100816 アネゴハダ ディープインパクト 1055.689283 1022.114517 1052.631703 1055.708732
1054024 202309050912 15 0 [975.6097560975609, 1025.6410256410256, 1037.4… 1061.946903 1200 58.0 1000.000000 マラキム S 675 2001103460 テイエムイダテン キングカメハメハ 1055.689283 1022.114517 1052.631703 1055.708732
1054025 202309050912 11 0 [975.6097560975609, 1025.6410256410256, 1037.4… 1021.276596 1200 56.0 1019.830028 Kingmambo S 657 2002100816 ハギノメーテル ディープインパクト 1055.689283 1022.114517 1052.631703 1055.708732
1054026 202309050912 4 0 [975.6097560975609, 1025.6410256410256, 1037.4… 1012.658228 1200 58.0 1031.518625 Machiavellian S 573 000a002270 クムシラコ Forestry 1055.689283 1022.114517 1052.631703 1055.708732

1054027 rows × 93 columns

次にレースごとの持ちタイムの平均値と上り3F持ちタイムの平均値とレース結果のlast3F_veltoL3F_velの平均値を各指標から引いたmochiTime_diff, mochiTime3F_diff, last3F_diff, toL3F_diffの相関係数を見る。

この平均値を引く操作は、レースごとに競走馬の情報から共通する情報(平均値)を抜き出すことで純粋な各競走馬のみの情報だけにする効果がある。
こうすることで、持ちタイムが他出走馬よりも高い場合にlast3F_diff, toL3F_diffが同じ関係にあるのかどうかを確認することができる。

In [17]:
idfp["mochiTime_diff"] = idfp["mochiTime"] - idfp["mochiTime_mean"]
idfp["mochiTime3F_diff"] = idfp["mochiTime3F"] - idfp["mochiTime3F_mean"]
idfp["last3F_diff"] = idfp["last3F_vel"] - idfp["last3F_vel_mean"]
idfp["toL3F_diff"] = idfp["toL3F_vel"] - idfp["toL3F_vel_mean"]

idfp[["last3F_diff", "toL3F_diff", "mochiTime_diff", "mochiTime3F_diff"]].corr(
).loc[["mochiTime_diff", "mochiTime3F_diff"]][["last3F_diff", "toL3F_diff"]]
Out[17]:
last3F_diff toL3F_diff
mochiTime_diff -0.027427 0.231037
mochiTime3F_diff 0.245777 0.019769

結果からmochiTime_difftoL3F_diffに対して相関が0.23とやや関係があるが、last3F_diffとはほとんど関係がないことが分かった。(上り3Fについても同様)
つまりこれはmochiTimeが他競走馬よりも高くとも最後の上り3Fでぶっちぎれるとは限らないが、上り3Fに達するまでの間はやや優勢であることを意味する。

スポンサーリンク

8-5.持ちタイムと脚質の関係¶

レース当日の出走馬の脚質(=戦法)は、出走馬同士の持ちタイムとこれまでの脚質の趣向を見て騎手が判断するものと考えられる。
ここでの目標は、持ちタイムと過去の脚質から当日の脚質の予測精度の改善を目指す。

8-5-0.まずは脚質の分類を行う¶

流れとしては、クラスタ用の特徴量を準備して前回のクラスタ結果をもとにKmeansモデルを作ってクラスタリングする。

まずはクラスタ用の特徴量の作成とKMeansモデルの初期化

In [18]:
from sklearn.cluster import KMeans  # KMeans法のモジュールをインポート

for col in ["label_1C", "label_lastC"]:
    idfp[f"{col}_rate"] = (idfp[col].astype(
        int)/idfp["horseNum"]).convert_dtypes()

# クラスタ数
n_cls = 4
rate = 2.5
# クラスタリングする特徴量を選定
cluster_columns = ["label_1C_rate", "label_lastC_rate"]
cluster_columns2 = ["label_1C_rate", "label_lastC_rate2"]
kmeans = KMeans(n_clusters=n_cls)  # 脚質が4種類なので、クラス数を4とする
idfp["label_lastC_rate2"] = rate*idfp["label_lastC_rate"]

# 後でクラスタ中心を振り直すが、形式上一旦fitしておかないといけない。
kmeans.fit(idfp[cluster_columns2].iloc[:n_cls*2])
Out[18]:
KMeans(n_clusters=4)

In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook.
On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.

KMeans(n_clusters=4)

以下でクラスタ中心をセットして、クラスタリングを実行
※非推奨なやり方かもしれないので、気になる場合は前回の「0007_past_results_analyze.ipynb」で行っていた順序でクラスタするとよい

In [19]:
# クラスタ中心をセット
centers = [
    [0.189736, 0.393819],
    [0.432436, 0.995918],
    [0.639462, 1.612348],
    [0.836256, 2.227643]
]
kmeans.cluster_centers_ = np.array(centers)
# 脚質の分類
idfp["cluster"] = kmeans.predict(idfp[cluster_columns2])

# 名前も付けておく
clsnames = ["逃げ", "先行", "差し", "追込"]
cls_map = {i: d for i, d in enumerate(clsnames)}
idfp["clsName"] = idfp["cluster"].map(cls_map)
idfp["clsName"].value_counts().to_frame().T
Out[19]:
clsName 逃げ 先行 差し 追込
count 300844 265800 254865 232518

このようにクラスタ中心さえ分かっていれば、KMeansのモデルの保存を行わずともモデルの読み込みができるので再現性を取ることができます。

8-5-1.脚質と持ちタイムの分析¶

脚質ごとに持ちタイムやレース出走速度の指標についての統計情報を確認する

In [20]:
corrList = ["mochiTime", "mochiTime3F", "mochiTime_diff",
            "mochiTime3F_diff", "last3F_diff", "toL3F_diff"]

for col in corrList:

    display(idfp.groupby("clsName")[[col]].describe().loc[clsnames])
mochiTime
count mean std min 25% 50% 75% max
clsName
逃げ 258848.0 985.994176 42.742734 572.792363 953.895072 981.996727 1016.949153 1132.075472
先行 227190.0 979.656237 40.427186 572.792363 949.868074 975.609756 1006.289308 1121.495327
差し 212951.0 976.014740 39.200328 774.193548 947.368421 972.972973 1002.386635 1121.495327
追込 181824.0 972.516510 38.608441 589.812332 944.881890 969.696970 997.920998 1118.012422
mochiTime3F
count mean std min 25% 50% 75% max
clsName
逃げ 258848.0 968.646572 53.006114 360.721443 932.642487 970.350404 1008.403361 1135.646688
先行 227190.0 970.887891 54.519333 532.544379 932.642487 972.972973 1011.235955 1146.496815
差し 212951.0 968.857080 56.268231 468.140442 930.232558 970.350404 1011.235955 1146.496815
追込 181824.0 966.219294 58.071345 448.877805 925.449871 967.741935 1008.403361 1142.857143
mochiTime_diff
count mean std min 25% 50% 75% max
clsName
逃げ 258848.0 6.116719 25.505291 -403.612475 -8.619463 7.654126 22.685072 116.473409
先行 227190.0 0.921728 24.562204 -388.444044 -12.802349 2.675792 16.706535 120.296286
差し 212951.0 -2.592773 24.677267 -199.250758 -16.370053 -0.718430 13.190352 113.370755
追込 181824.0 -6.822940 26.355921 -340.412237 -21.261141 -4.514596 10.019962 119.134579
mochiTime3F_diff
count mean std min 25% 50% 75% max
clsName
逃げ 258848.0 -0.413216 33.334974 -570.189121 -18.515103 3.198524 21.683411 149.818980
先行 227190.0 2.284065 33.114682 -368.165271 -15.850526 5.795106 24.446752 123.217747
差し 212951.0 0.905818 33.907771 -424.570076 -18.215112 4.471305 23.843222 131.324161
追込 181824.0 -3.326576 35.610113 -436.935491 -23.635281 0.332627 21.042714 124.822509
last3F_diff
count mean std min 25% 50% 75% max
clsName
逃げ 300844.0 -3.459033 24.051626 -291.193203 -16.573898 -0.966954 12.174729 129.230234
先行 265800.0 0.990127 22.672459 -339.673801 -10.933835 3.365449 15.628151 104.567541
差し 254865.0 3.009088 23.288424 -429.629501 -8.631121 5.714818 17.979008 106.645535
追込 232518.0 0.045341 29.906649 -571.858817 -12.386388 5.011666 18.668934 97.234467
toL3F_diff
count mean std min 25% 50% 75% max
clsName
逃げ 300844.0 11.348336 7.910389 -171.761417 5.710107 9.136467 15.032597 77.336374
先行 265800.0 3.160156 4.649946 -392.698760 0.452952 2.297917 4.967976 60.149903
差し 254865.0 -3.943559 4.930196 -217.817985 -5.990630 -3.144008 -1.109787 55.785473
追込 232518.0 -13.972995 12.429845 -531.175291 -17.764559 -10.547069 -6.413758 47.167105

ちょっと判断が難しいので、多群間の差を検定するクラスカルウォリス検定を使う

In [21]:
from scipy.stats import kruskal

for col in corrList:
    datalist = []
    for clsn in clsnames:
        datalist += [idfp[idfp["clsName"].isin([clsn])][col].dropna().values]
    print(f"対象: {col},\t\t", "検定結果:", kruskal(*datalist))
対象: mochiTime,		 検定結果: KruskalResult(statistic=11227.881964058324, pvalue=0.0)
対象: mochiTime3F,		 検定結果: KruskalResult(statistic=622.6613674719162, pvalue=1.2318670443068229e-134)
対象: mochiTime_diff,		 検定結果: KruskalResult(statistic=31447.23819973341, pvalue=0.0)
対象: mochiTime3F_diff,		 検定結果: KruskalResult(statistic=2925.9130552456795, pvalue=0.0)
対象: last3F_diff,		 検定結果: KruskalResult(statistic=14318.584832817241, pvalue=0.0)
対象: toL3F_diff,		 検定結果: KruskalResult(statistic=823589.7516604401, pvalue=0.0)

一応多群での有意差は認められたが、この検定の欠点はどの群とどの群、つまりどの脚質とどの脚質で差があるのかを教えてはくれない。
この場合多重検定を行う必要があり、2群の全組合せの検定(マンホイットニーのU検定)をした後にボンフェローニ補正による調整が必要になる。

In [22]:
import statsmodels.stats.multitest as smm
from itertools import combinations
from scipy.stats import mannwhitneyu

for col in corrList:
    datalist = []
    for cols in combinations(clsnames, 2):
        x = idfp[idfp["clsName"].isin([cols[0]])][col].dropna().values
        y = idfp[idfp["clsName"].isin([cols[1]])][col].dropna().values
        datalist += [mannwhitneyu(x, y, alternative="greater").pvalue]
    corrected_p_values = smm.multipletests(
        datalist, alpha=0.05, method='bonferroni')
    print(
        f"対象: {col},\t",
        f"有意か?: {corrected_p_values[0].all()},\t",
        f"p値: {corrected_p_values[1].round(6)}"
    )
対象: mochiTime,	 有意か?: True,	 p値: [0. 0. 0. 0. 0. 0.]
対象: mochiTime3F,	 有意か?: False,	 p値: [1. 1. 0. 0. 0. 0.]
対象: mochiTime_diff,	 有意か?: True,	 p値: [0. 0. 0. 0. 0. 0.]
対象: mochiTime3F_diff,	 有意か?: False,	 p値: [1. 1. 0. 0. 0. 0.]
対象: last3F_diff,	 有意か?: False,	 p値: [1. 1. 1. 1. 1. 0.]
対象: toL3F_diff,	 有意か?: True,	 p値: [0. 0. 0. 0. 0. 0.]

結果からすべての2群の組合せに対してmochiTime_difftoL3F_diffで有意差があり、p値も十分に小さいことが分かった。

とりわけ過去データである持ちタイムの情報(mochiTime_diff)が、レース結果から分かる脚質においてグループ間で差異があるという結果は非常にありがたいことである。

8-5-2.持ちタイムと前走の脚質の分析¶

とりあえずペース情報の追加

In [23]:
idf = pd.merge(idfp, idfpace[["raceId", "prePace",
               "pastPace", "prePace3F", "pastPace3F"]], on="raceId")

前回の話で脚質をランク学習で分類する方法を紹介した。
その分類するモデルについて、特徴量重要度を確認したところ最も重要だと判断されていたのが前走の脚質情報だった。

前回の脚質分類モデルの特徴量重要度

0
一部抜粋 ・・・
last3F_vel_lag1 2268.75
clsLabel_lag7 2355.62
toL3F_vel_lag1 3380.81
distance 3479.48
clsLabel_lag6 4548.93
raceGrade 6348.59
clsLabel_lag5 6371.05
bStallionId 9171.27
b2StallionId 10189.09
clsLabel_lag4 13089.48
stallionId 16163.15
clsLabel_lag3 21275.96
clsLabel_lag2 59780.57
breedId 66164.67
clsLabel_lag1 187454.07

脚質を分類するついでに持ちタイムと前回の脚質情報等からレース展開であるペース情報の推測に役立てられないかを考えたい

まずは前5走分の脚質情報を作る。

In [24]:
for lag in range(1, 6):
    idf[f"clsName_lag{lag}"] = idf.sort_values("raceDate").groupby("horseId")[
        "clsName"].shift(lag)

ついでに前走の脚質情報と持ちタイム等の有意性をみる

In [25]:
for col in corrList+["toL3F_vel"]:
    datalist = []
    for cols in combinations(clsnames, 2):
        x = idf[idf["clsName_lag1"].isin([cols[0]])][col].dropna().values
        y = idf[idf["clsName_lag1"].isin([cols[1]])][col].dropna().values
        datalist += [mannwhitneyu(x, y, alternative="greater").pvalue]
    corrected_p_values = smm.multipletests(
        datalist, alpha=0.05, method='bonferroni')
    print(
        f"対象: {col},\t",
        f"有意か?: {corrected_p_values[0].all()},\t",
        f"p値: {corrected_p_values[1].round(6)}"
    )
対象: mochiTime,	 有意か?: True,	 p値: [0. 0. 0. 0. 0. 0.]
対象: mochiTime3F,	 有意か?: False,	 p値: [1.e+00 1.e+00 1.e+00 5.e-06 0.e+00 0.e+00]
対象: mochiTime_diff,	 有意か?: True,	 p値: [0. 0. 0. 0. 0. 0.]
対象: mochiTime3F_diff,	 有意か?: False,	 p値: [1. 1. 1. 1. 0. 0.]
対象: last3F_diff,	 有意か?: False,	 p値: [1. 1. 1. 1. 0. 0.]
対象: toL3F_diff,	 有意か?: True,	 p値: [0. 0. 0. 0. 0. 0.]
対象: toL3F_vel,	 有意か?: True,	 p値: [0. 0. 0. 0. 0. 0.]

ということで、前走の脚質情報でもmochiTimemochiTime_diffの指標で順序関係の有意差があると考える。
つまり、前走が逃げと先行の2馬だと統計的にmochiTime逃げ>先行の関係であるということである。

更には、toL3F_difftoL3F_velで有意差があることから前走の脚質情報によって上り3Fに至るまでの速度が違うことが分かる。
ペース情報は決まった距離ごとのラップタイムの平均で算出することから、これは前走の脚質情報によってペース情報を知ることが出来得ると考える。

In [26]:
idf.groupby("clsName_lag1")[
    ["mochiTime", "mochiTime_diff"]].mean().loc[clsnames]
Out[26]:
mochiTime mochiTime_diff
clsName_lag1
逃げ 988.398772 5.241469
先行 980.234422 1.320295
差し 974.657872 -2.193461
追込 969.065237 -7.123053

実際に平均でみてもそうであると分かる。

これを手掛かりに脚質×持ちタイムを使ってペース情報の推測が出来ないか探る。

8-5-3.持ちタイム×前5走の脚質とペース情報の分析¶

改めてペース情報の確認

  • prePace: 残り600mを除いたスタートからの前半の200mペース情報
  • pastPace: 残り600mを除いたprePaceからの後半の200mペース情報
  • prePace3F: 残り600mまでの200mペース情報
  • pastPace3F: 残り600mの200mペース情報

目標としては前5走の脚質情報と持ちタイムからペース情報の割り出しを目指したい。
なぜなら、ペース情報を知ることで出走馬がそのペースについていけるかどうかを判断できると競馬予想に大きく役立てられると考えるからである。

まずは、先ほど前5走データの脚質情報を追加したので、レースごとの出走馬の前5走データから脚質情報を集約して各脚質の割合を算出する。
このようにすることで、出走馬の前5走の脚質の平均的な分布を知ることができると考えた。
つまり、出走馬の前5走のほとんどが逃げの脚質だと、全体の逃げの割合が増えるためレースのペースも上がるのではと考える。

In [27]:
dflist = {}
lagcolumns = ["clsName_lag1", "clsName_lag2",
              "clsName_lag3", "clsName_lag4", "clsName_lag5"]
for g, dfg in tqdm.tqdm(idf[["raceId",] + lagcolumns].groupby("raceId")):
    dflist[g] = (pd.Series(dfg[lagcolumns].values.reshape(-1)
                           ).value_counts() / dfg[lagcolumns].notna().sum().sum()).to_dict()
100%|██████████| 76142/76142 [03:06<00:00, 408.12it/s]
In [28]:
dfcls = pd.DataFrame.from_dict(dflist, orient="index")
idfp = pd.merge(idf, dfcls.reset_index(names="raceId"), on="raceId")
In [29]:
idfp3 = idfp.set_index(["field", "distance"])
idfp3["prePace3F_diff"] = idfp[["field", "distance",
                                "prePace3F"]].groupby(["field", "distance"])["prePace3F"].mean()
idfp["prePace3F_diff"] = idfp["prePace3F"] - \
    idfp3["prePace3F_diff"].reset_index(drop=True)

レースごとに脚質の割合から各タイム関連情報との相関を見る

In [30]:
idfp[["prePace", "pastPace", "prePace3F", "pastPace3F", "raceGrade", "prePace3F_diff", "toL3F_vel_mean"] +
     clsnames].corr().loc[["prePace", "pastPace", "prePace3F", "pastPace3F", "raceGrade", "prePace3F_diff", "toL3F_vel_mean"]][clsnames]
Out[30]:
逃げ 先行 差し 追込
prePace -0.152397 -0.003612 0.104303 0.134587
pastPace -0.442995 0.038607 0.295807 0.294105
prePace3F -0.438341 0.032476 0.293195 0.301456
pastPace3F -0.103249 -0.023433 0.080964 0.087497
raceGrade 0.252858 0.022263 -0.175212 -0.219931
prePace3F_diff -0.247496 0.016687 0.168746 0.208899
toL3F_vel_mean 0.442823 -0.021774 -0.295788 -0.318930

結果から特徴的なのは、先行の脚質の割合が高くとも低くともペース情報には関係がない結果になっている
他の脚質ではprePace3FtoL3F_vel_meanなど上り3Fに至るまでのタイム関連情報に対して相関が0.3から0.4程度とやや関係があるように見える。

実際にprePace3F逃げ追込の脚質の割合との関係をグラフで見てみる

In [31]:
for cn in clsnames[0::3]:
    xcol = cn
    ycol = "prePace3F"
    plt.figure(figsize=(15, 4))
    minVx = min(idfp[xcol].min(), idfp[xcol].min()) - 0.1
    minVy = min(idfp[ycol].min(), idfp[ycol].min())*0.99
    maxVx = max(idfp[xcol].max(), idfp[xcol].max()) + 0.1
    maxVy = max(idfp[ycol].max(), idfp[ycol].max())*1.01
    for n, f in enumerate(["芝", "ダ"], start=0):
        for m, dist in enumerate("SMILE", start=1):
            idfp2 = idfp[
                idfp["field"].isin([f]) &
                ~idfp["raceId"].str[:4].isin(["2024", "2023", "2022"]) &
                idfp["dist_cat"].isin([dist])
            ].drop_duplicates([xcol, ycol]).sort_values("clsName", key=lambda data: [{v: -k for k, v in cls_map.items()}[x] for x in data])  # .groupby("clsName")[[xcol, ycol]].mean()
            if len(idfp2) == 0:
                continue

            linear = np.polyfit(idfp2[idfp2[xcol].notna()][xcol].tolist(),
                                idfp2[idfp2[xcol].notna()][ycol].tolist(), 1)
            linear_fn = np.poly1d(linear)
            # region plot
            plt.subplot(2, 5, int(5*n+m))
            sns.scatterplot(
                idfp2,
                x=xcol,
                y=ycol,
                label=f"{f}-{dist}",
                # hue="clsName",
                edgecolors="black", facecolors="none",
                # alpha=0.5
            )
            plt.plot(np.arange(0, 1.05, 0.05), linear_fn(
                np.arange(0, 1.05, 0.05)), color="green")
            plt.hlines(
                idfp2[ycol].median(),
                xmin=minVx,
                xmax=maxVx,
                colors="red",
                linestyles=":"
            )
            plt.xlim(
                left=minVx,
                right=maxVx
            )
            plt.vlines(
                idfp2[xcol].median(),
                ymin=minVy,
                ymax=maxVy,
                colors="red",
                linestyles=":"
            )
            plt.ylim(
                bottom=minVy,
                top=maxVy
            )
            plt.grid(ls=":")
            plt.legend(loc="best")
            # endregion
    plt.tight_layout()
    plt.show()

1つ目のグラフは横軸に出走馬の逃げの脚質の割合と縦軸にレース結果であるprePace3Fとしたグラフで、2つ目のグラフは横軸が追込となったものである。
赤線は各軸に対する中央値で、緑線は線形フィットしたものである

グラフの形を見ればわかるように、逃げの割合のグラフでは右肩下がりに追込の場合は右肩上がりとなっている。
つまり、逃げの割合が高くなると上がり3Fに至るまでのタイムが早くなり、追込みの割合が高くなると遅くなるということである。

これは当初に挙げたペース情報の一般的な解釈(8-2節)と一致することがわかる。

スポンサーリンク

8-6.持ちタイム×前走の脚質を使ってペース情報の推定

先の分析で前走データの脚質の割合からペース情報の推定ができそうである。

よってLightGBMを使った回帰モデルでペース情報の推定を試みる。

8-6-1.LightGBMのモデル作成案¶

  • モデル:回帰モデル
  • 目的変数: 残り600m地点に到達するまでの200m単位の出走タイムの推定
  • 説明変数: 前走情報の脚質割合とmochiTimeの平均値、そのほかレース情報のカテゴリ
  • 学習期間: 2014年~2019年
  • 検証期間: 2020年
  • テスト期間: 2021年

8-6-2.特徴量作成¶

In [32]:
feature_columns = clsnames + \
    [
        "field", "place", "dist_cat", "distance",
        "condition", "raceGrade", "horseNum", "direction",
        "inoutside", 'mochiTime_mean', 'mochiTime3F_mean',
        'weather', 'mochiTime_mean_div', 'mochiTime3F_mean_div',
        "mochiTime_diff", "mochiTime", "mochiTime3F",
        "mochiTime_div", "mochiTime3F_div", "horseId", "breedId",
        "bStallionId", "b2StallionId", "stallionId"
    ]+lagcolumns
label_column = "toL3F"  # 新しく残り600m地点到達までの200m単位走破タイム
In [33]:
cat_list = [
    "field", "place", "dist_cat", 'weather',
    "condition", "direction", "inoutside", "horseId",
    "breedId", "bStallionId", "b2StallionId", "stallionId"
]+lagcolumns
for cat in cat_list:
    idfp[cat] = idfp[cat].astype("category")
In [34]:
idfp["mochiTime3F_mean_div"] = 200*60/idfp["mochiTime3F_mean"]
idfp["mochiTime_mean_div"] = 200*60/idfp["mochiTime_mean"]
idfp["mochiTime_div"] = 200*60/idfp["mochiTime"]
idfp["mochiTime3F_div"] = 200*60/idfp["mochiTime3F"]

# toL3F_velが走破速度(分速)になっているので、200m単位のタイムに変換
idfp["toL3F"] = 200*60/idfp["toL3F_vel"]
dffl = idfp[["raceId", "raceDate"]+feature_columns +
            ["prePace3F", "toL3F_vel_mean", "toL3F"]]  # .drop_duplicates("raceId", ignore_index=True)
dffl
Out[34]:
raceId raceDate 逃げ 先行 差し 追込 field place dist_cat distance b2StallionId stallionId clsName_lag1 clsName_lag2 clsName_lag3 clsName_lag4 clsName_lag5 prePace3F toL3F_vel_mean toL3F
0 200002010405 2000-06-18 0.500 NaN 0.500 NaN 函館 S 1000 000a000ab8 000a0001aa 差し NaN NaN NaN NaN 12.050000 961.058363 12.200000
1 200002010405 2000-06-18 0.500 NaN 0.500 NaN 函館 S 1000 000a0017c9 000a000287 NaN NaN NaN NaN NaN 12.050000 961.058363 12.650000
2 200002010405 2000-06-18 0.500 NaN 0.500 NaN 函館 S 1000 000a000f8c 000a000148 NaN NaN NaN NaN NaN 12.050000 961.058363 12.900000
3 200002010405 2000-06-18 0.500 NaN 0.500 NaN 函館 S 1000 000a0012cb 000a002072 NaN NaN NaN NaN NaN 12.050000 961.058363 12.100000
4 200002010405 2000-06-18 0.500 NaN 0.500 NaN 函館 S 1000 000a000484 000a0002a6 NaN NaN NaN NaN NaN 12.050000 961.058363 12.450000
971261 202309050912 2023-12-28 0.425 0.175 0.225 0.175 阪神 S 1200 000a0022ae 000a012056 逃げ 追込 追込 差し 追込 11.566667 1022.114517 11.866667
971262 202309050912 2023-12-28 0.425 0.175 0.225 0.175 阪神 S 1200 000a00232a 2010105827 差し 追込 逃げ 逃げ 先行 11.566667 1022.114517 11.700000
971263 202309050912 2023-12-28 0.425 0.175 0.225 0.175 阪神 S 1200 000a000d12 2008103552 先行 逃げ 逃げ 逃げ 先行 11.566667 1022.114517 12.000000
971264 202309050912 2023-12-28 0.425 0.175 0.225 0.175 阪神 S 1200 000a001d7e 2011104063 逃げ 逃げ 差し 逃げ 逃げ 11.566667 1022.114517 11.766667
971265 202309050912 2023-12-28 0.425 0.175 0.225 0.175 阪神 S 1200 000a001c1d 000a0113a1 差し 追込 差し 差し 差し 11.566667 1022.114517 11.633333

971266 rows × 38 columns

8-6-3.モデルの学習¶

In [35]:
params = {
    'metric': 'rmse',
    "categorical_feature": cat_list,
    'boosting_type': 'gbdt',
    'seed': 777,
}

dftrain, dfvalid, dftest = dffl[dffl["raceId"].str[:4] <= "2019"], dffl[dffl["raceId"].str[:4].isin(
    ["2020"])], dffl[dffl["raceId"].str[:4].isin(["2021"])]
train_data = lgbm.Dataset(
    dftrain[feature_columns], label=dftrain[label_column])
valid_data = lgbm.Dataset(
    dfvalid[feature_columns], label=dfvalid[label_column])
test_data = lgbm.Dataset(dftest[feature_columns], label=dftest[label_column])

# モデル学習
model = lgbm.train(params, train_data, num_boost_round=1000, valid_sets=[
                   train_data, valid_data], callbacks=[
    lgbm.early_stopping(
        stopping_rounds=50, verbose=True,),
    lgbm.log_evaluation(25 if True else 0)
],)
[LightGBM] [Warning] Categorical features with more bins than the configured maximum bin number found.
[LightGBM] [Warning] For categorical features, max_bin and max_bin_by_feature may be ignored with a large number of categories.
[LightGBM] [Warning] categorical_feature is set=field,place,dist_cat,weather,condition,direction,inoutside,horseId,breedId,bStallionId,b2StallionId,stallionId,clsName_lag1,clsName_lag2,clsName_lag3,clsName_lag4,clsName_lag5, categorical_column=4,5,6,8,9,11,12,15,23,24,25,26,27,28,29,30,31,32 will be ignored. Current value: categorical_feature=field,place,dist_cat,weather,condition,direction,inoutside,horseId,breedId,bStallionId,b2StallionId,stallionId,clsName_lag1,clsName_lag2,clsName_lag3,clsName_lag4,clsName_lag5
[LightGBM] [Info] Auto-choosing col-wise multi-threading, the overhead of testing was 0.124825 seconds.
You can set `force_col_wise=true` to remove the overhead.
[LightGBM] [Info] Total Bins 52303
[LightGBM] [Info] Number of data points in the train set: 803372, number of used features: 33
[LightGBM] [Info] Start training from score 12.202179
Training until validation scores don't improve for 50 rounds
[25]	training's rmse: 0.225986	valid_1's rmse: 0.218489
[50]	training's rmse: 0.212764	valid_1's rmse: 0.210909
[75]	training's rmse: 0.207456	valid_1's rmse: 0.209075
[100]	training's rmse: 0.203648	valid_1's rmse: 0.207544
[125]	training's rmse: 0.200818	valid_1's rmse: 0.206652
[150]	training's rmse: 0.19866	valid_1's rmse: 0.206306
[175]	training's rmse: 0.196741	valid_1's rmse: 0.205933
[200]	training's rmse: 0.195037	valid_1's rmse: 0.205793
[225]	training's rmse: 0.193364	valid_1's rmse: 0.205662
[250]	training's rmse: 0.191817	valid_1's rmse: 0.20552
[275]	training's rmse: 0.190307	valid_1's rmse: 0.205487
[300]	training's rmse: 0.188959	valid_1's rmse: 0.205517
[325]	training's rmse: 0.187661	valid_1's rmse: 0.20555
Early stopping, best iteration is:
[290]	training's rmse: 0.189474	valid_1's rmse: 0.205456

8-6-4.推論結果の追加¶

In [36]:
dftrain["pred"] = model.predict(
    dftrain[feature_columns], num_iteration=model.best_iteration)

dfvalid["pred"] = model.predict(
    dfvalid[feature_columns], num_iteration=model.best_iteration)

dftest["pred"] = model.predict(
    dftest[feature_columns], num_iteration=model.best_iteration)

8-6-5.特徴量重要度の確認¶

In [37]:
pd.DataFrame(model.feature_importance("gain"),
             index=feature_columns, columns=["重要度"]).round(3).sort_values("重要度")
Out[37]:
重要度
mochiTime_diff 208.708
差し 406.123
mochiTime3F_div 441.035
先行 582.645
追込 656.533
mochiTime3F 844.017
dist_cat 876.345
inoutside 1181.074
weather 1231.293
direction 1355.334
b2StallionId 1698.908
bStallionId 1718.064
clsName_lag5 2072.589
clsName_lag4 2681.468
stallionId 3211.795
clsName_lag3 3418.222
horseNum 3447.745
mochiTime3F_mean_div 4273.768
mochiTime3F_mean 4609.017
mochiTime 4903.437
clsName_lag2 5444.431
逃げ 5949.890
mochiTime_div 6860.008
horseId 7815.775
condition 11125.575
clsName_lag1 12426.860
raceGrade 13490.196
breedId 13742.759
field 20288.514
place 32822.763
distance 70305.094
mochiTime_mean_div 230721.615
mochiTime_mean 286728.204

結果からレースごとの出走馬の持ちタイムの平均情報が最も重要であり、次にレース距離, 競馬場, 馬場, 母の血統, レースグレードと続いている。

8-6-6.回帰モデルの評価指標の確認¶

ここでは決定係数と平均二乗誤差をみる。

  • 決定係数: モデルがどれだけデータをうまく説明しているかを示す。1に近いほど良いモデル
  • 平均二乗誤差: 予測値と実測値の誤差の二乗平均を示す。大きな誤差を重視する指標
In [38]:
from sklearn.metrics import r2_score, mean_squared_error

for key, dfg in [("train", dftrain), ("valid", dfvalid), ("test", dftest)]:
    print(
        f"データ: {key}, \tR2: {r2_score(dfg[label_column], dfg['pred']):.5f}, \tMSE: {mean_squared_error(dfg[label_column], dfg['pred']):.5f}")
データ: train, 	R2: 0.83310, 	MSE: 0.03590
データ: valid, 	R2: 0.78918, 	MSE: 0.04221
データ: test, 	R2: 0.78177, 	MSE: 0.04602

結果を見ると、決定係数の値がかなりよく、1に近ければそれだけ良い指標で普通は0.7を超えると十分良い結果だと言われていることもあり、値が0.78177とかなり良い値となっている。
つまり、ペース情報の予測はそれなりにうまくいっているといえる。

In [39]:
dft = pd.merge(idfp, dftest[["raceId", "horseId", "pred"]], on=[
               "raceId", "horseId"])

ランダムにレース情報を確認してみる。
実際に予測結果のペース情報が出走馬の出走タイムに沿っているか確認する。

実態とは微妙に違うが、脚質情報(clsName)は最終コーナ通過時の順位が大きく寄与しているので、今回の予測結果の値が小さいとそれだけ逃げの脚質である可能性が高い。

In [40]:
dft["pred_rank"] = dft.groupby("raceId")["pred"].rank()
dft[dft["raceId"].isin(["202105020811"])][[
    "clsName", "pred_rank", "label", "raceId", "raceName", "horseId", label_column, "pred", "label_1C_rate", "label_lastC_rate", "mochiTime_diff"]].sort_values("pred_rank")
Out[40]:
clsName pred_rank label raceId raceName horseId toL3F pred label_1C_rate label_lastC_rate mochiTime_diff
17552 逃げ 1.0 4 202105020811 第16回ヴィクトリアマイル(G1) 2016104470 11.60 11.758741 0.277778 0.222222 38.807396
17542 逃げ 2.0 18 202105020811 第16回ヴィクトリアマイル(G1) 2016101285 11.60 11.765989 0.166667 0.222222 22.946129
17541 逃げ 3.0 10 202105020811 第16回ヴィクトリアマイル(G1) 2017102822 11.52 11.778851 0.055556 0.055556 29.935226
17545 先行 4.0 9 202105020811 第16回ヴィクトリアマイル(G1) 2017100043 11.64 11.813869 0.388889 0.388889 -52.580145
17555 逃げ 5.0 15 202105020811 第16回ヴィクトリアマイル(G1) 2017102342 11.54 11.822320 0.111111 0.111111 47.834133
17554 追込 6.0 13 202105020811 第16回ヴィクトリアマイル(G1) 2017105613 11.84 11.830659 0.944444 0.944444 7.563091
17549 追込 7.0 7 202105020811 第16回ヴィクトリアマイル(G1) 2016104867 11.78 11.842108 0.777778 0.777778 -21.852278
17544 差し 8.0 1 202105020811 第16回ヴィクトリアマイル(G1) 2016104532 11.68 11.848054 0.5 0.555556 0.874072
17546 差し 9.0 2 202105020811 第16回ヴィクトリアマイル(G1) 2016105198 11.70 11.851504 0.611111 0.555556 -51.089795
17556 逃げ 10.0 6 202105020811 第16回ヴィクトリアマイル(G1) 2017105563 11.56 11.855721 0.166667 0.111111 14.342350
17540 逃げ 11.0 5 202105020811 第16回ヴィクトリアマイル(G1) 2016102775 11.60 11.872054 0.277778 0.222222 -8.994160
17539 先行 12.0 3 202105020811 第16回ヴィクトリアマイル(G1) 2017104804 11.64 11.877100 0.5 0.388889 9.249375
17550 先行 13.0 11 202105020811 第16回ヴィクトリアマイル(G1) 2015104322 11.64 11.890689 0.388889 0.388889 -40.524235
17547 追込 14.0 14 202105020811 第16回ヴィクトリアマイル(G1) 2017105607 11.78 11.898101 0.777778 0.777778 38.807396
17543 差し 15.0 8 202105020811 第16回ヴィクトリアマイル(G1) 2017104637 11.74 11.919487 0.611111 0.666667 10.941327
17553 追込 16.0 16 202105020811 第16回ヴィクトリアマイル(G1) 2016100625 11.78 11.919664 0.777778 0.777778 -20.263337
17551 差し 17.0 17 202105020811 第16回ヴィクトリアマイル(G1) 2016105009 11.74 11.919685 0.611111 0.666667 16.051479
17548 追込 18.0 12 202105020811 第16回ヴィクトリアマイル(G1) 2016104671 11.84 11.956164 1.0 0.944444 -42.048022

なんとなくそれなりに割り振りができてそうか?
実際にpred_rank1.0のもの(horseId=2016104470)では、脚質が逃げになっているのが分かる。

これはうまくいきすぎているとは思うが、上位3位まですべて逃げの脚質となっていることから、まぁ精度よく学習できていると思える。
しかし、6位や7位で追込の脚質がきていることから、幾分ブレがあるようである。

実際のクラスタ結果と今回の予測結果のランク分けに関連があるか確認する

In [41]:
dft[["cluster", "pred_rank"]].corr()
Out[41]:
cluster pred_rank
cluster 1.000000 0.395308
pred_rank 0.395308 1.000000

結果から相関が0.395308ほどとそれなりに関係があると分かる。

In [42]:
for col in ["pred"]:
    datalist = []
    for cols in combinations(clsnames, 2):
        x = dft[dft["clsName"].isin([cols[0]])][col].dropna().values
        y = dft[dft["clsName"].isin([cols[1]])][col].dropna().values
        datalist += [mannwhitneyu(x, y, alternative="less").pvalue]
    corrected_p_values = smm.multipletests(
        datalist, alpha=0.05, method='bonferroni')
    print(
        f"対象: {col},\t",
        f"有意か?: {corrected_p_values[0].all()},\t",
        f"p値: {corrected_p_values[1].round(6)}"
    )
対象: pred,	 有意か?: True,	 p値: [0.       0.       0.       0.005844 0.       0.024495]

一応検定してみると有意差が出ていることがわかる。(p値が微妙なものもあるが・・・)
つまり、予測タイムが早ければ早いほど実際に分類される脚質も逃げ > 先行 > 差し > 追込みの順になっているということである。

In [43]:
# region plot
dft["pred_q75"] = dft["raceId"].map(dft[["raceId", "pred_rank"]].groupby(
    "raceId")["pred_rank"].quantile(0.75).to_dict())
dft["pred_q75"] = dft["pred_rank"] >= dft["pred_q75"]
dft[dft["pred_q75"]]["label"].value_counts().sort_index()

dft["pred_q25"] = dft["raceId"].map(dft[["raceId", "pred_rank"]].groupby(
    "raceId")["pred_rank"].quantile(0.25).to_dict())
dft["pred_q25"] = dft["pred_rank"] <= dft["pred_q25"]
dft[dft["pred_q25"]]["label"].value_counts().sort_index()
idft = pd.concat(
    [
        dft[dft["pred_q25"]]["label"].value_counts(normalize=True).sort_index(
        ).to_frame().rename(columns={"proportion": "上位25%"}),
        dft[dft["pred_q75"]]["label"].value_counts(normalize=True).sort_index(
        ).to_frame().rename(columns={"proportion": "下位75%"})
    ],
    axis=1
)

(idft.sort_index(ascending=False).cumsum().sort_index(ascending=True).rename(
    columns={"上位25%": "上位25%_累積", "下位75%": "下位75%_累積"}).rename(index=lambda x: x-1)).plot(ax=idft.plot.bar(alpha=0.5).twinx())


plt.title("pred_rankと着順の関係")
plt.legend(loc="upper right", bbox_to_anchor=(
    1., 0.85),)
plt.show()
# endregion

グラフで比べてみると上位25%の方(青線)は累積分布の減り具合が早い(グラフがオレンジ線よりも下にきている)ため、下位75%よりも上位着順の割合が高いことを示している。

スポンサーリンク

8-7.まとめ¶

分かったこと¶

前5走の脚質情報と持ちタイムを使った分析によって、各出走馬の走破タイムの推測ができそうだと分かった。

サードモデルで期待すること¶

過去成績から割り出した脚質情報と持ちタイムを特徴量として追加することでセカンドモデルよりも高いパフォーマンスを達成すること

コメント

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