PR

脚質を教師なし学習で分類しランク学習で予測する方法

Python
この記事は約71分で読めます。
スポンサーリンク

7.過去成績の分析

セカンドモデルの作成では血統情報を考慮したモデルの開発を目的としていた。
これは競走馬が持つ血統が着順に影響を与えていることを前提としており、
いわばその競走馬の基礎力が血統に当たるものだと考えていた。

一方で今回の過去の成績から着順を予想しようとする考え方は、
その競走馬の実力を測ることに等しい。

これからの過去成績の分析から期待することは、
その競走馬の脚質を分析できること
レース展開の分析ができること
競馬場×馬場×距離カテゴリごとに、勝ちやすい脚質が分析できること

上記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-09-21 22:20:09.198 | INFO     | src.data_manager.data_loader:load_racedata:23 - Get Year Range: 2000 -> 2023.
2024-09-21 22:20:09.199 | INFO     | src.data_manager.data_loader:load_racedata:24 - Loading Race Info ...
2024-09-21 22:20:10.039 | INFO     | src.data_manager.data_loader:load_racedata:26 - Loading Race Data ...
2024-09-21 22:20:27.036 | INFO     | src.data_manager.data_loader:load_racedata:28 - Merging Race Info and Race Data ...
2024-09-21 22:20:29.228 | INFO     | src.data_manager.data_loader:load_horseblood:45 - Loading Horse Blood ...
2024-09-21 22:20:55.113 | INFO     | src.data_manager.preprocess_tools:__0_check_use_save_checkpoints:100 - Start PreProcess #0 ...
2024-09-21 22:20:55.113 | INFO     | src.data_manager.preprocess_tools:__1_exec_all_sub_prep1:103 - Start PreProcess #1 ...
2024-09-21 22:21:01.195 | INFO     | src.data_manager.preprocess_tools:__2_exec_all_sub_prep2:105 - Start PreProcess #2 ...
2024-09-21 22:21:13.814 | INFO     | src.data_manager.preprocess_tools:__3_convert_type_str_to_number:107 - Start PreProcess #3 ...
2024-09-21 22:21:17.459 | INFO     | src.data_manager.preprocess_tools:__4_drop_or_fillin_none_data:109 - Start PreProcess #4 ...
2024-09-21 22:21:20.847 | INFO     | src.data_manager.preprocess_tools:__5_exec_all_sub_prep5:111 - Start PreProcess #5 ...
2024-09-21 22:21:38.918 | INFO     | src.data_manager.preprocess_tools:__6_convert_label_to_rate_info:113 - Start PreProcess #6 ...
2024-09-21 22:21:49.395 | INFO     | src.data_manager.preprocess_tools:__7_convert_distance_to_smile:115 - Start PreProcess #7 ...
2024-09-21 22:21:49.663 | INFO     | src.data_manager.preprocess_tools:__8_category_encoding:117 - Start PreProcess #8 ...
2024-09-21 22:21:54.563 | INFO     | src.data_manager.preprocess_tools:__9_convert_raceClass_to_grade:119 - Start PreProcess #9 ...
2024-09-21 22:22:02.312 | INFO     | src.data_manager.preprocess_tools:__10_add_bloods_info:123 - Start PreProcess #10 ...
スポンサーリンク

7-1.過去成績の調べ方

競走馬ごとに過去成績を見ることは、

  • 脚質の確認
  • 馬場適正や持ちタイムの確認
  • レース展開の予想がしたい

簡単に上記のような理由があると考える

このうち、馬場適正については、定量的な評価軸が分からない
何を以って適性があると判断するのは個人の裁量に依ると考える

そのため、馬場適正については今回は考えないものとする

スポンサーリンク

7-2.脚質の分析¶

脚質とは、競走馬がレースの序盤,中盤,終盤での
主な立ち位置をとるかを四種類に分けたもの

分類は以下

  1. 逃げ(にげ)
    • 特徴: スタートから一気に前に出て、他の馬に追い越されないように先頭をキープする脚質。
      特に短距離レースで有効。馬が他の馬に追いかけられると燃えるタイプの場合、この脚質が向いている。
    • 戦術: スタートダッシュが非常に重要で、最初の数百メートルでリードを取る必要がある。
      ペース配分を間違えると終盤にバテるリスクがある。
  2. 先行(せんこう)
    • 特徴: スタートから早めに中団から前方のポジションを取る脚質。
      逃げ馬のすぐ後ろについて、直線で一気に抜き去る戦法を取ることが多い。ペースの読みが重要。
    • 戦術: レース中盤までにポジションを確保し、後半に向けて徐々にペースを上げる。
      無駄なエネルギーを使わずに、逃げ馬を追いかけることが求められる。
  3. 差し(さし)
    • 特徴: 中団から後方の位置でレースを進め、終盤の直線で一気にスピードを上げて前の馬を抜き去る脚質。
      ペースが速いレースや、前半で消耗した馬が多いときに有利。
    • 戦術: 序盤はエネルギーを温存し、直線に入るところで仕掛ける。
      直線が長いコースや、ペースが速いレースに向いている。
  4. 追い込み(おいこみ)
    • 特徴: スタートでは最後方に位置し、最終コーナーから直線で一気に加速して前の馬をすべて抜き去る脚質。
      特にペースが崩れたレースでの逆転劇が見どころ。
    • 戦術: 最初はエネルギーを最大限に温存し、最後の直線に全てをかける。
      追い込みはリスクが高く、ペースや展開に左右されやすい。

名馬には差しの脚質が多いと言われているが、
これは差しという戦法を取るにはレース展開などの
周りの状況に合わせてタイミングよく前に飛び出せるという
器用な立ち回りが求められるため、
騎手の意図をよく理解できる利口さと
それを実行できる実力が必要となることから、
自然と能力の高い競走馬が差しの戦法を取るとされている

あと単純に差しで後方から最後の直線で一気に勝ちを奪うのは、
見ていて面白いしロマンがあるので、印象に残りやすいのもありそう

スポンサーリンク

7-3.脚質はどうすれば求められるか¶

前提として、脚質とは戦法であることから最終コーナ通過時または上り3F到達時までの順位でどの立ち位置にいるかを示すものと考える
そのため、コーナ通過順位の1コーナと最終コーナをどの順位で通過したかでその競走馬の脚質が見えてくると考える

よってここではレースの最初のコーナ通過順位と最終コーナ通過順位の2つを使って脚質を調べる

スポンサーリンク

7- 4.2021年までのデータに絞って脚質分析¶

また、今後のモデル分析に向けて未知データもある程度残しておきたい

よって、2021年までのデータを使って分析し、2022年のデータを未知データとする

In [2]:
idfb = df[(df["raceDate"].dt.year < 2022)]
idfb.shape
Out[2]:
(1034651, 82)

脚質とは、レースで馬郡のどの位置を走っているかなので、
コーナ通過順位にまつわるデータとして以下が該当していると考える

  • label_1C: 最初のコーナー通過時の順位
  • label_lastC: 最終コーナー通過時の順位

また通過順位は出走頭数によってその数字の意味が違うので、出走頭数で通過順位を割って割合として扱うこととする

