PR

血統から競走馬の成長度合いを分析する方法

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

2.重賞レースと血統の関係

 

G1などの重賞レースで好成績を残している競走馬は、種牡馬や繫殖牝馬として次の世代の競走馬を輩出している。
その親の血統によっては、より勝ちやすい血統が存在しているのかどうか確認してみたい。

確認の方針としては、過去数年~十数年の範囲で産駒が重賞レースを制覇した競走馬の中で、
種牡馬や母父の血統によって重賞クラスに勝利するまでの期間に違いがあるかを確認してみる

スポンサーリンク

2-0.下準備

2-0-1.必要そうなものをインポート

In [1]:
import random
from wordcloud import WordCloud
import seaborn as sns
import japanize_matplotlib

import matplotlib.pyplot as plt
import re

from typing import Literal
import pathlib


import warnings
import sys
import pandas as pd
sys.path.append("..")

from src.core.db.controller import execSQL, getTableList, getDataFrame  # noqa
from src.data_manager.preprocess_tools import DataPreProcessor  # noqa
from src.data_manager.data_loader import DataLoader  # noqa


warnings.filterwarnings("ignore")

2-0-2.血統データをDBから取得

 

2-0-2-1.接続先DB情報

In [2]:
# 接続先DB (このnotebookの場所が「notebook」フォルダにあるので一つ上の階層に戻ってDBファイルパスを生成)
root = pathlib.Path(".").absolute().parent
dbpath = root / "data" / "keibadata.db"
dbpath
Out[2]:
WindowsPath('e:/dev_um_ai/dev-um-ai/data/keibadata.db')
 

2-0-2-2.DBから血統情報のテーブル一覧を取得

In [3]:
# 血統情報が入っているテーブルは「horseblood」という接頭辞がついたテーブルなので、その一覧を取得
horseblood_list = [tbl for tbl in getTableList(dbpath) if "horseblood" in tbl]
horseblood_list.sort(key=lambda x: int(x[-4:]))
horseblood_list
Out[3]:
['horseblood1986',
 'horseblood1987',
 'horseblood1988',
 'horseblood1989',
 'horseblood1990',
 'horseblood1991',
 'horseblood1992',
 'horseblood1993',
 'horseblood1994',
 'horseblood1995',
 'horseblood1996',
 'horseblood1997',
 'horseblood1998',
 'horseblood1999',
 'horseblood2000',
 'horseblood2001',
 'horseblood2002',
 'horseblood2003',
 'horseblood2004',
 'horseblood2005',
 'horseblood2006',
 'horseblood2007',
 'horseblood2008',
 'horseblood2009',
 'horseblood2010',
 'horseblood2011',
 'horseblood2012',
 'horseblood2013',
 'horseblood2014',
 'horseblood2015',
 'horseblood2016',
 'horseblood2017',
 'horseblood2018',
 'horseblood2019',
 'horseblood2020',
 'horseblood2021']
 

2-0-2-3.テーブル一覧から5代血統情報をDataFrameに変換

In [4]:
# DBから取得:concatでhorseblood_listにあるテーブル情報のDataFrameをすべて結合する
dfblood = pd.concat(
    [getDataFrame(tbl, dbpath) for tbl in horseblood_list], ignore_index=True)
dfblood
Out[4]:
  horseId gen1 gen2 gen3 gen4 gen5 gen1ID gen2ID gen3ID gen4ID gen5ID
0 1986102130 ジュニアス\nJunius(米)\n Raja Baba\n\n Bold Ruler\n\n Nasrullah Nearco 000a0001af 000a0010fa 000a000e94 000a000f88 000a000f8b
1 1986102130 ジュニアス\nJunius(米)\n Raja Baba\n\n Bold Ruler\n\n Nasrullah Mumtaz Begum 000a0001af 000a0010fa 000a000e94 000a000f88 000a007851
2 1986102130 ジュニアス\nJunius(米)\n Raja Baba\n\n Bold Ruler\n\n Miss Disco Discovery 000a0001af 000a0010fa 000a000e94 000a00780d 000a0013fd
3 1986102130 ジュニアス\nJunius(米)\n Raja Baba\n\n Bold Ruler\n\n Miss Disco Outdone 000a0001af 000a0010fa 000a000e94 000a00780d 000a0078d1
4 1986102130 ジュニアス\nJunius(米)\n Raja Baba\n\n Missy Baba\n\n My Babu Djebel 000a0001af 000a0010fa 000a007cd4 000a000f85 000a000ede
3727867 2021105244 マンハッタンセレブ\n\n サトルチェンジ\nSubtle Change(愛)\n Law Society\n\n Bold Bikini Ran-Tan 2003102531 000a000334 000a0016ec 000a0083ad 000a0083ac
3727868 2021105244 マンハッタンセレブ\n\n サトルチェンジ\nSubtle Change(愛)\n Santa Luciana\n\n Luciano Henry the Seventh 2003102531 000a000334 000a0002d8 000a001347 000a000e22
3727869 2021105244 マンハッタンセレブ\n\n サトルチェンジ\nSubtle Change(愛)\n Santa Luciana\n\n Luciano Light Arctic 2003102531 000a000334 000a0002d8 000a001347 000a00838d
3727870 2021105244 マンハッタンセレブ\n\n サトルチェンジ\nSubtle Change(愛)\n Santa Luciana\n\n Suleika Ticino 2003102531 000a000334 000a0002d8 000a00807e 000a001239
3727871 2021105244 マンハッタンセレブ\n\n サトルチェンジ\nSubtle Change(愛)\n Santa Luciana\n\n Suleika Schwarzblaurot 2003102531 000a000334 000a0002d8 000a00807e 000a00807f

3727872 rows × 11 columns

2-0-3.2000年から2023年の出走情報を取得

 

2-0-3-1.ベース前処理の実行

In [5]:
start_year = 2000
end_year = 2023

data_loader = DataLoader(start_year, end_year, dbpath=dbpath)
dataPreP = DataPreProcessor()
df = data_loader.load_racedata()
df = dataPreP.exec_pipeline(df)
 
2024-07-26 08:54:44.431 | INFO     | src.data_manager.data_loader:load_racedata:23 - Get Year Range: 2000 -> 2023.
2024-07-26 08:54:44.432 | INFO     | src.data_manager.data_loader:load_racedata:24 - Loading Race Info ...
2024-07-26 08:54:46.434 | INFO     | src.data_manager.data_loader:load_racedata:26 - Loading Race Data ...
2024-07-26 08:55:04.040 | INFO     | src.data_manager.data_loader:load_racedata:28 - Merging Race Info and Race Data ...
 
2024-07-26 08:55:06.359 | INFO     | src.data_manager.preprocess_tools:__0_check_use_save_checkpoints:34 - Start PreProcess #0 ...
2024-07-26 08:55:06.364 | INFO     | src.data_manager.preprocess_tools:__1_exec_all_sub_prep1:37 - Start PreProcess #1 ...
2024-07-26 08:55:12.689 | INFO     | src.data_manager.preprocess_tools:__2_exec_all_sub_prep2:39 - Start PreProcess #2 ...
2024-07-26 08:55:25.719 | INFO     | src.data_manager.preprocess_tools:__3_convert_type_str_to_number:41 - Start PreProcess #3 ...
2024-07-26 08:55:29.376 | INFO     | src.data_manager.preprocess_tools:__4_drop_or_fillin_none_data:43 - Start PreProcess #4 ...
2024-07-26 08:55:33.087 | INFO     | src.data_manager.preprocess_tools:__5_exec_all_sub_prep5:45 - Start PreProcess #5 ...
2024-07-26 08:55:55.764 | INFO     | src.data_manager.preprocess_tools:__6_convert_label_to_rate_info:47 - Start PreProcess #6 ...
2024-07-26 08:56:06.591 | INFO     | src.data_manager.preprocess_tools:__7_convert_distance_to_smile:49 - Start PreProcess #7 ...
2024-07-26 08:56:06.835 | INFO     | src.data_manager.preprocess_tools:__8_category_encoding:51 - Start PreProcess #8 ...
 

以上で準備完了

2-0-4.パート1の内容: レースのクラスのグレード分けを行う

 

前回の内容から以下のマッピングをそのまま流用して、グレード分けを行う

前回記事

In [6]:
# それぞれのマッピングを作っておく
def map_race_grade(data: str):
    if re.search(r"\(G1\)", data) or re.search(r"\(GI\)", data):
        return 7
    if re.search(r"\(G2\)", data) or re.search(r"\(GII\)", data):
        return 6
    if re.search(r"\(G3\)", data) or re.search(r"\(GIII\)", data):
        return 5
    return None


grade_mapping_dict = {
    '4歳未勝利': 0, '4歳新馬': 0, '4歳以上500万下': 1, '4歳500万下': 1, '4歳以上900万下': 2, '4歳以上オープン': 4,
    '4歳以上1600万下': 3, '4歳オープン': 4, '4歳未出走': 0, '5歳以上オープン': 4, '4歳900万下': 2, '3歳新馬': 0,
    '3歳未勝利': 0, '3歳オープン': 4, '3歳500万下': 1, '3歳未出走': 0, '3歳以上オープン': 4, '3歳900万下': 2,
    '2歳新馬': 0, '3歳以上500万下': 1, '3歳以上1000万下': 2, '2歳未勝利': 0, '2歳オープン': 4, '3歳以上1600万下': 3,
    '2歳500万下': 1, '4歳以上1000万下': 2, '3歳1000万下': 2, '3歳以上1勝クラス': 1, '3歳以上2勝クラス': 2,
    '3歳以上3勝クラス': 3, '2歳1勝クラス': 1, '3歳1勝クラス': 1, '4歳以上1勝クラス': 1, '4歳以上2勝クラス': 2,
    '4歳以上3勝クラス': 3
}

# クラスをグレードに置き換える
idf = df[df["raceName"].map(map_race_grade).notna()
         ]["raceName"].map(map_race_grade)
df["raceGrade"] = pd.concat(
    [idf, df[~df.index.isin(idf.index)]["raceDetail"].map(grade_mapping_dict)])

2-0-5.パート1の内容: 種牡馬と繁殖牝馬の種牡馬情報を追加する

 

前回の内容から出走情報に父と母父、そして追加で、母母父と父父そして父父父の情報も追加する