In [3]:
for col in ["label_1C", "label_lastC"]:
    idfb[f"{col}_rate"] = (idfb[col].astype(
        int)/idfb["horseNum"]).convert_dtypes()
idfb[["label_1C_rate", "label_lastC_rate"]]
Out[3]:
label_1C_rate label_lastC_rate
0 0.375 0.5
1 0.25 0.125
2 0.75 1.0
3 1.0 0.875
4 0.375 0.5
1034646 0.9375 0.9375
1034647 0.1875 0.1875
1034648 0.375 0.4375
1034649 0.25 0.25
1034650 0.9375 0.9375

1034651 rows × 2 columns

スポンサーリンク

7-5.脚質の分類¶

分類するための手法(クラスタリング)は有名なものとして、K-Means法がある。
このアルゴリズムは、データポイントを「k」個のクラスターに分けることを目的としている。
それぞれのクラスターは、平均(centroid)と呼ばれる中心点を持ち、その中心点からの距離が最も近いデータポイントをそのクラスターに割り当てる手法である。

つまり、今回の話で言えば、最初のコーナー通過順位と最終コーナー通過順位の2種類の特徴量を使ってK-Means法でクラスタリングすると、脚質を4つに分けられるだろうという魂胆である。

In [4]:
from sklearn.cluster import KMeans  # KMeans法のモジュールをインポート
# クラスタ数
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とする
idfb["label_lastC_rate2"] = rate*idfb["label_lastC_rate"]
kmeans.fit(idfb[cluster_columns2])
Out[4]:
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)

クラスタリングのラベルを追加

このとき、クラスタリングの中心点が毎度ランダムになっていて都合が悪い
なので、label_1C_rateの値が小さい順にクラスタを並び替える

In [5]:
idfb["cluster"] = kmeans.predict(idfb[cluster_columns2])
dfcluster = pd.DataFrame(
    kmeans.cluster_centers_.tolist(), columns=cluster_columns
).sort_values(cluster_columns[0])
dfcluster["reCluster"] = list(range(n_cls))
idfb["cluster"] = idfb["cluster"].map(dfcluster["reCluster"].to_dict())

クラスタリングの中心点を確認

In [6]:
dfcluster
Out[6]:
label_1C_rate label_lastC_rate reCluster
2 0.189736 0.393819 0
0 0.432436 0.995918 1
3 0.639462 1.612348 2
1 0.836256 2.227643 3

内容を見るに

  • reCluster: 0: 最初から最終コーナまで、前方の位置を取っているので「逃げ」の脚質に類似
  • reCluster: 1: 最初から最終コーナまで、中央やや前方気味の位置なので「先行」の脚質に類似
  • reCluster: 2: 最初から最終コーナまで、中央やや後方気味の位置なので「差し」の脚質に類似
  • reCluster: 3: 最初から最終コーナまで、後方の位置を取っているので「追込」の脚質に類似

以上のようなクラスタリングが出来ている

よって、reClusterの値が小さい順に「逃げ」「先行」「差し」「追込」と名付ける

In [7]:
clsnames = ["逃げ", "先行", "差し", "追込"]
cls_map = {i: d for i, d in enumerate(clsnames)}
idfb["clsName"] = idfb["cluster"].map(cls_map)
idfb["clsName"]
Out[7]:
0          先行
1          逃げ
2          追込
3          追込
4          先行
           ..
1034646    追込
1034647    逃げ
1034648    先行
1034649    逃げ
1034650    追込
Name: clsName, Length: 1034651, dtype: object
スポンサーリンク

7-6.脚質と特徴量の関係をみたい¶

簡単に散布図を使って2次元グラフ上でどういったデータがどういった脚質に分類されているのか確認する

In [8]:
# plt.figure(figsize=(12, 12))
plt.rcParams["font.size"] = 10
g = sns.scatterplot(idfb.drop_duplicates(cluster_columns).sort_values(
    "clsName", key=lambda x: x.map({c: i for i, c in enumerate(clsnames)})),
    x="label_1C_rate", y="label_lastC_rate", hue="clsName", alpha=0.5,)
plt.ylim(bottom=-0.0, top=1.05)
plt.grid(ls=":")
plt.scatter(dfcluster[cluster_columns[0]],
            dfcluster[cluster_columns[1]]/rate, marker="X", s=100, c="black")
plt.legend(loc="best")
for txt, row in zip(clsnames, dfcluster[cluster_columns].values):
    plt.text(row[0], row[1]/rate, s=txt, c="black",
             weight="bold", size="xx-large")
plt.title("クラスタ数: 4")
sns.move_legend(g, "upper left", bbox_to_anchor=(
    0.05, -0.1), title='cluster', fontsize=11, ncol=4)
plt.show()

それなりにきれいな分かれ方をしている。
しかし、先行や差しのクラスタを見ると1Cを過ぎたところと最終コーナを通過した時を見るとかなり幅が広い。
つまり、1コーナを後方で通過したのち最終コーナを前方で通過すると先行の脚質に、逆に1Cを中間位置で通過後最終コーナを後方で通過すると差しの脚質になる。
クラスタ中心周辺のデータは皆が考える脚質の特徴に近い動きをするが、クラスタから遠い位置にいる点をどうすべきか困る瞬間がいつかくるかも。
そのため、クラスタ数を増やした場合も見てみようと思う

7-7.クラスタリングの深堀¶

もう少しクラスタリングのやり方を変える
以下の方針で分類

  1. 分類をさらに16クラスに増やす
  2. クラスタリング結果の中心点をlabel_lastC_rateで昇順に並び変え
  3. ソート結果の上から4つごとに逃げ, 先行, 差し, 追込に分類
  4. 分類した4グループ間でlabel_1C_rateを昇順に並び変え

上記の方針の目的は、レースでは最終コーナ通過時に馬群のどの位置にいるかが重要だと考え、最もゴールに近いことからも早く通過すればするほど有利なのは間違いない
そのため、クラスタの中心点のlabel_lastC_rateの値が小さいデータほど逃げの戦法を取っている可能性が高いと考えた
そして、label_1C_rateが小さい場合は出走してから1コーナー通過した段階でレースのペースを作りリードする立場にあると見なせる
よってこのクラスタ分けの結果は、label_lastC_rateが小さいものから脚質の特徴を割り振り、脚質別にlabel_1C_rateが小さければ小さいほどペースメーカーを担うものを分類できると期待する

In [9]:
dflist = []
dfclist = []
sub_ncls = 4
# cluster_columns2 = cluster_columns

kmeans = KMeans(sub_ncls*n_cls)
kmeans.fit(idfb[cluster_columns2])
idfb[f"cluster2"] = kmeans.predict(idfb[cluster_columns2])
idfc = pd.DataFrame(
    kmeans.cluster_centers_.tolist(), columns=cluster_columns
)
idfc.sort_values(cluster_columns[1], inplace=True)
idfc[f"reCluster2"] = list(range(sub_ncls*n_cls))
idfb[f"cluster2"] = idfb[f"cluster2"].map(
    idfc[f"reCluster2"].to_dict())
dflist += [idfb]
dfclist += [idfc]
idfc = pd.concat(dfclist, ignore_index=True)

# label_lastC_rateが低い順にクラスを分け直す
clsnames2 = [f"{g}{i}" for g in clsnames for i in range(sub_ncls)]

idfc = idfc.sort_values(cluster_columns[1], ignore_index=True)
idfc["Cname"] = [g for g in range(n_cls) for _ in range(sub_ncls)]
idfc.sort_values(["Cname", cluster_columns2[0]],
                 ignore_index=True, inplace=True)
new_cls_map = {old: new for old, new in zip(
    idfc["reCluster2"].tolist(), clsnames2)}
idfc["reCluster3"] = idfc["reCluster2"].map(new_cls_map)

idfc
Out[9]:
label_1C_rate label_lastC_rate reCluster2 Cname reCluster3
0 0.110332 0.244875 0 0 逃げ0
1 0.211037 0.466209 1 0 逃げ1
2 0.273888 0.687737 3 0 逃げ2
3 0.618455 0.485045 2 0 逃げ3
4 0.362448 0.906389 4 1 先行0
5 0.427353 1.158729 6 1 先行1
6 0.655619 1.344776 7 1 先行2
7 0.694150 0.973195 5 1 先行3
8 0.389894 1.448262 8 2 差し0
9 0.443119 1.865435 11 2 差し1
10 0.649980 1.592815 9 2 差し2
11 0.781112 1.794013 10 2 差し3
12 0.451094 2.326620 14 3 追込0
13 0.809284 2.040235 12 3 追込1
14 0.900282 2.271790 13 3 追込2
15 0.946882 2.500000 15 3 追込3

なんとなくうまくいってそう?

ということで、クラスタ分けの結果とクラスタ中心がどうなったかを確認してみる

In [10]:
idfAll = pd.concat(dflist, ignore_index=True)
idfAll["cluster2"] = idfAll["cluster2"].map(
    idfc.set_index("reCluster2")["reCluster3"].to_dict())

plt.figure(figsize=(8, 6))
plt.rcParams["font.size"] = 11

g = sns.scatterplot(idfAll.drop_duplicates(cluster_columns), x="label_1C_rate",
                    y="label_lastC_rate", hue="cluster2", alpha=0.75, hue_order=idfc["reCluster3"].tolist())
# plt.vlines(x=0, ymax=2, ymin=-1, colors="black", alpha=0.5)
# plt.ylim(bottom=0.025, top=1.025)
# plt.xlim(left=-0.25, right=1.025)
plt.scatter(idfc[cluster_columns[0]],
            idfc[cluster_columns[1]]/rate, marker="X", s=100, c="black", label="Center")
for txt, row in zip(clsnames2, idfc[cluster_columns].values):
    plt.text(row[0], row[1]/rate, s=txt, c="black",
             weight="bold", size="xx-large")
# plt.legend(loc="best")
sns.move_legend(g, "upper left", bbox_to_anchor=(
    0.1, -0.1), title='cluster2', fontsize=11, ncol=4)
plt.grid(ls=":")
plt.show()

分布から「逃げ0」から「逃げ2」はlabel_lastC_rate, label_1C_rateがちょうど同じ値に近く値自身も小さい
よって、このクラスはレース序盤から逃げの戦法をとり最終コーナまで同じペースと位置を維持できるほどの能力が他の競走馬よりもあったことが分かる
一方で「追込1」から「追込3」では、さっきとは真逆でレースのペースについていけなかった競走馬であると考えられる
このクラスタリングのやり方でもまだ「先行」と「差し」の区別が難しい状態ではあるがlabel_lastC_rateを基準に見れば「先行0」以外の「先行」グループは「差し」グループと区別がついているように見える

スポンサーリンク

7-8.最終着順と脚質の関係¶

ではこのクラスタリング結果から最終着順が1着になっているクラスタの分布を確認する

In [11]:
idfAll["label_rate"] = idfAll["label"]/idfAll["horseNum"]
plt.figure(figsize=(32, 8), )
plt.rcParams["font.size"] = 18

params = {
    "x": "label",
    # "y":"clsName",
    "hue": "cluster2",
    # "bins": 7,
    "multiple": "dodge",
    # "kde": True,
    "stat": "probability",
    "hue_order": clsnames2
}
for num, dist in enumerate("SMILE", start=1):
    plt.subplot(2, 5, num)
    sns.histplot(
        idfAll[idfAll["label"].isin([1]) & ~idfAll["field"].isin(["芝"]) & ~idfAll["dist_cat"].isin(
            [dist])][cluster_columns+["label", "label_rate", "cluster2", "cluster"]],
        **params,
        legend=num == 6,

    )
    plt.ylim(0, 0.45)
    plt.grid(ls=":")
    plt.xticks([1], ["1着"])
    plt.xlabel(f"芝-{dist}")
    if dist == "E":
        continue
    flag = num == 4
    plt.subplot(2, 5, 5+num)
    g = sns.histplot(
        idfAll[idfAll["label"].isin([1]) & ~idfAll["field"].isin(["ダ"]) & ~idfAll["dist_cat"].isin(
            [dist])][cluster_columns+["label", "label_rate", "cluster2", "cluster"]],
        **params,
        legend=flag
    )
    plt.ylim(0, 0.45)
    plt.grid(ls=":")
    plt.xticks([1], ["1着"])
    plt.xlabel(f"ダ-{dist}")
    if flag:
        sns.move_legend(g, "upper left", bbox_to_anchor=(
            1.0, 1), title='cluster2', fontsize=14, ncol=4)

plt.show()

結果からやはり最終コーナを上位で通過した競走馬はゴールに近い分、1着になる割合が高いことが分かる
この傾向は馬場や距離カテゴリ別で見ても同じであることから、最初に逃げの戦法をとる競走馬が有利であることが分かる

クラスタリングの分け方が多すぎるので、末尾の数字を切って「逃げ」「先行」「差し」「追込」の4種類に減らして1着になる割合を確認する

In [12]:
idfAll["clsName"] = idfAll["cluster2"].str[:-1]

plt.figure(figsize=(32, 8), )
plt.rcParams["font.size"] = 18

params = {
    "x": "label",
    # "y":"clsName",
    "hue": "clsName",
    # "bins": 7,
    "multiple": "dodge",
    # "kde": True,
    "stat": "probability",
    "hue_order": clsnames
}
for num, dist in enumerate("SMILE", start=1):
    plt.subplot(2, 5, num)
    sns.histplot(
        idfAll[idfAll["label"].isin([1]) & ~idfAll["field"].isin(["芝"]) & ~idfAll["dist_cat"].isin(
            [dist])][cluster_columns+["label", "label_rate", "clsName"]],
        **params,
        legend=num == 6,

    )
    plt.ylim(0, 1)
    plt.grid(ls=":")
    plt.xticks([1], ["1着"])
    plt.xlabel(f"芝-{dist}")
    if dist == "E":
        continue
    flag = num == 4
    plt.subplot(2, 5, 5+num)
    g = sns.histplot(
        idfAll[idfAll["label"].isin([1]) & ~idfAll["field"].isin(["ダ"]) & ~idfAll["dist_cat"].isin(
            [dist])][cluster_columns+["label", "label_rate", "clsName"]],
        **params,
        legend=flag
    )
    plt.ylim(0, 1)
    plt.grid(ls=":")
    plt.xticks([1], ["1着"])
    plt.xlabel(f"ダ-{dist}")
    if flag:
        sns.move_legend(g, "upper left", bbox_to_anchor=(
            1.0, 1), title='clsName', fontsize=14, ncol=4)