In [7]:
def add_blood_info_to_df(df: pd.DataFrame, mode: Literal["s", "ss", "sb", "b", "bs", "bb", "sss", "ssb", "sbs", "sbb", "bss", "bsb", "bbs", "bbb"]):
    index_map = {
        "s": (0, "stallion", "gen1"), "ss": (0, "sStallion", "gen2"), "sb": (8, "sBreed", "gen2"),
        "sss": (0, "s2Stallion", "gen3"), "ssb": (4, "s2Breed", "gen3"), "sbs": (8, "sbStallion", "gen3"), "sbb": (12, "sbBreed", "gen3"),
        "b": (16, "breed", "gen1"), "bs": (16, "bStallion", "gen2"), "bb": (24, "bBreed", "gen2"),
        "bss": (16, "bsStallion", "gen3"), "bsb": (20, "bsBreed", "gen3"), "bbs": (24, "b2Stallion", "gen3"), "bbb": (28, "b2Breed", "gen3"),
    }
    idx, prefix, genCol = index_map[mode]
    idf = dfblood.iloc[idx::32].reset_index(drop=True)
    idf[genCol] = idf[genCol].str.split("\n", expand=True)[
        0].str.replace("\n", "")
    df[f"{prefix}Id"] = df["horseId"].map(
        idf.set_index("horseId")[f"{genCol}ID"])
    df[f"{prefix}Name"] = df["horseId"].map(idf.set_index("horseId")[genCol])
    return df, idf[["horseId", genCol, f"{genCol}ID"]].rename(columns={genCol: "GenName", f"{genCol}ID": "GenID"})


df, dfstallion = add_blood_info_to_df(df, "s")
df, dfsStallion = add_blood_info_to_df(df, "ss")
df, dfbStallion = add_blood_info_to_df(df, "bs")
df, dfs2Stallion = add_blood_info_to_df(df, "sss")
df, dfb2Stallion = add_blood_info_to_df(df, "bbs")
df[["horseId", "stallionId", "stallionName", "sStallionId", "sStallionName",
    "s2StallionId", "s2StallionName", "bStallionId", "bStallionName"]]
Out[7]:
  horseId stallionId stallionName sStallionId sStallionName s2StallionId s2StallionName bStallionId bStallionName
0 1997100761 000a000d4b エブロス 000a001607 Mr. Prospector 000a000e46 Raise a Native 000a000081 ブレイヴェストローマン
1 1997100656 000a000013 アフリート 000a001607 Mr. Prospector 000a000e46 Raise a Native 000a000d2e ストームオンザルース
2 1997100203 1982101222 サクラユタカオー 000a000355 テスコボーイ 000a000fc8 Princely Gift 000a000258 ノーザンテースト
3 1997104609 000a000d87 ロイヤルアカデミーII 000a000dfe Nijinsky 000a000e04 Northern Dancer 000a0001ff マナード
4 1997106623 1988106402 カリスタグローリ 000a000081 ブレイヴェストローマン 000a000e03 Never Bend 000a00010d エンペリー
1126068 2019103898 000a012056 Siyouni 000a002328 Pivotal 000a001f3f Polar Falcon 000a014559 Solon
1126069 2019106102 2010105827 キズナ 2002100816 ディープインパクト 000a00033a サンデーサイレンス 000a00fa35 ウォーエンブレム
1126070 2017102603 2008103552 ロードカナロア 2001103460 キングカメハメハ 000a001d7e Kingmambo 1989109110 パラダイスクリーク
1126071 2019100653 2011104063 サトノアラジン 2002100816 ディープインパクト 000a00033a サンデーサイレンス 2001103114 ダイワメジャー
1126072 2018103205 000a0113a1 ディスクリートキャット 000a002270 Forestry 000a001a98 Storm Cat 2000101426 ネオユニヴァース

1126073 rows × 9 columns

 

以上で前回分で分析した情報の追加完了

スポンサーリンク

2-1.そもそも重賞レースってどのくらい凄いの?

 

レースのグレードごとに出走する競走馬の数をカウントすれば、どれだけ重賞レースに出走できる競走馬数が少ないのかが分かる
出し方は、raceGradeカラムでgroupbyしてhorseIdカラムに対してユニーク数を出す

In [8]:
df.groupby("raceGrade")["horseId"].nunique().rename(
    index=lambda x: f"Grade {x}")
Out[8]:
raceGrade
Grade 0    106535
Grade 1     49977
Grade 2     20513
Grade 3      8851
Grade 4     10991
Grade 5      8456
Grade 6      5304
Grade 7      3653
Name: horseId, dtype: int64
 

おかしい、ピラミッド形式になっているはず・・・
もう少し詳しく年度別に見てみる

In [9]:
idf = df.copy()
idf["year"] = df["raceDate"].dt.year
idfg = pd.concat(
    [
        idfg.groupby("year")[["horseId"]].nunique().rename(
            columns={"horseId": f"Grade {col}"}) for col, idfg in idf.groupby("raceGrade")
    ] + [idf.groupby("year")[["horseId"]].nunique().rename(
        columns={"horseId": f"Grade ALL"})], axis=1
)
idfg["G race"] = idfg[["Grade 5", "Grade 6", "Grade 7"]].sum(axis=1)
idfg["G race rate"] = (100*idfg["G race"]/idfg["Grade ALL"]).round(1)
display(idfg)
 
  Grade 0 Grade 1 Grade 2 Grade 3 Grade 4 Grade 5 Grade 6 Grade 7 Grade ALL G race G race rate
year                      
2000 4614 3797 1382 591 672 564 314 234 8168 1112 13.6
2001 4883 4159 1503 500 689 561 298 228 8754 1087 12.4
2002 5089 4065 1540 473 751 553 325 226 9145 1104 12.1
2003 5362 4123 1597 486 757 581 311 238 9585 1130 11.8
2004 5543 4208 1610 437 743 562 324 242 9855 1128 11.4
2005 5692 4171 1553 451 702 574 312 233 10006 1119 11.2
2006 5867 4247 1672 516 734 574 323 227 10230 1124 11.0
2007 5939 4245 1760 639 690 573 351 243 10249 1167 11.4
2008 6218 4173 1791 710 758 591 371 241 10709 1203 11.2
2009 6209 4160 1780 713 760 600 365 253 10710 1218 11.4
2010 6301 4193 1745 724 763 618 351 252 10790 1221 11.3
2011 6385 4110 1776 715 757 619 361 257 10802 1237 11.5
2012 6505 4045 1749 702 765 611 354 249 10809 1214 11.2
2013 6461 4064 1757 718 766 629 369 256 10801 1254 11.6
2014 6598 3882 1686 719 761 628 384 239 10772 1251 11.6
2015 6594 3887 1734 737 788 611 389 240 10868 1240 11.4
2016 6689 3905 1739 714 735 638 354 246 10921 1238 11.3
2017 6711 3940 1721 697 725 635 348 265 11000 1248 11.3
2018 6786 4022 1687 704 724 611 339 299 11142 1249 11.2
2019 6967 3772 1766 764 727 644 339 262 11255 1245 11.1
2020 7061 3522 1850 864 838 634 372 249 11407 1255 11.0
2021 6977 3441 1824 903 855 649 392 267 11287 1308 11.6
2022 6927 3470 1744 906 849 666 390 280 11280 1336 11.8
2023 7021 3518 1777 934 871 674 407 274 11448 1355 11.8
 

こうやって見ると、年間の出走馬数が8000~11500頭いる中、
G1~G3を出走する競走馬は毎年1100~1350頭であり、
全体の大体12%の競走馬が重賞レースを走っている

 

厳密には1年間に同じ競走馬が何度も出走するほか、1着になるとクラスが上がるため
同じ馬を別のクラスで重複しているが、クラスごとの出走馬数を数えていることに注意

 

とはいえ、数字だけだとレースグレード同士の大小関係が見づらいのでヒートマップにする

比較しやすいようにグレードのカラムごとの差分を見ると分かりやすい
そのため、(Grade x) - (Grade x+1)を計算し、
(Grade x) < (Grade x+1)の場合はFalse, (Grade x) >= (Grade x+1)の場合はTrueとする
つまり、グレードが上がっていくたびに出走馬の数が減っていくはずなので、
ヒートマップはすべて真っ白になるはず

In [10]:
sns.heatmap(~(idfg[idfg.columns[:8]].diff(axis=1) > 0),
            annot=True, linewidths=0.01)

plt.show()
 
 

結果をみると、2000年から2018年ではオープンクラスであるGrade 4
3勝クラスであるGrade 3よりも出走馬の数が多くなっているのに、
なぜか2019年以降からはピラミッド形式になっている…

 