plt.show()

逃げの脚質の競走馬が1着になる競走馬の中で6割から最大8割にものぼることが分かる

つまり、勝てる競走馬を割り出すには出走馬の中から最終コーナを上位で回れるものを予測するのが重要そうだと分かった

ちなみに、逃げの脚質の競走馬の回収率も見てみる

In [13]:
for col in clsnames:
    idfa = idfAll[idfAll["cluster2"].str.contains(col)]
    p_list = []
    for idx in range(sub_ncls):
        iidfa = idfa[idfa["cluster2"].isin([f"{col}{idx}"])]
        p_list += [
            {
                "脚質": f"{col}{idx}",
                "収益": iidfa[iidfa["label"].isin([1])]["odds"].sum(),
                "ベット数": len(iidfa),
                "回収率": iidfa[iidfa["label"].isin([1])]["odds"].sum()/len(iidfa)
            }
        ]

    p_list += [
        {
            "脚質": f"{col}",
            "収益": idfa[idfa["label"].isin([1])]["odds"].sum(),
            "ベット数": len(idfa),
            "回収率": idfa[idfa["label"].isin([1])]["odds"].sum()/len(idfa)
        }
    ]
    display(pd.DataFrame(p_list).set_index("脚質").convert_dtypes().T)
脚質 逃げ0 逃げ1 逃げ2 逃げ3 逃げ
収益 249274.7 110895.9 72044.1 34140.4 466355.1
ベット数 134710 94824 89857 19122 338513
回収率 1.850454 1.169492 0.801764 1.785399 1.377658
脚質 先行0 先行1 先行2 先行3 先行
収益 58545.9 38815.5 44927.4 33516.4 175805.2
ベット数 83106 84739 65353 29309 262507
回収率 0.704473 0.458059 0.687457 1.143553 0.669716
脚質 差し0 差し1 差し2 差し3 差し
収益 6509.8 2108.2 30332.0 30596.6 69546.6
ベット数 34814 26703 72985 78347 212849
回収率 0.186988 0.07895 0.415592 0.390527 0.326741
脚質 追込0 追込1 追込2 追込3 追込
収益 411.6 20406.2 13219.6 4006.0 38043.4
ベット数 18722 76428 82176 43456 220782
回収率 0.021985 0.266999 0.160869 0.092185 0.172312

逃げの脚質だけ十分収益を出せることが分かる
※これはレース結果をもとに割り振った脚質を使っての分析なので、予想モデル作成では使えない情報であることに注意

スポンサーリンク

7-9.クラスタリング結果の使い方

脚質の分析から1着の競走馬を予測したい ⇒ 「逃げ」の脚質を予測したいと言い換えて考えてみる

どう考えればいいか?¶

過去の成績からどの脚質に割り振られるかを分類できると良いのではないか?
大事なことは脚質とはレース中の戦法であるので、これまでのファーストモデルやセカンドモデルのように競走馬自身の情報+レース情報だけで得た1着になる確信度を予測するモデルでは不十分だということ。
つまり、他の競走馬を考慮してレース中の戦法を取るので、脚質の分類を考えたい場合はレースに出走する競走馬同士の情報を考慮する必要がある。

上記のような考え方の場合、思いつくのがニューラルネットワークを用いた学習方法であるがこれまでの動画や記事の流れで行くと少し話が飛躍してしまう。
なので、ここではもう少しLightGBMをこすることにする。

LightGBMによるランキング学習アプローチ

  • 特徴量: 競走馬の過去の脚質分布と出走する競走馬全体の脚質分布と出走馬たちの過去の走破速度など、脚質に関連しそうな情報を使う
  • 目的変数: 2パターンを用意。「逃げ」「先行」「差し」「追込」の4クラスとさらに4クラスに細分化した16クラス
  • タスク: ランク学習

ランク学習(/ランキング学習)とは?
回帰モデルや分類モデルでは、目的変数の値を直接求めようと学習する。
つまり、回帰モデルでは例えば競走馬の走破タイムを予測する場合、そのモデルはその競走馬の走破タイムを予想するという感じ。

しかし、ランク学習では目的変数同士の大小関係を学習する。
つまり、データAとデータBがある評価指標に基づきA>Bという関係があるとき、ランク学習ではA>Bとなるようにモデルを学習する。
なので、損失関数はA>Bとなるパラメータを良しとし、A<Bとなるパラメータに罰則を与えるように学習する。
どのようにランク学習するかというと、LightGBMにはちょうどランク学習用のモデルが用意されているのでLightGBMを使ってやるのが手っ取り早い。
通常、ランク学習はクエリと呼ばれる一つのグループごとにデータ間の大小を学習していくのも、競馬予想というタスクでも非常に都合がいい

なぜランク学習?¶

ランク学習は目的変数の大小関係を学習するものであること、そして今回の目的は脚質4種類(または16種類)を予測するものである。
そもそも脚質というのはレース中の戦法であり出走中の出走位置を示す。
そのため、この脚質というものには「逃げ」>「先行」>「差し」>「追込」という順序関係が存在している。
そしてこの順序はレースごとに決まるものなので、レースごとに目的変数の大小関係を学習できるランク学習がうってつけである。

スポンサーリンク

7-10.LightGBMによる脚質の予測¶

7-9節の話から、脚質を予測するランク学習のLightGBMモデルを作成する

特徴量の作成

脚質に関係しそうな指標が必要であるが、それらは過去の成績から分かるものという前提を置く。
過去成績から

レースごとに競走馬の過去の脚質や走破速度の集計をしないといけないが正直かなり大変
泥臭いがやってみることにする

In [14]:
dflist = []
mode = "clsName"
modelist = clsnames
dfg = idfAll[["horseId", "raceId", "raceDate",
              "field", "dist_cat"]].groupby("raceId")
dfhg = idfAll[["horseId", "raceId", "raceDate",
               "field", "dist_cat"]].groupby("horseId")
idfa = idfAll[["horseId", "raceId", "raceDate", "field", "dist_cat", mode]]
# 処理数を減らすために2014年のデータから集計する
for raceId in tqdm.tqdm(idfAll[["raceDate", "raceId"]][idfAll["raceDate"].dt.year > 2013]["raceId"].sort_values().unique()):
    dfrace = dfg.get_group(raceId)
    raceDate: datetime.datetime = dfrace["raceDate"].unique()[0]
    field = dfrace["field"].unique()[0]
    dist_cat = dfrace["dist_cat"].unique()[0]

    idfh = idfa[idfa["horseId"].isin(dfrace["horseId"].tolist()) & idfa["field"].isin([
        field]) & idfa["dist_cat"].isin([dist_cat])]
    idfh: pd.DataFrame = idfh[idfh["raceDate"] < raceDate]
    if len(idfh) > 0:
        horseList = sorted(idfh["horseId"].unique())
        dfcls = (idfh.groupby("horseId")[mode].value_counts(
        )/idfh.groupby("horseId")[mode].count())
        dfcls = pd.concat([dfcls.loc[hId].to_frame(hId)
                           for hId in horseList], axis=1).T.reset_index(names="horseId").fillna(0)
        dfcls["raceId"] = raceId
        dfclsAll = (idfh[mode].value_counts()/len(idfh)
                    ).rename(lambda x: f"ALL{x}").to_frame().T
        dfclsAll["raceId"] = raceId

        dfcls = dfcls.set_index(["raceId", "horseId"]).reset_index()
        dfcls = pd.merge(dfrace[["raceId", "horseId"]], dfcls, on=[
                         "raceId", "horseId"], how="left")
        dfcls = pd.merge(dfcls, dfclsAll, on="raceId", how="left")
        dflist += [dfcls]
    else:
        dflist += [dfrace[["raceId", "horseId"]]]
    # break
    if raceDate.year == 2015:
        break
dffeature = pd.concat(dflist, ignore_index=True)
 12%|█▏        | 3326/26617 [09:33<1:06:55,  5.80it/s]

1年間のループで9分程度かかる
少なくとも2014年以降のデータを処理するのだから、まともに逐次処理をしたら1時間以上はかかってしまう
さすがに待ってられないので、並列処理を行う関数を用意した

notebookに関数を記載して並列処理はどう頑張っても無理なので、競馬予想AIプログラムのソースに並列処理用の関数を作成した

この並列処理で作成する特徴量は以下

  • 過去の成績から分類した脚質の分布を計算

以下は本ソース↓を入手しないと実行できないので注意ください

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

In [15]:
from src.multi_func_utils.quantile_past_velosity import multi_exec_for_notebook
mode = "clsName"
modelist = clsnames
# mode = "cluster2"
# modelist = clsnames2
dffeature = multi_exec_for_notebook(
    mode,
    2014,
    2021,
    idfAll[[mode, "horseId", "raceId", "last3F_vel",
            "toL3F_vel", "raceDate", "field", "dist_cat"]],
    256,
    10
)
processing horseId ...: 100%|██████████| 26617/26617 [08:25<00:00, 52.62race/s] 

自分の環境で9分程度で終わる
かなり便利なので、活用ください

もう少し整理して学習できる形までもっていく

In [16]:
# 説明変数

feature_columns = [
    "raceId",
    "breedId", "bStallionId", "b2StallionId", "stallionId",
    "field", "dist_cat", "distance", "place", "condition",

    "clsLabel_lag1", "clsLabel_lag2", "clsLabel_lag3", "clsLabel_lag4", "clsLabel_lag5",
    "clsLabel_lag6", "clsLabel_lag7", "clsLabel_lag8", "clsLabel_lag9", "clsLabel_lag10",

    "last3F_vel_lag1", "last3F_vel_lag2", "last3F_vel_lag3",
    "toL3F_vel_lag1", "toL3F_vel_lag2", "toL3F_vel_lag3",
    "raceGrade", "raceGrade_diff1",
]+dffeature.columns.tolist()[2:]
In [17]:
dflabel = idfAll[["raceDate", "raceId", "raceDetail", "jockeyId", "horseId", "favorite", "clsName", "label", "cluster2",
                  "field", "dist_cat", "distance", "place", "condition", "odds", "raceGrade", "last3F_vel", "toL3F_vel", "label_1C", "label_lastC"]]
for col in ["horseId", "breedId", "bStallionId", "b2StallionId", "stallionId"]:
    idfAll[col] = idfAll[col].astype(str)

dffl = pd.merge(dflabel, dffeature, on=[
                "raceId", "horseId"], how="left").fillna(0)

for col in ["breedId", "bStallionId", "b2StallionId", "stallionId"]:
    dffl[col] = dffl["horseId"].map(idfAll.set_index("horseId")[col].to_dict())
dffl
Out[17]:
raceDate raceId raceDetail jockeyId horseId favorite clsName label cluster2 field 追込 差し ALL逃げ ALL先行 ALL追込 ALL差し breedId bStallionId b2StallionId stallionId
0 2000-01-05 200006010101 4歳未勝利 00733 1997100761 5 先行 7 先行1 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 1992105075 000a000081 000a001840 000a000d4b
1 2000-01-05 200006010101 4歳未勝利 00635 1997100656 1 逃げ 1 逃げ0 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 1990106707 000a000d2e 000a000456 000a000013
2 2000-01-05 200006010101 4歳未勝利 00725 1997100203 8 追込 8 追込3 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 1979101701 000a000258 000a000f51 1982101222
3 2000-01-05 200006010101 4歳未勝利 00672 1997104609 2 追込 2 追込2 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 1988100198 000a0001ff 000a000ec9 000a000d87
4 2000-01-05 200006010101 4歳未勝利 00621 1997106623 7 先行 3 先行1 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 1987102907 000a00010d 000a00026d 1988106402
1034646 2021-12-28 202109060912 3歳以上3勝クラス 01141 2016110041 12 追込 15 追込2 0.000000 0.300000 0.424242 0.280303 0.174242 0.121212 000a013aee 000a0126fd 1996190001 000a01239b
1034647 2021-12-28 202109060912 3歳以上3勝クラス 01126 2017105661 2 逃げ 2 逃げ1 0.000000 0.000000 0.424242 0.280303 0.174242 0.121212 2009106088 2002100816 000a010a17 2008103552
1034648 2021-12-28 202109060912 3歳以上3勝クラス 01171 2016104227 15 先行 8 先行1 0.200000 0.200000 0.424242 0.280303 0.174242 0.121212 2005102027 1996106512 1986109108 1998101554
1034649 2021-12-28 202109060912 3歳以上3勝クラス 01180 2017106137 6 逃げ 16 逃げ2 0.100000 0.000000 0.424242 0.280303 0.174242 0.121212 1999101675 000a000d7f 000a001702 2003110212
1034650 2021-12-28 202109060912 3歳以上3勝クラス 01182 2017101370 3 追込 11 追込2 0.333333 0.166667 0.424242 0.280303 0.174242 0.121212 2011104651 2002100816 000a0014ce 000a011155

1034651 rows × 32 columns

以上で説明変数と目的変数を持つDataFrameに変換できた

In [18]:
agg_list = ["field", "dist_cat"]

dfcheck = dffl[["raceId"]+agg_list+dffeature.columns.tolist()[len(modelist):] +
               ["last3F_vel", "toL3F_vel"]]
dfc = dfcheck.groupby("raceId")[
    ["last3F_vel", "toL3F_vel"]].mean().reset_index()
dfcheck = pd.merge(dfc, dfcheck.drop_duplicates(
    "raceId", ignore_index=True)[["raceId"]+agg_list+dffeature.columns.tolist()[-len(modelist):]],
    on="raceId", how="left")

dfcagg = dfcheck.groupby(agg_list)[
    ["last3F_vel", "toL3F_vel"] +
    dffeature.columns[-len(modelist):].tolist()].corr()[
        ["last3F_vel", "toL3F_vel"]].reset_index(level=len(agg_list), names=[0, 0, "mode"])
display(dfcagg[dfcagg["mode"].isin(dffeature.columns[-len(modelist):])
               ].loc["芝"].loc[[s for s in "SMILE"]].T)
display(dfcagg[dfcagg["mode"].isin(dffeature.columns[-len(modelist):])
               ].loc["ダ"].loc[[s for s in "SMIL"]].T)