どうやら調べると2019年より競馬の出走条件に大きな変更があったようで、
具体的には、2019年から4歳馬の降級制度を廃止したようである。(ソース:https://dir.netkeiba.com/keibamatome/stayhome/2020s_rule_detail.html)

 

つまり、下級のレースで誤って1位になってしまい、
分不相応な階級(つまりグレード)のレースに出走しなければならない場合でも、
4歳になったとて下のグレードのレースに出走することができなくなったとのこと

2018年までは4歳馬になった段階でこれまで獲得してきた賞金が半分になったそう。
そのおかげで下級のレースへ出走できた。つまり、4歳馬以降は古馬と呼ばれており、
2,3歳馬との実力差を埋めるためと競走馬不足を補うための制度だったとのこと
だが、競走馬の生産が盛んになり、競走馬薄の現状もなくなりその必要がなくなったとのこと

 

そうなってくると、オープンクラスの方が3勝クラスよりも出走する
競走馬が多いというのはいささか違和感のある結果である。

スポンサーリンク

2-2.上記とはまた別に見落としがあった

 

レースグレードの分け方に問題があったようだ
以下の分け方の方が正しいとのこと

2-2-1.クラス分けグレード表・改

獲得賞金 クラス グレード
0 新馬, 未勝利 0
500万下 1勝クラス 1
1000万下, 900万下 2勝クラス 2
1600万下 3勝クラス 3
それ以上 オープン 4
リステッド 5
G3 6
G2 7
G1 8
 

というわけで、リステッドレースが何なのか調べる

2-2-2.リステッドクラスの調査

そもそもどこにいるのやら
旧クラス分けグレード表でいうと4以上の中にいるはず

In [11]:
df[df["raceGrade"].isin([4, 5, 6, 7])]["raceDetail"].unique()
Out[11]:
array(['4歳以上オープン', '4歳オープン', '5歳以上オープン', '3歳オープン', '3歳以上オープン', '2歳オープン'],
      dtype=object)
 

ない。
raceDetailカラムにないのかもしれない

旧クラス分けグレード表でいう4の中だけで「raceName」カラムをみてみる

In [12]:
df[df["raceGrade"].isin([4,])]["raceName"].unique()
Out[12]:
array(['ジュニアカップ(OP)', '万葉ステークス(OP)', '紅梅ステークス(OP)', '淀短距離ステークス(OP)',
       'ニューイヤーS(OP)', '若駒ステークス(OP)', 'フローラステークス(OP)', '銀嶺ステークス(OP)',
       'クロッカスステークス(OP)', 'バイオレットS(OP)', '白富士ステークス(OP)', 'エルフィンステークス(OP)',
       'すばるステークス(OP)', 'ヒヤシンスステークス(OP)', 'すみれステークス(OP)', 'オーシャンステークス(OP)',
       '仁川ステークス(OP)', 'アネモネステークス(OP)', '大阪城ステークス(OP)', '若葉ステークス(OP)',
       '東風ステークス(OP)', '伏竜ステークス(OP)', 'コーラルステークス(OP)', 'マーガレットS(OP)',
       '忘れな草賞(OP)', '大阪ーハンブルクC(OP)', 'ベンジャミンS(OP)', '若草ステークス(OP)',
       'エイプリルステークス(OP)', 'オーストラリアT(OP)', 'やまびこステークス(OP)', 'メトロポリタンS(OP)',
       'スイートピーS(OP)', '端午ステークス(OP)', 'プリンシパルS(OP)', 'オアシスステークス(OP)',
       '都大路ステークス(OP)', '葵ステークス(OP)', '栗東ステークス(OP)', '昇竜ステークス(OP)',
       '駒草賞(OP)', 'ブリリアントS(OP)', 'テレビ愛知オープン(OP)', '巴賞(OP)', '菖蒲ステークス(OP)',
       'パラダイスステークス(OP)', '米子ステークス(OP)', 'レインボーステークス(OP)', 'UHB杯(OP)',
       '菩提樹ステークス(OP)', 'ラベンダー賞(OP)', '北九州短距離S(OP)', 'バーデンバーデンC(OP)',
       'KBC杯(OP)', 'マリーンステークス(OP)', '吾妻小富士オープン(OP)', '札幌日刊スポーツ杯(OP)',
       'サマースプリントS(OP)', 'クローバー賞(OP)', 'ダリア賞(OP)', 'フェニックス賞(OP)',
       'オーガストステークス(OP)', '小倉日経オープン(OP)', 'キーンランドカップ(OP)', 'コスモス賞(OP)',
       'ひまわり賞(OP)', '紫苑ステークス(OP)', '札幌日経オープン(OP)', 'ききょうステークス(OP)',
       '芙蓉ステークス(OP)', 'ギャラクシーS(OP)', 'すずらん賞(OP)', '野路菊ステークス(OP)',
       '福島民報杯(OP)', 'ポートアイランドS(OP)', 'アイビーステークス(OP)', 'エニフステークス(OP)',
       'いちょうステークス(OP)', 'もみじステークス(OP)', '福島民友カップ(OP)', 'カシオペアステークス(OP)',
       '京都3歳ステークス(OP)', 'トパーズステークス(OP)', '福島3歳ステークス(OP)', 'オーロカップ(OP)',
       'アンドロメダS(OP)', 'ターコイズステークス(OP)', '師走ステークス(OP)', 'ディセンバーS(OP)',
       'シクラメンステークス(OP)', '報知杯中京3歳S(OP)', 'ベテルギウスS(OP)', 'ホープフルステークス(OP)',
       'カウントダウンS(OP)', '菜の花賞(OP)', '京葉ステークス(OP)', '欅ステークス(OP)',
       '大沼ステークス(OP)', '灘ステークス(OP)', '朱鷺ステークス(OP)', '北陸ステークス(OP)',
       'NSTオープン(OP)', '関越ステークス(OP)', '阿蘇ステークス(OP)', 'BSN賞(OP)',
       'ペルセウスステークス(OP)', '秋野ステークス(OP)', 'オパールS(OP)', '京都2歳ステークス(OP)',
       'ドンカスターS(OP)', '福島2歳ステークス(OP)', '霜月ステークス(OP)', 'キャピタルステークス(OP)',
       '春待月ステークス(OP)', '摩耶ステークス(OP)', '報知杯中京2歳S(OP)', '2001ファイナルS(OP)',
       '橘ステークス(OP)', '福島テレビオープン(OP)', 'マリーゴールド賞(OP)', 'カンナステークス(OP)',
       '萩ステークス(OP)', '秋風ステークス(OP)', 'さざんかステークス(OP)', 'クリスマスローズS(OP)',
       '2002ファイナルS(OP)', '東京リニューアル記念(OP)', '駿風ステークス(OP)',
       'サウジアラビアカップ(OP)', 'みなみ北海道S(OP)', '青函ステークス(OP)', '2004ファイナルS(OP)',
       '岡部幸雄騎手引退記念(OP)', '谷川岳ステークス(OP)', '白百合ステークス(OP)', '越後ステークス(OP)',
       'オパールステークス(OP)', '2005ファイナルS(OP)', '千葉ステークス(OP)', 'しらかばステークス(OP)',
       '尾張ステークス(OP)', '2006ファイナルS(OP)', '東京グランドオープン(OP)', 'アルデバランS(OP)',
       '2007ファイナルS(OP)', '門松ステークス(OP)', 'バレンタインS(OP)', '春雷ステークス(OP)',
       'アイルランドT(OP)', '室町ステークス(OP)', 'ブラジルカップ(OP)', '京洛ステークス(OP)',
       '2008ファイナルS(OP)', '名鉄杯(OP)', 'ジャニュアリーS(OP)', 'ポラリスステークス(OP)',
       '六甲ステークス(OP)', 'NST賞(OP)', '第1回レパードステークス(重賞)', '太秦ステークス(OP)',
       '2009ファイナルS(OP)', 'ジュニアカップ ', '万葉ステークス ', 'ジャニュアリーS ',
       '淀短距離ステークス ', 'ニューイヤーS ', '大和ステークス ', '紅梅ステークス ', '若駒ステークス ',
       'クロッカスステークス ', '白富士ステークス ', 'エルフィンステークス ', 'すばるステークス ', 'バレンタインS ',
       'アルデバランS ', 'ヒヤシンスステークス ', '千葉ステークス ', 'すみれステークス ', '仁川ステークス ',
       '大阪城ステークス ', 'アネモネステークス ', 'ポラリスステークス ', '東風ステークス ', '若葉ステークス ',
       '六甲ステークス ', 'マーガレットS ', 'コーラルステークス ', '伏竜ステークス ', '大阪ーハンブルクC ',
       '福島民報杯 ', '春雷ステークス ', '忘れな草賞 ', '京葉ステークス ', 'メトロポリタンS ',
       'オーストラリアT ', '橘ステークス ', '谷川岳ステークス ', 'スイートピーS ', '端午ステークス ',
       'プリンシパルS ', 'オアシスステークス ', '都大路ステークス ', '葵ステークス ', '栗東ステークス ',
       'メイステークス ', '昇竜ステークス ', 'テレビ愛知オープン ', '欅ステークス ', '白百合ステークス ',
       '天王山ステークス ', 'ブリリアントS ', '函館グランドオープン ', 'バーデンバーデンC ', '福島テレビオープン ',
       '大沼ステークス ', '米子ステークス ', '巴賞 ', 'マリーンステークス ', 'ラベンダー賞 ', 'KBC杯 ',
       'UHB杯 ', 'NST賞 ', 'みなみ北海道S ', 'ダリア賞 ', '関越ステークス ', 'フェニックス賞 ',
       '第2回レパードステークス(G)', '小倉日経オープン ', 'しらかばステークス ', '朱鷺ステークス ', 'ひまわり賞 ',
       'クローバー賞 ', '阿蘇ステークス ', 'BSN賞 ', '札幌日経オープン ', 'コスモス賞 ', '紫苑ステークス ',
       'カンナステークス ', '野路菊ステークス ', 'エニフステークス ', 'ききょうステークス ', 'すずらん賞 ',
       '芙蓉ステークス ', 'ポートアイランドS ', 'ペルセウスステークス ', '夕刊フジ杯オパールS ', 'アイルランドT ',
       'いちょうステークス ', '室町ステークス ', '福島民友カップ ', 'ブラジルカップ ', '萩ステークス ',
       'カシオペアステークス ', '京洛ステークス ', 'オーロカップ ', 'アンドロメダS ', '福島2歳ステークス ',
       '霜月ステークス ', '京都2歳ステークス ', 'キャピタルステークス ', '太秦ステークス ', 'クリスマスローズS ',
       'ターコイズステークス ', 'ラピスラズリS ', 'ベテルギウスS ', 'ディセンバーS ', 'ギャラクシーS ',
       '師走ステークス ', 'ホープフルステークス ', '2010ファイナルS ', '2010アンコールS ',
       '洛陽ステークス ', 'エスペランサS ', 'アハルテケステークス ', '被災地支援いぶき賞 ', 'ルミエールステークス ',
       '被災地支援つばさ賞 ', '五稜郭ステークス ', '夏至ステークス ', '祇園ステークス ', '安土城ステークス ',
       '信越ステークス ', 'パラダイスステークス ', '2011ファイナルS ', '2011アンコールS ',
       '鞍馬ステークス ', '天保山ステークス ', 'ジュライステークス ', '報知杯中京2歳S ', 'UHB賞 ',
       '丹頂ステークス ', 'ラジオ日本賞 ', '第1回アルテミスステークス(G)', '尾張ステークス ', 'リゲルステークス ',
       'フェアウェルS ', 'ディープインパクトC ', '第2回アルテミスステークス(G)', '京都オータムリーフP ',
       'ポルックスステークス ', 'オルフェーヴルカップ ', '青竜ステークス ', '韋駄天ステークス ', '鳳雛ステークス ',
       '第1回いちょうステークス(G)', 'エルコンドルパサーM ', 'もみじステークス ', 'アイビーステークス ',
       'オータムリーフS ', 'タンザナイトS ', '2014ファイナルS ', '総武ステークス ', 'モンゴル大統領賞 ',
       '第1回サウジアラビアRC(G)', 'グリーンチャンネルC ', '第1回ターコイズステークス(G)',
       '2015ファイナルS ', 'カーバンクルS ', '夢見月ステークス ', 'ルミエールオータムD ',
       '第2回ターコイズステークス(G)', '2016ファイナルS ', '名鉄杯 ', 'オクトーバーS ',
       '第1回葵ステークス(G)', 'ジュニアカップ(L)', 'ニューイヤーS(L)', '紅梅ステークス(L)',
       '淀短距離ステークス(L)', '若駒ステークス(L)', 'すばるステークス(L)', 'クロッカスステークス(L)',
       '白富士ステークス(L)', 'エルフィンステークス(L)', '洛陽ステークス(L)', 'ヒヤシンスステークス(L)',
       'マーガレットS(L)', '仁川ステークス(L)', 'すみれステークス(L)', '大阪城ステークス(L)',
       '東風ステークス(L)', 'アネモネステークス(L)', '若葉ステークス(L)', '六甲ステークス(L)',
       'コーラルステークス(L)', '春雷ステークス(L)', '忘れな草賞(L)', '福島民報杯(L)', '京葉ステークス(L)',
       'オアシスステークス(L)', 'スイートピーS(L)', '谷川岳ステークス(L)', 'ブリリアントS(L)',
       '橘ステークス(L)', '都大路ステークス(L)', 'プリンシパルS(L)', '栗東ステークス(L)',
       '鳳雛ステークス(L)', '第2回葵ステークス(G)', '安土城ステークス(L)', '白百合ステークス(L)',
       'スレイプニルS ', '米子ステークス(L)', '大沼ステークス(L)', 'パラダイスステークス(L)', '名鉄杯(L)',
       '札幌日経オープン(L)', 'BSN賞(L)', '朱鷺ステークス(L)', 'エニフステークス(L)',
       'ながつきステークス ', 'ポートアイランドS(L)', '信越ステークス(L)', 'グリーンチャンネルC(L)',
       '夕刊フジ杯オパールS(L)', 'アイビーステークス(L)', 'ブラジルカップ(L)', 'オクトーバーS(L)',
       '萩ステークス(L)', 'ルミエールオータムD(L)', 'カシオペアステークス(L)', 'オーロカップ(L)',
       'アンドロメダS(L)', '福島民友カップ(L)', 'キャピタルステークス(L)', 'ラピスラズリS(L)',
       '師走ステークス(L)', 'リゲルステークス(L)', 'ディセンバーS(L)', 'ベテルギウスS(L)',
       '北九州短距離S ', '吾妻小富士ステークス ', 'メトロポリタンS(L)', '第3回葵ステークス(G)',
       '三宮ステークス ', 'ジュライステークス(L)', 'ケフェウスステークス ', '新潟牝馬ステークス ',
       'カトレアステークス ', 'カノープスステークス ', 'りんくうステークス ', '門司ステークス ', '関門橋ステークス ',
       '名古屋城ステークス ', '第4回葵ステークス(G)', 'TVh賞 ', '大阪スポーツ杯 ', '卯月ステークス ',
       '松風月ステークス ', '青函ステークス ', 'コールドムーンS ', '睦月ステークス ', '令月ステークス ',
       'モルガナイトS ', '京都グランドオープン ', '越後ステークス ', '平城京ステークス ', '安達太良ステークス ',
       '藤森ステークス ', '新潟牝馬ステークス(L)', 'みちのくステークス '], dtype=object)
 

あまりにも多い。ここからリステッドであるものとそうでないものを導くのは無理がある。

しかしよく見ると「(L)」やら「(OP)」やらがついてるものがいる
直感的に「(L)」がリステッドクラスなのだろう
もしかすると「リステッド」とか含んでいるraceNameがあるかもしれない

こういう時は文字列系のメソッドであるcontainsを使う

In [13]:
df[df["raceName"].str.contains(
    r"\(L\)|リステッド", regex=True)]["raceName"].unique()
Out[13]:
array(['ジュニアカップ(L)', 'ニューイヤーS(L)', '紅梅ステークス(L)', '淀短距離ステークス(L)',
       '若駒ステークス(L)', 'すばるステークス(L)', 'クロッカスステークス(L)', '白富士ステークス(L)',
       'エルフィンステークス(L)', '洛陽ステークス(L)', 'ヒヤシンスステークス(L)', 'マーガレットS(L)',
       '仁川ステークス(L)', 'すみれステークス(L)', '大阪城ステークス(L)', '東風ステークス(L)',
       'アネモネステークス(L)', '若葉ステークス(L)', '六甲ステークス(L)', 'コーラルステークス(L)',
       '春雷ステークス(L)', '忘れな草賞(L)', '福島民報杯(L)', '京葉ステークス(L)', 'オアシスステークス(L)',
       'スイートピーS(L)', '谷川岳ステークス(L)', 'ブリリアントS(L)', '橘ステークス(L)',
       '都大路ステークス(L)', 'プリンシパルS(L)', '栗東ステークス(L)', '鳳雛ステークス(L)',
       '安土城ステークス(L)', '白百合ステークス(L)', '米子ステークス(L)', '大沼ステークス(L)',
       'パラダイスステークス(L)', '名鉄杯(L)', '札幌日経オープン(L)', 'BSN賞(L)', '朱鷺ステークス(L)',
       'エニフステークス(L)', 'ポートアイランドS(L)', '信越ステークス(L)', 'グリーンチャンネルC(L)',
       '夕刊フジ杯オパールS(L)', 'アイビーステークス(L)', 'ブラジルカップ(L)', 'オクトーバーS(L)',
       '萩ステークス(L)', 'ルミエールオータムD(L)', 'カシオペアステークス(L)', 'オーロカップ(L)',
       'アンドロメダS(L)', '福島民友カップ(L)', 'キャピタルステークス(L)', 'ラピスラズリS(L)',
       '師走ステークス(L)', 'リゲルステークス(L)', 'ディセンバーS(L)', 'ベテルギウスS(L)',
       'メトロポリタンS(L)', 'ジュライステークス(L)', '新潟牝馬ステークス(L)'], dtype=object)
 

リステッドと含まれているレース名はなかったが、ひとまず「(L)」を含むレース名は取得できた
一部を確認してみる。

大阪城ステークス(L)で確認
2023年レース結果URL:https://race.netkeiba.com/race/result.html?race_id=202309010811

リステッドクラスとか書いてくれてないので、ちょっと良く分からない・・・
とりあえず、過去24年間の出走データからリステッドクラスのユニーク数を出してみると65レースあった

In [14]:
df[df["raceName"].str.contains(r"\(L\)", regex=True)]["raceName"].nunique()
Out[14]:
65
In [15]:
# 一応年度別にリステッドクラスのレース数を見る
idf = df.copy()
idf["year"] = df["raceDate"].dt.year
idf[df["raceName"].str.contains(r"\(L\)", regex=True)].groupby('year')[
    "raceName"].nunique()
Out[15]:
year
2019    62
2020    63
2021    63
2022    63
2023    64
Name: raceName, dtype: int64
 

厄介なことに年度ごとに開催するレース数が違うが、だいたい年間で63レースほどある。
先の集計結果ではユニーク数は65レースあったが、2019年から2023年までの間に
リステッドクラスとして追加されたものやなくなったものがあるのかもしれない

 

公式サイトの方で2019年で1レース足らないが、おそらく前処理の段階で除外されてしまったのかもしれない
調べるのは面倒で1レースだけであるから、大きな影響はないとして見逃す

 

とりあえず、2019年以降からリステッドクラスとなったレースは2018年以前でもリステッドクラスとみなすことにする

In [16]:
# (L)が入っているレース名だけ取り出して、(L)を取り除いたレース名で検索できるか確認する
for rname in df[df["raceName"].str.contains(r"\(L\)", regex=True)]["raceName"].unique():
    print(df[df["raceName"].str.contains(
        rname.replace("(L)", ""))]["raceName"].unique())
 
['ジュニアカップ(OP)' 'ジュニアカップ ' 'ジュニアカップ(L)']
['ニューイヤーS(OP)' 'ニューイヤーS ' 'ニューイヤーS(L)']
['紅梅ステークス(OP)' '紅梅ステークス ' '紅梅ステークス(L)']
['淀短距離ステークス(OP)' '淀短距離ステークス ' '淀短距離ステークス(L)']
['若駒ステークス(OP)' '若駒ステークス ' '若駒ステークス(L)']
['すばるステークス(OP)' 'すばるステークス ' 'すばるステークス(L)']
['クロッカスステークス(OP)' 'クロッカスステークス ' 'クロッカスステークス(L)']
['白富士ステークス(OP)' '白富士ステークス ' '白富士ステークス(L)']
['エルフィンステークス(OP)' 'エルフィンステークス ' 'エルフィンステークス(L)']
['洛陽ステークス(3勝)' '洛陽ステークス ' '洛陽ステークス(L)']
['ヒヤシンスステークス(OP)' 'ヒヤシンスステークス ' 'ヒヤシンスステークス(L)']
['マーガレットS(OP)' 'マーガレットS ' 'マーガレットS(L)']
['仁川ステークス(OP)' '仁川ステークス ' '仁川ステークス(L)']
['すみれステークス(OP)' 'すみれステークス ' 'すみれステークス(L)']
['大阪城ステークス(OP)' '大阪城ステークス ' '大阪城ステークス(L)']
['東風ステークス(OP)' '東風ステークス ' '東風ステークス(L)']
['アネモネステークス(OP)' 'アネモネステークス ' 'アネモネステークス(L)']
['若葉ステークス(OP)' '若葉ステークス ' '若葉ステークス(L)']
['六甲ステークス(OP)' '六甲ステークス ' '六甲ステークス(L)']
['コーラルステークス(OP)' 'コーラルステークス ' 'コーラルステークス(L)']
['春雷ステークス(OP)' '春雷ステークス ' '春雷ステークス(L)']
['忘れな草賞(OP)' '忘れな草賞 ' '忘れな草賞(L)']
['福島民報杯(OP)' '福島民報杯 ' '福島民報杯(L)']
['京葉ステークス(3勝)' '京葉ステークス(OP)' '京葉ステークス ' '京葉ステークス(L)']
['オアシスステークス(OP)' 'オアシスステークス ' 'オアシスステークス(L)']
['スイートピーS(OP)' 'スイートピーS ' 'スイートピーS(L)']
['谷川岳ステークス(OP)' '谷川岳ステークス ' '谷川岳ステークス(L)']
['ブリリアントS(OP)' 'ブリリアントS ' 'ブリリアントS(L)']
['橘ステークス(OP)' '橘ステークス ' '橘ステークス(L)']
['都大路ステークス(OP)' '都大路ステークス ' '都大路ステークス(L)']
['プリンシパルS(OP)' 'プリンシパルS ' 'プリンシパルS(L)']
['栗東ステークス(OP)' '栗東ステークス ' '栗東ステークス(L)']
['鳳雛ステークス ' '鳳雛ステークス(L)']
['安土城ステークス ' '安土城ステークス(L)']
['白百合ステークス' '白百合ステークス(2勝)' '白百合ステークス(OP)' '白百合ステークス ' '白百合ステークス(L)']
['米子ステークス(OP)' '米子ステークス ' '米子ステークス(L)']
['大沼ステークス(3勝)' '大沼ステークス(OP)' '大沼ステークス ' '大沼ステークス(L)']
['パラダイスステークス(OP)' 'パラダイスステークス ' 'パラダイスステークス(L)']
['名鉄杯(2勝)' '名鉄杯(OP)' '名鉄杯 ' '名鉄杯(L)']
['札幌日経オープン(OP)' '札幌日経オープン ' '札幌日経オープン(L)']
['BSN賞(3勝)' 'BSN賞(OP)' 'BSN賞(2勝)' 'BSN賞 ' 'BSN賞(L)']
['朱鷺ステークス(OP)' '朱鷺ステークス(3勝)' '朱鷺ステークス ' '朱鷺ステークス(L)']
['エニフステークス(OP)' 'エニフステークス ' 'エニフステークス(L)']
['ポートアイランドS(OP)' 'ポートアイランドS ' 'ポートアイランドS(L)']
['信越ステークス ' '信越ステークス(L)']
['グリーンチャンネルC(2勝)' 'グリーンチャンネルC(1勝)' 'グリーンチャンネルC ' 'グリーンチャンネルC(L)']
['夕刊フジ杯オパールS ' '夕刊フジ杯オパールS(L)']
['アイビーステークス(OP)' 'アイビーステークス ' 'アイビーステークス(L)']
['ブラジルカップ(3勝)' 'ブラジルカップ(OP)' 'ブラジルカップ ' 'ブラジルカップ(L)']
['オクトーバーS(3勝)' 'オクトーバーS ' 'オクトーバーS(L)']
['萩ステークス(OP)' '萩ステークス ' '萩ステークス(L)']
['ルミエールオータムD ' 'ルミエールオータムD(L)']
['カシオペアステークス(OP)' 'カシオペアステークス ' 'カシオペアステークス(L)']
['オーロカップ(OP)' 'オーロカップ ' 'オーロカップ(L)']
['アンドロメダS(OP)' 'アンドロメダS ' 'アンドロメダS(L)']
['福島民友カップ(OP)' '福島民友カップ ' '福島民友カップ(L)']
['キャピタルステークス(3勝)' 'キャピタルステークス(OP)' 'キャピタルステークス ' 'キャピタルステークス(L)']
['ラピスラズリS ' 'ラピスラズリS(L)']
['師走ステークス(OP)' '師走ステークス ' '師走ステークス(L)']
['リゲルステークス ' 'リゲルステークス(L)']
['ディセンバーS(OP)' 'ディセンバーS ' 'ディセンバーS(L)']
['ベテルギウスS(OP)' 'ベテルギウスS ' 'ベテルギウスS(L)']
['メトロポリタンS(OP)' 'メトロポリタンS ' 'メトロポリタンS(L)']
['ジュライステークス(3勝)' 'ジュライステークス ' 'ジュライステークス(L)']
['新潟牝馬ステークス ' '新潟牝馬ステークス(L)']
 

上手く行ってそうだ

スポンサーリンク

2-3.レースのクラスのグレード分けを改めて行う

 

ということで、リステッドクラス用のマッピングを作成してグレード分けする

In [17]:
listed_racename_list = [rname.replace("(L)", "") for rname in df[df["raceName"].str.contains(
    r"\(L\)", regex=True)]["raceName"].unique()]

listed_racename_list = idf[idf["raceName"].str.contains(
    "|".join(listed_racename_list))]["raceName"].unique().tolist()

listed_mapping = {rname: 5 for rname in listed_racename_list}


# それぞれのマッピングを作っておく


def map_race_grade(data: str):

    if re.search(r"\(G1\)", data) or re.search(r"\(GI\)", data):

        return 8

    if re.search(r"\(G2\)", data) or re.search(r"\(GII\)", data):

        return 7

    if re.search(r"\(G3\)", data) or re.search(r"\(GIII\)", data):

        return 6

    return None


grade_mapping_dict = {

    '4歳未勝利': 0, '4歳新馬': 0, '4歳以上500万下': 1, '4歳500万下': 1, '4歳以上900万下': 2, '4歳以上オープン': 4,

    '4歳以上1600万下': 3, '4歳オープン': 4, '4歳未出走': 0, '5歳以上オープン': 4, '4歳900万下': 2, '3歳新馬': 0,

    '3歳未勝利': 0, '3歳オープン': 4, '3歳500万下': 1, '3歳未出走': 0, '3歳以上オープン': 4, '3歳900万下': 2,

    '2歳新馬': 0, '3歳以上500万下': 1, '3歳以上1000万下': 2, '2歳未勝利': 0, '2歳オープン': 4, '3歳以上1600万下': 3,

    '2歳500万下': 1, '4歳以上1000万下': 2, '3歳1000万下': 2, '3歳以上1勝クラス': 1, '3歳以上2勝クラス': 2,

    '3歳以上3勝クラス': 3, '2歳1勝クラス': 1, '3歳1勝クラス': 1, '4歳以上1勝クラス': 1, '4歳以上2勝クラス': 2,

    '4歳以上3勝クラス': 3

}


# クラスをグレードに置き換える

idf = df["raceName"].map(map_race_grade)

idf = idf[~idf.isna()]

idf2 = df[~df.index.isin(idf.index)]["raceName"].map(listed_mapping)

idf2 = idf2[~idf2.isna()]

df["raceGrade"] = pd.concat([idf, idf2, df[~df.index.isin(idf.index.tolist(
)+idf2.index.tolist())]["raceDetail"].map(grade_mapping_dict)]).astype(int)
 

再度分け直したので、集計もやり直す

In [18]:
idf = df.copy()
idf["year"] = df["raceDate"].dt.year
idfg = pd.concat(
    [
        idfg.groupby("year")[["horseId"]].nunique().rename(
            columns={"horseId": f"Grade {col}"}) for col, idfg in idf.groupby("raceGrade")
    ] + [idf.groupby("year")[["horseId"]].nunique().rename(
        columns={"horseId": f"Grade ALL"})], axis=1
).fillna(0).convert_dtypes()
idfg["G race"] = idfg[["Grade 6", "Grade 7", "Grade 8"]].sum(axis=1)
idfg["G race rate"] = (100*idfg["G race"]/idfg["Grade ALL"]).round(1)
display(idfg)
sns.heatmap((idfg[idfg.columns[:9]].diff(axis=1).fillna(0)
            <= 0).astype(int), annot=True, linewidths=0.01)
plt.show()
 
  Grade 0 Grade 1 Grade 2 Grade 3 Grade 4 Grade 5 Grade 6 Grade 7 Grade 8 Grade ALL G race G race rate
year                        
2000 4614 3797 1380 585 475 447 564 314 234 8168 1112 13.6
2001 4883 4159 1501 496 488 387 561 298 228 8754 1087 12.4
2002 5089 4065 1538 468 511 446 553 325 226 9145 1104 12.1
2003 5362 4123 1594 484 539 457 581 311 238 9585 1130 11.8
2004 5543 4208 1604 434 522 457 562 324 242 9855 1128 11.4
2005 5692 4170 1553 449 503 456 574 312 233 10006 1119 11.2
2006 5867 4247 1671 509 511 434 574 323 227 10230 1124 11.0
2007 5939 4244 1760 637 454 452 573 351 243 10249 1167 11.4
2008 6218 4173 1789 707 541 479 591 371 241 10709 1203 11.2
2009 6209 4160 1780 710 497 495 600 365 253 10710 1218 11.4
2010 6301 4193 1745 719 497 523 618 351 252 10790 1221 11.3
2011 6385 4110 1776 712 503 480 619 361 257 10802 1237 11.5
2012 6505 4045 1748 698 506 490 611 354 249 10809 1214 11.2
2013 6461 4064 1756 714 504 522 629 369 256 10801 1254 11.6
2014 6598 3882 1686 718 495 498 628 384 239 10772 1251 11.6
2015 6594 3887 1734 736 508 519 611 389 240 10868 1240 11.4
2016 6689 3905 1739 713 459 498 638 354 246 10921 1238 11.3
2017 6711 3940 1721 697 430 487 635 348 265 11000 1248 11.3
2018 6786 4022 1687 704 426 494 611 339 299 11142 1249 11.2
2019 6967 3772 1766 764 454 482 644 339 262 11255 1245 11.1
2020 7061 3522 1850 864 524 559 634 372 249 11407 1255 11.0
2021 6977 3441 1824 903 541 564 649 392 267 11287 1308 11.6
2022 6927 3470 1744 906 533 561 666 390 280 11280 1336 11.8
2023 7021 3518 1777 934 586 530 674 407 274 11448 1355 11.8
 
 

結局、オープンクラスの方が3勝クラスよりも出走数が多い問題は解決していないどころか、悪化したように見える

スポンサーリンク

2-4.オープンクラスが3勝クラスよりも出走馬数が多くなる理由

 

そもそもレースのクラス分けは獲得賞金や勝利回数だけでなく、格付けが存在しているらしい
その格付けによって新馬戦からいきなり重賞レースへ出走するなどができるようで、1勝するごとに1つグレードが上がるという分けでもないらしい
つまり、新馬・未勝利戦から1勝クラス~3勝クラスからいきなりオープン戦に出るというのは別におかしくないようである
そのため、3勝クラスよりオープンクラスの出走馬数が多いのは、そういうのが原因なのだと考えられる。

スポンサーリンク

2-5.決定事項: モデルの予測対象は2019年以降とする

 

2019年以降からレース出走条件が変わっていることから、
ファーストモデルでは2018年以降を予測対象としていたが、
現在でも同様の出走条件となっている2019年以降からを予測対象とするように変更する。

スポンサーリンク

2-6.重賞レースに出走できる競走馬の特徴

 

重賞レースに出走できる馬はかなり能力が高いと思われる。
おまけに、3勝クラスで勝利するなどで順当にクラスを上がってくるだけでなく、
新馬戦からいきなり重賞レースに出走する競走馬などもおり、
重賞レースに出走する競走馬の下積み時代にかなり違いがあると思われる

 

という訳で重賞レースを走る競走馬について以下の仮説を立てて検証してみる。

  1. 下積み時代、つまり新馬戦~オープンクラスのレースの出走回数が少ないのでは?
  2. 血統によって重賞レースに出走できるできないが分かるのでは?

2-6-1.重賞レースに勝利するまでの出走回数の確認

 

方針としては、

  • step0: 1998年以降に生まれた競走馬のみで絞る
  • step1: 過去1回以上重賞レースに出走したことのある競走馬に絞って
  • step2: 最初に重賞レースに勝利するまでのデータに絞り込み、
  • step3: それまでの出走回数を数えることにする

といった感じで調べる

 

とりあえず、大前提として2000年あたりの出走情報だと2000年より前の年から出走している競走馬が沢山いるため、
事前に1998年以降に生まれた競走馬のみを対象に分析を行う

スクレイピングの段階で競走馬の誕生日を抜き出しているが、
horseIdの上4桁が誕生年になっているので、ここは簡単にその情報を使う

In [19]:
# step0-1: まずは馬の誕生年の情報を追加
df["birthY"] = df["horseId"].str[:4].astype(int)
df["birthY"].sort_values().unique()
Out[19]:
array([1986, 1988, 1991, 1992, 1993, 1994, 1995, 1996, 1997, 1998, 1999,
       2000, 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010,
       2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2020, 2021])
In [20]:
# step0-2: 1997年以前生まれの競走馬しかいないレース情報はすべて削除する
filter_raceId_list = df[df["birthY"].isin(
    list(range(1998, 2024)))]["raceId"].unique()
dfG = df[df["raceId"].isin(filter_raceId_list)]
dfG["year"] = dfG["raceDate"].dt.year
dfG.groupby("year")[["raceId"]].nunique().T
Out[20]:
year 2000 2001 2002 2003 2004 2005 2006 2007 2008 2009 2014 2015 2016 2017 2018 2019 2020 2021 2022 2023
raceId 427 2598 3316 3318 3317 3313 3320 3321 3320 3319 3326 3326 3326 3329 3328 3325 3331 3326 3331 3329

1 rows × 24 columns

 

一旦重賞クラスに出走する競走馬について軽く調べてみたいので、1度でも出走したことのある競走馬のみに絞る

In [21]:
# step1
Ghorse_list = dfG[dfG["raceGrade"].isin([8, 7, 6]) & dfG["birthY"].isin(
    list(range(1998, 2024)))]["horseId"].unique()
dfG = dfG[dfG["horseId"].isin(Ghorse_list)]
 

一旦どんなお馬さんがいるのか確認した中で気になったものを取り上げる

In [22]:
display_columns = ["raceId", "raceDate", "raceName", "raceGrade", "label", "favorite", "odds",
                   "horseId", "horseName", "stallionName", "bStallionName", "sStallionName", "s2StallionName"]
 
In [23]:
horseId = "2018100274"
df[df["horseId"].isin([horseId])][display_columns]
Out[23]:
  raceId raceDate raceName raceGrade label favorite odds horseId horseName stallionName bStallionName sStallionName s2StallionName
985510 202006050205 2020-12-06 2歳新馬 0 2 1 2.9 2018100274 ジャックドール モーリス Unbridled’s Song スクリーンヒーロー グラスワンダー
988544 202009060804 2020-12-27 2歳未勝利 0 2 1 2.6 2018100274 ジャックドール モーリス Unbridled’s Song スクリーンヒーロー グラスワンダー
1004102 202109021005 2021-04-25 3歳未勝利 0 1 1 1.7 2018100274 ジャックドール モーリス Unbridled’s Song スクリーンヒーロー グラスワンダー
1005424 202105020511 2021-05-08 プリンシパルS(L) 5 5 4 9.0 2018100274 ジャックドール モーリス Unbridled’s Song スクリーンヒーロー グラスワンダー
1020932 202107050108 2021-09-11 3歳以上1勝クラス 1 1 1 3.0 2018100274 ジャックドール モーリス Unbridled’s Song スクリーンヒーロー グラスワンダー
1023458 202107050909 2021-10-03 浜名湖特別 2 1 1 1.8 2018100274 ジャックドール モーリス Unbridled’s Song スクリーンヒーロー グラスワンダー
1030518 202105050811 2021-11-28 ウェルカムステークス 3 1 3 5.1 2018100274 ジャックドール モーリス Unbridled’s Song スクリーンヒーロー グラスワンダー
1037971 202205010111 2022-01-29 白富士ステークス(L) 5 1 1 2.2 2018100274 ジャックドール モーリス Unbridled’s Song スクリーンヒーロー グラスワンダー
1044132 202207020211 2022-03-13 第58回金鯱賞(G2) 7 1 1 2.0 2018100274 ジャックドール モーリス Unbridled’s Song スクリーンヒーロー グラスワンダー
1046898 202209020411 2022-04-03 第66回大阪杯(G1) 8 5 2 3.7 2018100274 ジャックドール モーリス Unbridled’s Song スクリーンヒーロー グラスワンダー
1064099 202201020411 2022-08-21 第58回札幌記念(G2) 7 1 3 4.6 2018100274 ジャックドール モーリス Unbridled’s Song スクリーンヒーロー グラスワンダー
1072497 202205040911 2022-10-30 第166回天皇賞(秋)(G1) 8 4 3 5.0 2018100274 ジャックドール モーリス Unbridled’s Song スクリーンヒーロー グラスワンダー
1092540 202309020411 2023-04-02 第67回大阪杯(G1) 8 1 2 3.6 2018100274 ジャックドール モーリス Unbridled’s Song スクリーンヒーロー グラスワンダー
1100516 202305030211 2023-06-04 第73回安田記念(G1) 8 5 5 8.0 2018100274 ジャックドール モーリス Unbridled’s Song スクリーンヒーロー グラスワンダー
1110066 202301020411 2023-08-20 第59回札幌記念(G2) 7 6 1 2.3 2018100274 ジャックドール モーリス Unbridled’s Song スクリーンヒーロー グラスワンダー
1118444 202305040911 2023-10-29 第168回天皇賞(秋)(G1) 8 11 5 15.2 2018100274 ジャックドール モーリス Unbridled’s Song スクリーンヒーロー グラスワンダー
 

2023年G1の大阪杯で1着になったジャックドールでみると、2020年12月にデビューしてから
G1勝利するまでの2年5カ月間で12レース出場し7勝5敗と実力を遺憾なく発揮している
デビュー時から人気も高いという素晴らしいサラブレッドぶり
どうして下積みとして下位クラスをこんなにも出場させられていたのか分からないほど
もしかしたら、競走馬の中にも成長度合いによって下積みを長くしたり等があるのかもしれない

 
In [24]:
horseId = "2019105283"
df[df["horseId"].isin([horseId])][display_columns]
Out[24]:
  raceId raceDate raceName raceGrade label favorite odds horseId horseName stallionName bStallionName sStallionName s2StallionName
1020613 202110040805 2021-09-05 2歳新馬 0 1 1 1.7 2019105283 ドウデュース ハーツクライ Vindication サンデーサイレンス Halo
1025567 202105040509 2021-10-23 アイビーステークス(L) 5 1 2 3.8 2019105283 ドウデュース ハーツクライ Vindication サンデーサイレンス Halo
1033603 202109060611 2021-12-19 第73回朝日フューチュリティ(G1) 8 1 3 7.8 2019105283 ドウデュース ハーツクライ Vindication サンデーサイレンス Halo
1043159 202206020411 2022-03-06 第59回報知弥生ディープ記念(G2) 7 2 1 2.2 2019105283 ドウデュース ハーツクライ Vindication サンデーサイレンス Halo
1048437 202206030811 2022-04-17 第82回皐月賞(G1) 8 3 1 3.9 2019105283 ドウデュース ハーツクライ Vindication サンデーサイレンス Halo
1054056 202205021211 2022-05-29 第89回東京優駿(G1) 8 1 3 4.2 2019105283 ドウデュース ハーツクライ Vindication サンデーサイレンス Halo
1086114 202309010211 2023-02-12 第116回京都記念(G2) 7 1 1 2.5 2019105283 ドウデュース ハーツクライ Vindication サンデーサイレンス Halo
1118437 202305040911 2023-10-29 第168回天皇賞(秋)(G1) 8 7 2 4.3 2019105283 ドウデュース ハーツクライ Vindication サンデーサイレンス Halo
1121955 202305050812 2023-11-26 第43回ジャパンカップ(G1) 8 4 3 13.2 2019105283 ドウデュース ハーツクライ Vindication サンデーサイレンス Halo
1125541 202306050811 2023-12-24 第68回有馬記念(G1) 8 1 2 5.2 2019105283 ドウデュース ハーツクライ Vindication サンデーサイレンス Halo
 

早い段階で重賞クラスで活躍している競走馬も見てみると、
デビュー戦時点で1番人気、オッズ1.7、堂々の1着から
2021年朝日杯フューチュリティステークスのG1制覇まで3戦全勝の爆走ぶり
父にはハーツクライを引っ提げ、現在も活躍中のG1三冠馬であるドウデュース
成績を見るとあまりにも化け物
5歳馬になった今でもオッズがほぼ10を上回らない人気ぶりという早くから頭角を現すものもいる
重賞クラスに1度でも勝利した競走馬のみのデータに絞る

In [25]:
# step2: 処理が難しいがこういうのは全てgroupbyが何とかしてくれる
group_df = dfG.groupby("horseId")[["label", "raceGrade"]].filter(
    lambda group: group[group["raceGrade"].isin([6, 7, 8])]["label"].isin([1]).any())
dfG1 = dfG.loc[group_df.index]
# 一応1998年生まれ以降の競走馬のみ残す
dfG1 = dfG1[dfG1["birthY"].isin(list(range(1998, 2024)))]
dfG1["horseId"].nunique(), round(
    100*dfG1["horseId"].nunique()/df["horseId"].nunique(), 3)
Out[25]:
(1486, 1.297)
 

24年間で1486頭しかいない。全体の競走馬の中でも1.297%程度しかいない。
想像よりも重賞クラスで勝つのは厳しいようだ
(そもそも重賞クラスのレース数が少ないから当たり前ではある)
最後に初めて重賞クラスで勝利するまでの情報に絞り込む

In [26]:
# step3: こういう場合は、Gwinカラムを追加して、
# 重賞に勝利した行にフラグを立てて、groupbyからのcumsumしてあげる
dfG1["Gwin"] = (dfG1["label"].isin([1]) &
                dfG1["raceGrade"].isin([6, 7, 8])).astype(int)
dfG1["Gwin"] = dfG1.groupby("horseId")["Gwin"].cumsum()
# 最後にGwinが0の行だけ取り出せばOK
dfG1 = dfG1[dfG1["Gwin"].isin([0])]
# それでは実際にカウントしてみる
dfG_cnt = dfG1.groupby("horseId")["horseId"].count()
dfG_cnt.describe()
Out[26]:
count    1478.000000
mean        9.978349
std         8.712177
min         1.000000
25%         3.000000
50%         7.000000
75%        15.000000
max        47.000000
Name: horseId, dtype: float64
 

集計結果から、平均して10レース弱で重賞クラスに勝利しているようで、
中央値としても7レースであることから、特段大きな偏りがないように見える

2-6-2.G1, G2, G3に勝利したお馬さんが重賞クラスに勝利するまでの平均出走回数

In [27]:
# さっきまでの処理を再利用
def count_raceNum_for_G(target: list[int], firstwin=[6, 7, 8], mode=True, dfG=dfG):
    group_df = dfG.groupby("horseId")[["label", "raceGrade"]].filter(
        lambda group: group[group["raceGrade"].isin(target)]["label"].isin([1]).any())
    dfG1 = dfG.loc[group_df.index][["horseId", "birthY", "raceGrade", "label"]]

    dfG1 = dfG1[dfG1["birthY"].isin(list(range(1998, 2024)))]
    dfG1["win"] = (dfG1["label"].isin([1]) &
                   dfG1["raceGrade"].isin(firstwin)).astype(int)
    dfG1["win"] = dfG1.groupby("horseId")["win"].cumsum()
    win_horseId_list = []
    dfG2 = dfG1[dfG1["win"].isin([0])]
    if 0 in target:
        win_horseId_list = list(
            set(dfG1["horseId"].unique()) - set(dfG2["horseId"].unique()))
    dfG_cnt = dfG2.groupby("horseId")["horseId"].count()
    dfG_cnt = pd.concat([dfG_cnt, pd.Series(0, index=win_horseId_list)])
    if mode:
        return dfG_cnt.describe()
    return dfG_cnt
In [28]:
pd.concat([count_raceNum_for_G(
    [6+idx]).rename(f"G{3-idx}") for idx in range(3)], axis=1).T.convert_dtypes()
Out[28]:
  count mean std min 25% 50% 75% max
G3 1093 10.281793 8.996907 1 3 8 15.0 47
G2 567 8.305115 7.280621 1 3 6 11.5 46
G1 315 6.64127 6.373328 1 2 4 9.0 36
 

平均出走回数でみると、G3 < G2 < G1の順で勝つまでのレース数が少なくなっていく
つまり、G1で勝てるお馬さんは早い段階からその実力が出ることが容易に想像できる。
G1勝利馬で見ると、中央値ですら重賞クラスに勝利するまで4レースしか走らない。
とはいえ、G2やG3で勝つ馬もだいたい6から8レース走ってから重賞クラスに勝利している。

In [29]:
# 馬の平均出走回数
dfG.groupby("horseId")[["raceId"]].count().describe().T
Out[29]:
  count mean std min 25% 50% 75% max
raceId 9766.0 21.378558 12.361211 1.0 12.0 20.0 29.0 96.0
 

出走情報全体で見てもお馬さんの平均出走回数は21回とかなり走っている。
そう言った中で、8レースほどで重賞クラスに勝利できるかどうかでそのお馬さんの実力が分かりそうだ

スポンサーリンク

2-7.重賞クラス勝利平均出走回数の上位25%と下位25%の父と母父の分布確認

2-7-1.G1, G2, G3を制した種牡馬産駒の平均出走回数で調べる

 

すこし気になった、成長度合いのスピードが血統によって違いが出ているのではないかという点
簡単に種牡馬の産駒数を数える

In [30]:
# G1,G2,G3に勝利した競走馬を分析
pickup = 15
threash = 0.25
dflist = []
multiColumns = []
# 2019年以降に出走したことのある競走馬のみに絞る
dfG2 = dfG[dfG["horseId"].isin(dfG[dfG["raceDate"].dt.year.isin(
    list(range(2019, 2024)))]["horseId"].unique())]
for g, idfG in dfG2.groupby(["field", "dist_cat"]):
    # field, dist_catごとに出走したことのある競走馬に対して、
    # G1, G2, G3で勝利したことのある競走馬
    dfG_cnt = count_raceNum_for_G(
        list(range(9)), [6, 7, 8], mode=False, dfG=idfG).rename(f"Grace")
    upperhorse = dfG_cnt[dfG_cnt.quantile(threash) >= dfG_cnt].index.tolist()
    lowerhorse = dfG_cnt[dfG_cnt.quantile(1-threash) < dfG_cnt].index.tolist()
    if len(upperhorse) == 0:
        upperhorse = dfG_cnt[dfG_cnt.isin([dfG_cnt.min()])].index.tolist()
    if len(lowerhorse) == 0:
        lowerhorse = dfG_cnt[dfG_cnt.isin([dfG_cnt.max()])].index.tolist()

    dfGhigh25 = dfG2[dfG2["horseId"].isin(upperhorse)]
    dfGlow25 = dfG2[dfG2["horseId"].isin(lowerhorse)]

    # 種牡馬
    dftarget = dfstallion
    dflist += [dftarget[dftarget["horseId"].isin(dfGhigh25["horseId"].unique())]["GenName"].value_counts().sort_values(
        ascending=False).head(pickup).reset_index().apply(lambda x: ", ".join([str(x) for x in x.tolist()]), axis=1)]
    dflist += [dftarget[dftarget["horseId"].isin(dfGlow25["horseId"].unique())]["GenName"].value_counts().sort_values(
        ascending=False).head(pickup).reset_index().apply(lambda x: ", ".join([str(x) for x in x.tolist()]), axis=1)]

    multiColumns += [[g[0], f"high{int(threash*100)}%", g[1]],
                     [g[0], f"low{int(threash*100)}%", g[1]]]
threash_col = [f"high{int(threash*100)}%", f"low{int(threash*100)}%"]
idfgen = pd.concat(dflist, axis=1).rename(
    index=lambda idx: f"{1+idx}位").fillna("")
idfgen.columns = pd.MultiIndex.from_tuples(multiColumns)
idfgen = idfgen.T.sort_index(key=lambda x: [({"ダ": 0, "芝": 1, f"high{int(threash*100)}%": 0} | {
                             k: n for n, k in enumerate("SMILE")}).get(ix, 1) for ix in x])
idfgen[idfgen.columns[:5]]
Out[30]:
      1位 2位 3位 4位 5位
high25% S ヘニーヒューズ, 7 ロードカナロア, 5 ダイワメジャー, 5 キンシャサノキセキ, 3 ルーラーシップ, 2
M キズナ, 8 キングカメハメハ, 8 ダイワメジャー, 8 ハーツクライ, 7 ヘニーヒューズ, 7
I キングカメハメハ, 4 シニスターミニスター, 4 ディープインパクト, 3 キズナ, 2 ハーツクライ, 2
L ステイゴールド, 1 ハーツクライ, 1 メイショウボーラー, 1 ゴールドアリュール, 1 シニスターミニスター, 1
low25% S サウスヴィグラス, 6 クロフネ, 4 パイロ, 3 ダイワメジャー, 2 ロードカナロア, 2
M キングカメハメハ, 11 エンパイアメーカー, 8 ヘニーヒューズ, 7 カジノドライヴ, 7 シニスターミニスター, 6
I キングカメハメハ, 3 ゴールドアリュール, 2 シンボリクリスエス, 2 ブラックタイド, 2 ステイゴールド, 2
L バゴ, 1 ワークフォース, 1      
high25% S ロードカナロア, 13 エピファネイア, 8 キンシャサノキセキ, 7 ルーラーシップ, 6 クロフネ, 5
M ディープインパクト, 62 ルーラーシップ, 31 ハーツクライ, 28 ロードカナロア, 22 エピファネイア, 20
I ディープインパクト, 44 ハーツクライ, 20 ロードカナロア, 13 エピファネイア, 13 ルーラーシップ, 13
L ディープインパクト, 16 ハーツクライ, 13 ルーラーシップ, 9 ハービンジャー, 9 エピファネイア, 8
E ディープインパクト, 4 ドゥラメンテ, 2 アドマイヤドン, 1 ハーツクライ, 1 ルーラーシップ, 1
low25% S ダイワメジャー, 10 アドマイヤムーン, 9 キンシャサノキセキ, 8 ロードカナロア, 8 ショウナンカンプ, 6
M ディープインパクト, 48 ダイワメジャー, 29 ハーツクライ, 25 ロードカナロア, 19 ステイゴールド, 17
I ディープインパクト, 17 ハーツクライ, 16 ステイゴールド, 16 ハービンジャー, 14 キングカメハメハ, 9
L ルーラーシップ, 11 ディープインパクト, 10 ハーツクライ, 10 ステイゴールド, 10 キングカメハメハ, 8
E オルフェーヴル, 4 タイキシャトル, 1 ディープブリランテ, 1 ディープインパクト, 1  
 

確認してみると、芝ではどちらもディープインパクトが目立っており、他の種牡馬で見ても大した違いはないように見える。
ダートの方では、早く結果を残すものとそうでないもので、違いが出ているがとはいえ産駒数が少ないのもあり、誤差の範囲にも見える

 

みんな大好きワードクラウドでも出してみましょう

In [31]:
colormap = "RdYlBu"
# linux系なら以下あたりにフォントファイルがあるかも?調べてもらう方が早いと思います。
# font_path="/usr/share/fonts/truetype/fonts-japanese-gothic.ttf"

# Windowsの人は以下あたりにあるかと
font_path = "C:/Windows/Fonts/meiryo.ttc"

for field in ["ダ", "芝"]:
    idfg = idfgen.loc[field]
    ncols = len(idfg.loc[idfg.index[0][0]])
    nrows = len(threash_col)
    plt.figure(figsize=(27, 9))
    cnt = 1
    for row, idx in enumerate(threash_col, start=1):
        for cidx in "SMILE"[:ncols]:
            word_list = []
            for word in idfg.loc[(idx, cidx)].tolist():
                if word == "":
                    break
                word_list += [word.split(", ")[0]]*int(word.split(", ")[1])
            if len(word_list) == 0:
                break
            random.shuffle(word_list)
            plt.subplot(nrows, ncols, cnt)
            wordcloud = WordCloud(
                background_color="gray",
                width=800,
                height=800,
                font_path=font_path,
                colormap=colormap,
            ).generate(" ".join(word_list))
            plt.imshow(wordcloud, interpolation="bilinear")
            plt.title(", ".join([field, idx, cidx]))
            plt.axis("off")
            cnt += 1
    plt.show()
 
 
 

詳細は述べません、感じてください。

2-7-2.G1, G2, G3を制した母父産駒の平均出走回数で調べる

In [32]:
# G1,G2,G3に勝利した競走馬を分析
pickup = 15
threash = 0.25
dflist = []
multiColumns = []
# 2019年以降に出走したことのある競走馬のみに絞る
dfG2 = dfG[dfG["horseId"].isin(dfG[dfG["raceDate"].dt.year.isin(
    list(range(2019, 2024)))]["horseId"].unique())]
for g, idfG in dfG2.groupby(["field", "dist_cat"]):
    # field, dist_catごとに出走したことのある競走馬に対して、
    # G1, G2, G3で勝利したことのある競走馬
    dfG_cnt = count_raceNum_for_G(
        list(range(9)), [6, 7, 8], mode=False, dfG=idfG).rename(f"Grace")
    upperhorse = dfG_cnt[dfG_cnt.quantile(threash) >= dfG_cnt].index.tolist()
    lowerhorse = dfG_cnt[dfG_cnt.quantile(1-threash) < dfG_cnt].index.tolist()
    if len(upperhorse) == 0:
        upperhorse = dfG_cnt[dfG_cnt.isin([dfG_cnt.min()])].index.tolist()
    if len(lowerhorse) == 0:
        lowerhorse = dfG_cnt[dfG_cnt.isin([dfG_cnt.max()])].index.tolist()

    dfGhigh25 = dfG2[dfG2["horseId"].isin(upperhorse)]
    dfGlow25 = dfG2[dfG2["horseId"].isin(lowerhorse)]

    # 母の種牡馬で見る
    dftarget = dfbStallion
    dflist += [dftarget[dftarget["horseId"].isin(dfGhigh25["horseId"].unique())]["GenName"].value_counts().sort_values(
        ascending=False).head(pickup).reset_index().apply(lambda x: ", ".join([str(x) for x in x.tolist()]), axis=1)]
    dflist += [dftarget[dftarget["horseId"].isin(dfGlow25["horseId"].unique())]["GenName"].value_counts().sort_values(
        ascending=False).head(pickup).reset_index().apply(lambda x: ", ".join([str(x) for x in x.tolist()]), axis=1)]

    multiColumns += [[g[0], f"high{int(threash*100)}%", g[1]],
                     [g[0], f"low{int(threash*100)}%", g[1]]]
threash_col = [f"high{int(threash*100)}%", f"low{int(threash*100)}%"]
idfgen = pd.concat(dflist, axis=1).rename(
    index=lambda idx: f"{1+idx}位").fillna("")
idfgen.columns = pd.MultiIndex.from_tuples(multiColumns)
idfgen = idfgen.T.sort_index(key=lambda x: [({"ダ": 0, "芝": 1, f"high{int(threash*100)}%": 0} | {
                             k: n for n, k in enumerate("SMILE")}).get(ix, 1) for ix in x])
idfgen[idfgen.columns[:5]]
Out[32]:
      1位 2位 3位 4位 5位
high25% S キングカメハメハ, 5 アグネスタキオン, 4 フジキセキ, 4 クロフネ, 4 タイキシャトル, 3
M キングカメハメハ, 10 フレンチデピュティ, 10 フジキセキ, 6 ブライアンズタイム, 6 エンパイアメーカー, 5
I スペシャルウィーク, 5 キングカメハメハ, 2 マンハッタンカフェ, 2 アーミジャー, 1 タニノギムレット, 1
L キングカメハメハ, 2 ブライアンズタイム, 1 モンジュー, 1 サンデーサイレンス, 1 トワイニング, 1
low25% S スペシャルウィーク, 4 サンデーサイレンス, 4 クロフネ, 3 エイシンサンディ, 3 Afternoon Deelites, 2
M スペシャルウィーク, 9 フジキセキ, 7 サンデーサイレンス, 6 キングカメハメハ, 6 ダンスインザダーク, 5
I サンデーサイレンス, 5 サクラバクシンオー, 2 キングカメハメハ, 2 フレンチデピュティ, 1 Deputy Minister, 1
L フジキセキ, 1 サンデーサイレンス, 1      
high25% S ディープインパクト, 12 スペシャルウィーク, 9 サンデーサイレンス, 6 クロフネ, 6 サクラバクシンオー, 4
M ディープインパクト, 36 キングカメハメハ, 32 サンデーサイレンス, 17 アグネスタキオン, 15 スペシャルウィーク, 13
I キングカメハメハ, 17 クロフネ, 13 ディープインパクト, 11 サンデーサイレンス, 10 シンボリクリスエス, 9
L ディープインパクト, 11 キングカメハメハ, 10 サンデーサイレンス, 8 シンボリクリスエス, 6 クロフネ, 5
E ダンスインザダーク, 1 Machiavellian, 1 ディープインパクト, 1 Green Tune, 1 Acatenango, 1
low25% S フレンチデピュティ, 7 ブライアンズタイム, 7 フジキセキ, 4 キングカメハメハ, 4 ディープインパクト, 3
M サンデーサイレンス, 24 ディープインパクト, 16 アグネスタキオン, 14 フレンチデピュティ, 13 ダンスインザダーク, 13
I サンデーサイレンス, 13 アグネスタキオン, 8 クロフネ, 7 ダンスインザダーク, 6 キングカメハメハ, 5
L サンデーサイレンス, 6 シンボリクリスエス, 6 ダンスインザダーク, 5 キングカメハメハ, 4 クロフネ, 4
E デヒア, 1 Kingmambo, 1 トニービン, 1 Motivator, 1 キングカメハメハ, 1
 

ダートは種牡馬のときと同様に産駒数も少なく上位, 下位での違いが見られないが、
芝の方では上位ではディープインパクトが、下位ではサンデーサイレンスが目立つ

サンデーサイレンスの血を持つ母親が単純に多いだけの可能性もあるが、
とはいえほとんどの距離カテゴリでサンデーサイレンスが多い点から、
母父にサンデーサイレンスを持つ血統は序盤の活躍は少ない可能性がある。
また、上位で見るとそのほとんどがディープインパクトであることから、
ディープインパクトの種牡馬であるサンデーサイレンスの特徴は薄れ、
逆に早熟の特徴が母父にディープインパクトの血を持つ産駒には見られるようである

In [33]:
for field in ["ダ", "芝"]:
    idfg = idfgen.loc[field]
    ncols = len(idfg.loc[idfg.index[0][0]])
    nrows = len(threash_col)
    plt.figure(figsize=(27, 9))
    cnt = 1
    for row, idx in enumerate(threash_col, start=1):
        for cidx in "SMILE"[:ncols]:
            word_list = []
            for word in idfg.loc[(idx, cidx)].tolist():
                if word == "":
                    break
                word_list += [word.split(", ")[0]]*int(word.split(", ")[1])
            if len(word_list) == 0:
                break
            random.shuffle(word_list)
            plt.subplot(nrows, ncols, cnt)
            wordcloud = WordCloud(
                background_color="gray",
                width=800,
                height=800,
                font_path=font_path,
                colormap=colormap,
            ).generate(" ".join(word_list))
            plt.imshow(wordcloud, interpolation="bilinear")
            plt.title(", ".join([field, idx, cidx]))
            plt.axis("off")
            cnt += 1
    plt.show()
 
 
スポンサーリンク

2-8.まとめ

2-8-1.分かったこと

 
  • 2019年以降からレースの出走条件が変わり、上位クラスに出走していた古馬は下位クラスに出走できなくなったこととリステッドクラスが導入された
  • オープンクラスは、3勝クラスまでを順番に勝ち上がってから出場する必要はなく、運営が認めれば新馬戦を勝利後にいきなり重賞レース参加することができる
  • 実際に重賞クラスに勝利できる競走馬はその半分は8レース以内にその実力が現れることが分かった
  • 芝とダートで特定の血統を持つ産駒に成長度合いと成績に関係があるか調べたところ、芝の重賞クラスに勝利した競走馬の中で母父に限りそのような関係がありそうだと分かった。

2-8-2.決定事項

 
  1. モデルの予測対象は2019年以降のデータとする
  2. レースを9段階にグレード分けする

2-8-3.次回やること

 
  1. 血統と単勝・連対・複勝率の関係
  2. 芝とダートでモデルを分けるべきか否か
  3. どこまで血統を考慮すれば良いか確認

コメント

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