dist_cat S S S S M M M M I I I I L L L L E E E E
mode ALL逃げ ALL先行 ALL追込 ALL差し ALL逃げ ALL先行 ALL追込 ALL差し ALL逃げ ALL先行 ALL追込 ALL差し ALL逃げ ALL先行 ALL追込 ALL差し ALL逃げ ALL先行 ALL追込 ALL差し
last3F_vel 0.261902 0.247019 0.246711 0.238991 0.175259 0.191068 0.177675 0.174538 0.106486 0.130416 0.104578 0.090673 0.058324 0.076681 0.0714 0.063912 -0.138959 -0.083822 -0.048007 0.018372
toL3F_vel 0.114912 0.083135 0.057759 0.068758 0.185017 0.1282 0.068506 0.091219 0.143761 0.10382 0.055048 0.0765 0.127299 0.091104 0.055876 0.078228 0.177726 0.120185 -0.002268 0.002295
dist_cat S S S S M M M M I I I I L L L L
mode ALL逃げ ALL先行 ALL追込 ALL差し ALL逃げ ALL先行 ALL追込 ALL差し ALL逃げ ALL先行 ALL追込 ALL差し ALL逃げ ALL先行 ALL追込 ALL差し
last3F_vel 0.203459 0.168425 0.141095 0.125438 0.177574 0.118702 0.087341 0.085897 -0.07525 -0.080099 -0.06143 -0.05057 -0.123317 -0.050925 -0.050657 -0.081938
toL3F_vel 0.198653 0.121691 0.093205 0.104436 0.195155 0.101723 0.037013 0.057296 0.216384 0.090145 0.005541 0.048202 0.096596 0.035073 0.042731 0.010028

ランク学習の実行

In [19]:
# ラベル作成
clsnames2_map = {col: num for num, col in enumerate(modelist)}
dffl["clsLabel"] = dffl[mode].map(clsnames2_map)
dffl.sort_values("raceDate", inplace=True, ignore_index=True)
for col in ["clsLabel", "last3F_vel", "toL3F_vel"]:
    for num in range(1, 11):
        dffl[f"{col}_lag{num}"] = dffl.groupby(
            "horseId")[col].shift(num).fillna(len(clsnames2_map) if col == "clsLabel" else 100)

dffl["raceGrade"] = dffl["raceGrade"].astype(int)
dffl[f"raceGrade_diff{1}"] = dffl.groupby(
    "horseId")["raceGrade"].shift(1).fillna(len(clsnames2_map))

cat_list = [
    "horseId", 'jockeyId', "field", "place", "dist_cat",
    "condition", "breedId", "bStallionId", "b2StallionId", "stallionId",
    "clsLabel_lag1", "clsLabel_lag2", "clsLabel_lag3", "clsLabel_lag4",
    "clsLabel_lag5", "clsLabel_lag6", "clsLabel_lag7",
    "clsLabel_lag8", "clsLabel_lag9", "clsLabel_lag10",
]

for col in cat_list:
    dffl[col] = dffl[col].astype("category")

# データセットの作成
dffl = dffl[~dffl["raceDetail"].str.contains(r"新馬|未出走", regex=True)]
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[1:]], label=len(clsnames2_map)-dftrain["clsLabel"],
    group=dftrain.groupby("raceId")["raceId"].count().values)
valid_data = lgbm.Dataset(
    dfvalid[feature_columns[1:]], label=len(clsnames2_map)-dfvalid["clsLabel"],
    group=dfvalid.groupby("raceId")["raceId"].count().values)
test_data = lgbm.Dataset(dftest[feature_columns[1:]], label=len(clsnames2_map)-dftest["clsLabel"],
                         group=dftest.groupby("raceId")["raceId"].count().values)

# ハイパーパラメータ
params = {
    'objective': 'lambdarank',
    'metric': 'ndcg',
    "categorical_feature": cat_list,
    'ndcg_eval_at': [1, 2, 5, 6, 9, 10, 13, 14],
    'boosting_type': 'gbdt',
    'seed': 77777,
}
# モデル学習
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=horseId,jockeyId,field,place,dist_cat,condition,breedId,bStallionId,b2StallionId,stallionId,clsLabel_lag1,clsLabel_lag2,clsLabel_lag3,clsLabel_lag4,clsLabel_lag5,clsLabel_lag6,clsLabel_lag7,clsLabel_lag8,clsLabel_lag9,clsLabel_lag10, categorical_column=0,1,2,3,4,5,7,8,9,10,11,12,13,14,15,16,17,18 will be ignored. Current value: categorical_feature=horseId,jockeyId,field,place,dist_cat,condition,breedId,bStallionId,b2StallionId,stallionId,clsLabel_lag1,clsLabel_lag2,clsLabel_lag3,
[LightGBM] [Info] Auto-choosing row-wise multi-threading, the overhead of testing was 0.053087 seconds.
You can set `force_row_wise=true` to remove the overhead.
And if memory is not enough, you can set `force_col_wise=true`.
[LightGBM] [Info] Total Bins 24642
[LightGBM] [Info] Number of data points in the train set: 863964, number of used features: 35
Training until validation scores don't improve for 50 rounds
[25]	training's ndcg@1: 0.843891	training's ndcg@2: 0.803822	training's ndcg@5: 0.77251	training's ndcg@6: 0.782315	training's ndcg@9: 0.82669	training's ndcg@10: 0.841891	training's ndcg@13: 0.881093	training's ndcg@14: 0.890835	valid_1's ndcg@1: 0.791929	valid_1's ndcg@2: 0.767522	valid_1's ndcg@5: 0.756387	valid_1's ndcg@6: 0.769228	valid_1's ndcg@9: 0.819869	valid_1's ndcg@10: 0.834516	valid_1's ndcg@13: 0.872589	valid_1's ndcg@14: 0.881679
[50]	training's ndcg@1: 0.876644	training's ndcg@2: 0.830328	training's ndcg@5: 0.789127	training's ndcg@6: 0.797255	training's ndcg@9: 0.838379	training's ndcg@10: 0.853031	training's ndcg@13: 0.890935	training's ndcg@14: 0.900257	valid_1's ndcg@1: 0.796639	valid_1's ndcg@2: 0.773315	valid_1's ndcg@5: 0.762127	valid_1's ndcg@6: 0.774117	valid_1's ndcg@9: 0.824116	valid_1's ndcg@10: 0.838998	valid_1's ndcg@13: 0.87561	valid_1's ndcg@14: 0.884699
[75]	training's ndcg@1: 0.897311	training's ndcg@2: 0.848846	training's ndcg@5: 0.800598	training's ndcg@6: 0.807356	training's ndcg@9: 0.846625	training's ndcg@10: 0.860812	training's ndcg@13: 0.89759	training's ndcg@14: 0.906719	valid_1's ndcg@1: 0.801261	valid_1's ndcg@2: 0.777251	valid_1's ndcg@5: 0.76557	valid_1's ndcg@6: 0.777975	valid_1's ndcg@9: 0.826394	valid_1's ndcg@10: 0.841601	valid_1's ndcg@13: 0.877868	valid_1's ndcg@14: 0.886542
[100]	training's ndcg@1: 0.909757	training's ndcg@2: 0.861211	training's ndcg@5: 0.808605	training's ndcg@6: 0.814481	training's ndcg@9: 0.852163	training's ndcg@10: 0.866117	training's ndcg@13: 0.902053	training's ndcg@14: 0.910991	valid_1's ndcg@1: 0.805229	valid_1's ndcg@2: 0.780408	valid_1's ndcg@5: 0.767036	valid_1's ndcg@6: 0.778882	valid_1's ndcg@9: 0.827725	valid_1's ndcg@10: 0.843028	valid_1's ndcg@13: 0.878744	valid_1's ndcg@14: 0.887548
[125]	training's ndcg@1: 0.917983	training's ndcg@2: 0.869941	training's ndcg@5: 0.814615	training's ndcg@6: 0.81961	training's ndcg@9: 0.856238	training's ndcg@10: 0.869888	training's ndcg@13: 0.905259	training's ndcg@14: 0.914071	valid_1's ndcg@1: 0.803927	valid_1's ndcg@2: 0.778637	valid_1's ndcg@5: 0.765896	valid_1's ndcg@6: 0.779012	valid_1's ndcg@9: 0.827867	valid_1's ndcg@10: 0.842825	valid_1's ndcg@13: 0.878667	valid_1's ndcg@14: 0.887545
[150]	training's ndcg@1: 0.924424	training's ndcg@2: 0.877224	training's ndcg@5: 0.819484	training's ndcg@6: 0.824108	training's ndcg@9: 0.859524	training's ndcg@10: 0.872959	training's ndcg@13: 0.907906	training's ndcg@14: 0.916558	valid_1's ndcg@1: 0.802368	valid_1's ndcg@2: 0.777627	valid_1's ndcg@5: 0.765502	valid_1's ndcg@6: 0.778659	valid_1's ndcg@9: 0.827456	valid_1's ndcg@10: 0.842543	valid_1's ndcg@13: 0.878128	valid_1's ndcg@14: 0.887114
Early stopping, best iteration is:
[102]	training's ndcg@1: 0.91067	training's ndcg@2: 0.861983	training's ndcg@5: 0.809186	training's ndcg@6: 0.814931	training's ndcg@9: 0.852512	training's ndcg@10: 0.866482	training's ndcg@13: 0.902359	training's ndcg@14: 0.91129	valid_1's ndcg@1: 0.80809	valid_1's ndcg@2: 0.781693	valid_1's ndcg@5: 0.767534	valid_1's ndcg@6: 0.77931	valid_1's ndcg@9: 0.828219	valid_1's ndcg@10: 0.84344	valid_1's ndcg@13: 0.879178	valid_1's ndcg@14: 0.887915

学習が完了したがモデルの予測結果は分類モデルと違い、グループごとの大小関係しか出さない
そのため、予測結果からランク付けを改めて行わなければならない。

ランク付け=脚質の振り分けと同じ
学習では「逃げ」>「先行」>「差し」>「追込」といった大小関係を学習させていたので、モデルの予測結果が高いものであればあるほど「逃げ」に近い脚質になることを期待し、逆であれば「追込」に近い脚質になることを期待するように予測しているということである。

ランク付けの方針は

  1. レースごとに予測確度の最大値と最小値を計算
  2. 学習データの方で割り出したクラスごとの割合を用いて割り振る

詳細はcalc_pred関数を参照

In [20]:
dftrain["pred_proba"] = model.predict(
    dftrain[feature_columns[1:]], num_iteration=model.best_iteration)
In [21]:
q_map = (dftrain[mode].value_counts() /
         dftrain[mode].count()).sort_values().values.cumsum()[:-1]
q_map
Out[21]:
array([0.20616947, 0.41852786, 0.67257664])
In [22]:
def calc_pred(df1, q_map=q_map):
    df1["pred"] = len(clsnames2_map)-1

    df1["pred_max"] = df1["raceId"].map(df1.groupby(
        "raceId")["pred_proba"].max().to_dict())
    df1["pred_min"] = df1["raceId"].map(df1.groupby(
        "raceId")["pred_proba"].min().to_dict())
    for q in q_map:
        df1["pred_q"] = df1["pred_min"] + (df1["pred_max"]-df1["pred_min"])*q
        df1["pred"] -= (df1["pred_proba"] > df1["pred_q"]).astype(int)

    return df1
In [23]:
df1 = dftest
df1["pred_proba"] = model.predict(
    df1[feature_columns[1:]], num_iteration=model.best_iteration)
df1 = calc_pred(df1)
raceId = np.random.choice(df1["raceId"])

df1[df1["raceId"].isin([raceId])][[
    "raceId", "field", "place", "dist_cat",
    "favorite", "odds", "label", "pred",
    "pred_proba", "clsLabel",
    "label_lastC", "label_1C",
]+dffeature.columns.tolist()[2:]]
Out[23]:
raceId field place dist_cat favorite odds label pred pred_proba clsLabel label_lastC label_1C 逃げ 先行 追込 差し ALL逃げ ALL先行 ALL追込 ALL差し
1003701 202105020212 東京 M 4 5.4 2 0 0.522640 0 4.0 4 0.833333 0.000000 0.000000 0.166667 0.456897 0.224138 0.181034 0.137931
1003702 202105020212 東京 M 6 17.0 3 0 1.105371 0 2.0 2 1.000000 0.000000 0.000000 0.000000 0.456897 0.224138 0.181034 0.137931
1003705 202105020212 東京 M 14 56.2 10 0 0.612269 0 2.0 2 0.800000 0.200000 0.000000 0.000000 0.456897 0.224138 0.181034 0.137931
1003711 202105020212 東京 M 15 181.8 16 1 -0.720992 0 1.0 1 0.250000 0.750000 0.000000 0.000000 0.456897 0.224138 0.181034 0.137931
1003713 202105020212 東京 M 8 19.0 12 0 -0.058157 1 7.0 4 0.600000 0.000000 0.000000 0.400000 0.456897 0.224138 0.181034 0.137931
1003715 202105020212 東京 M 3 5.4 5 0 0.353259 2 10.0 10 0.666667 0.333333 0.000000 0.000000 0.456897 0.224138 0.181034 0.137931
1003716 202105020212 東京 M 12 51.5 13 2 -1.158555 3 13.0 12 0.300000 0.300000 0.200000 0.200000 0.456897 0.224138 0.181034 0.137931
1003717 202105020212 東京 M 7 18.8 4 1 -0.658060 0 4.0 4 0.222222 0.444444 0.222222 0.111111 0.456897 0.224138 0.181034 0.137931
1003718 202105020212 東京 M 16 197.4 11 3 -1.986400 2 10.0 10 0.000000 0.400000 0.300000 0.300000 0.456897 0.224138 0.181034 0.137931
1003719 202105020212 東京 M 1 3.8 14 0 0.772138 0 4.0 4 1.000000 0.000000 0.000000 0.000000 0.456897 0.224138 0.181034 0.137931
1003720 202105020212 東京 M 11 47.4 7 0 0.755578 1 8.0 8 0.833333 0.166667 0.000000 0.000000 0.456897 0.224138 0.181034 0.137931
1003721 202105020212 東京 M 13 54.0 9 2 -1.263945 3 13.0 12 0.400000 0.300000 0.200000 0.100000 0.456897 0.224138 0.181034 0.137931
1003722 202105020212 東京 M 5 9.1 15 0 0.644181 1 8.0 8 0.666667 0.166667 0.000000 0.166667 0.456897 0.224138 0.181034 0.137931
1003723 202105020212 東京 M 2 5.3 8 3 -2.719537 3 15.0 16 0.000000 0.000000 1.000000 0.000000 0.456897 0.224138 0.181034 0.137931
1003724 202105020212 東京 M 10 34.1 1 2 -1.744182 3 15.0 15 0.000000 0.000000 0.000000 0.000000 0.456897 0.224138 0.181034 0.137931
1003728 202105020212 東京 M 9 21.8 6 2 -1.733182 2 10.0 12 0.200000 0.300000 0.200000 0.300000 0.456897 0.224138 0.181034 0.137931
In [24]:
dfvalid["pred_proba"] = model.predict(
    dfvalid[feature_columns[1:]], num_iteration=model.best_iteration)
dfvalid = calc_pred(dfvalid)
In [25]:
dftest["pred_proba"] = model.predict(
    dftest[feature_columns[1:]], num_iteration=model.best_iteration)
dftest = calc_pred(dftest)

pd.concat([dftest["clsLabel"].value_counts().rename("真値").sort_index().to_frame().T,
           dftest["pred"].value_counts().rename("予測値").sort_index().to_frame().T])
Out[25]:
0 1 2 3
真値 13649 10800 8617 9035
予測値 12474 10727 8777 10123
In [26]:
dft = dftest  # [(dftest["pred_proba"] > 1/len(clsnames2_map))]
idfheat = pd.concat([(dft[["clsLabel", "pred"]].groupby("pred")["clsLabel"].value_counts(
)/dft[["clsLabel", "pred"]].groupby("pred")["clsLabel"].count()).loc[num]
    for num in range(len(clsnames2_map))], axis=1).sort_index()
idfheat
Out[26]:
0 1 2 3
clsLabel
0 0.582492 0.338678 0.200752 0.097600
1 0.231602 0.311923 0.294178 0.195891
2 0.116242 0.209565 0.259770 0.260693
3 0.069665 0.139834 0.245300 0.445816
In [27]:
pd.concat([(dft[["label", "pred"]].groupby("pred")["label"].value_counts(
)/dft[["label", "pred"]].groupby("pred")["label"].count()).loc[num]
    for num in range(len(clsnames2_map))], axis=1).sort_index().head(10)
Out[27]:
0 1 2 3
label
1 0.115200 0.073087 0.052979 0.034871
2 0.103736 0.076816 0.059132 0.038329
3 0.090749 0.078494 0.065512 0.047614
4 0.083854 0.079426 0.066651 0.054628
5 0.074074 0.075697 0.073032 0.064408
6 0.069024 0.077095 0.072918 0.069149
7 0.062690 0.071409 0.079070 0.077151
8 0.057800 0.071688 0.078273 0.079226
9 0.056999 0.067773 0.075197 0.076361
10 0.052349 0.063764 0.070411 0.077941
In [28]:
idfg = dfvalid

print("検証データ")
for name, i in clsnames2_map.items():
    idft = idfg[idfg["pred"].isin([i])]
    idft1 = pd.merge(idft[["raceId", "horseId", "pred", "pred_proba"]],
                     idfAll, on=["raceId", "horseId"], how="inner")
    idft1 = idft1[~idft1["raceGrade"].isin([0, 1, 2, 3, 4, 5])]
    print(
        f'Cluster: {i}-{name},\t{round(idft1[idft1["label"].isin([1])]["odds"].sum(),1)},' +
        f'\t{len(idft1)},\t{idft1["label"].isin([1]).sum()},' +
        f'\t{round(idft1["label"].isin([1,]).mean(), 3)},' +
        f'\t{round(idft1[idft1["label"].isin([1])]["odds"].sum()*100/len(idft1), 2)}'
    )
検証データ
Cluster: 0-逃げ,	475.5,	567,	46,	0.081,	83.86
Cluster: 1-先行,	826.5,	503,	41,	0.082,	164.31
Cluster: 2-差し,	255.9,	390,	27,	0.069,	65.62
Cluster: 3-追込,	342.6,	439,	14,	0.032,	78.04
In [29]:
idfg = dftest

print("テストデータ")
for name, i in clsnames2_map.items():
    idft = idfg[idfg["pred"].isin([i])]
    idft1 = pd.merge(idft[["raceId", "horseId", "pred", "pred_proba"]],
                     idfAll, on=["raceId", "horseId"], how="inner")
    idft1 = idft1[~idft1["raceGrade"].isin([0, 1, 2, 3, 4, 5])]
    print(
        f'Cluster: {i}-{name},\t{round(idft1[idft1["label"].isin([1])]["odds"].sum(),1)},' +
        f'\t{len(idft1)},\t{idft1["label"].isin([1]).sum()},' +
        f'\t{round(idft1["label"].isin([1,]).mean(), 3)},' +
        f'\t{round(idft1[idft1["label"].isin([1])]["odds"].sum()*100/len(idft1), 2)}'
    )
テストデータ
Cluster: 0-逃げ,	687.1,	552,	56,	0.101,	124.47
Cluster: 1-先行,	337.0,	531,	36,	0.068,	63.47
Cluster: 2-差し,	165.1,	387,	17,	0.044,	42.66
Cluster: 3-追込,	246.6,	438,	20,	0.046,	56.3

何度か試してほしいけど、シードに関わらず結構結果が変わってしまうようである。

自分の環境で実行したときのある時の結果を以下に出し、以降はこの結果をもとに話す。

検証データ
Cluster: 0-逃げ,  475.5,  567,    46, 0.081,  83.86
Cluster: 1-先行,  826.5,  503,    41, 0.082,  164.31
Cluster: 2-差し,  255.9,  390,    27, 0.069,  65.62
Cluster: 3-追込,  342.6,  439,    14, 0.032,  78.04
テストデータ
Cluster: 0-逃げ,  687.1,  552,    56, 0.101,  124.47
Cluster: 1-先行,  337.0,  531,    36, 0.068,  63.47
Cluster: 2-差し,  165.1,  387,    17, 0.044,  42.66
Cluster: 3-追込,  246.6,  438,    20, 0.046,  56.3

検証データとテストデータで結果がバラバラ、一貫性がない、これでは意味がない。
ただ、脚質を予想すれば的中率を上げられるヒントがあるかもしれない。

最後に重要度を確認して締めくくる

In [30]:
pd.DataFrame(model.feature_importance(
    "gain").tolist(), index=feature_columns[1:]).round(2).sort_values(0)
Out[30]:
0
ALL差し 0.00
ALL先行 0.00
差し 0.00
追込 0.00
先行 0.00
place 0.00
condition 0.00
last3F_vel_lag3 0.00
ALL追込 15.90
dist_cat 38.18
field 87.81
last3F_vel_lag2 171.21
toL3F_vel_lag3 372.45
toL3F_vel_lag2 799.25
clsLabel_lag10 807.78
clsLabel_lag9 1180.73
逃げ 1277.10
raceGrade_diff1 1366.99
clsLabel_lag8 1717.88
ALL逃げ 1757.12
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

コメント

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