はじめに¶
私は競馬予想AIの開発をしています。動画で制作過程の解説をしています。良ければ見ていってください。
また、共有するソースの一部は有料のものを使ってます。
同じように分析したい方は、以下の記事から入手ください。
0.下準備¶
import pandas as pd
import warnings
import sys
sys.path.append("..")
from src.core.db.controller import execSQL, getTableList, getDataFrame
import numpy as np
import pathlib
from src.data_manager.preprocess_tools import DataPreProcessor
from src.data_manager.data_loader import DataLoader
import matplotlib.pyplot as plt
import japanize_matplotlib
import re
from wordcloud import WordCloud
import random
warnings.filterwarnings("ignore")
データベースからデータの読み込み
root = pathlib.Path(".").absolute().parent
dbpath = root / "data" / "keibadata.db"
dbpath
# 血統情報のテーブル一覧をDBから取得する
horseblood_list = [tbl for tbl in getTableList(dbpath) if "horseblood" in tbl]
horseblood_list.sort(key=lambda x: int(x[-4:]))
horseblood_list
血統情報のテーブル情報を取得する
dfblood = pd.concat([
getDataFrame(tablename, dbpath) for tablename in horseblood_list
], ignore_index=True)
dfblood
2000年から2023年分の出走情報も取得する
from src.data_manager.data_loader import DataLoader
start_year = 2000
end_year = 2023
data_loader = DataLoader(start_year, end_year, dbpath)
dataPreP = DataPreProcessor()
df = data_loader.load_racedata()
df
読み込んだ出走情報から、前処理を行う
df = dataPreP.exec_pipeline(df)
以上で準備完了。
1.血統データの分析¶
読み込んだ血統データは5代血統まであり分析が大変なので、まずは簡単に1代血統を調べてみて、何が分かるか調べてみる
# よって、1代血統の情報だけに絞り込み
dfgen1 = dfblood[["horseId", "gen1", "gen1ID"]].drop_duplicates(subset=["horseId", "gen1ID"], ignore_index=True)
dfgen1["gen1"] = dfgen1["gen1"].str.split("\n", n=1, expand=True)[0]
dfgen1
# 出走情報に1代血統の情報を追加する
# 1代血統には、母と父の情報が入っている。
# ちょっと見てみる
dfgen1
一つのhorseIdに2つの親のhorseIdが紐づいている
# なので配列の奇数番目と偶数番目を参照できるやつを使って、
# 種牡馬の情報と繫殖牝馬の情報に分ける
stallion_dict = dfgen1.loc[dfgen1.index.tolist()[::2]].set_index("horseId").to_dict()
# ↑これで種牡馬のみ取り出せる
# 同じ感じで繫殖牝馬も手に入れる
breed_dict = dfgen1.loc[dfgen1.index.tolist()[1::2]].set_index("horseId").to_dict()
種牡馬と繫殖牝馬の二つを出走情報へ追加する
df["stallionName"] = df["horseId"].map(stallion_dict["gen1"])
df["stallionId"] = df["horseId"].map(stallion_dict["gen1ID"])
df["breedName"] = df["horseId"].map(breed_dict["gen1"])
df["breedId"] = df["horseId"].map(breed_dict["gen1ID"])
df[["stallionId", "stallionName", "breedId", "breedName"]].tail(20)
2.種牡馬の調査¶
父親の産駒の出走数の分布を確認してみる
dfstallion_cnt = df.groupby(["stallionId", "stallionName"])["horseId"].count().sort_values()
dfstallion_cnt
# ディープインパクトやキングカメハメハが大量にいるから、偏りが激しそう
dfstallion_cnt.describe()
1頭の種牡馬の産駒たちの合計出走回数の平均が500回強であるにも関わらず、
全体の分布の中央値は42回しかない
さらに言えば、75%分位つまり4分の3の種牡馬の産駒は200回程度までしか走っていない。
恐ろしく偏りがすさまじい
あくまでこれは出走回数の話なので、産駒の数で見てみる
dfgen1.loc[dfgen1.index.tolist()[::2]].groupby(["gen1ID", "gen1"])["horseId"].count().sort_values()
やっぱり当然ながら偏りが激しい
# 分布も確認
dfgen1.loc[dfgen1.index.tolist()[::2]].groupby(["gen1ID", "gen1"])["horseId"].count().describe()
出走回数と大差ない結果
やはり種牡馬は偏りが激しく、特定の種牡馬に産駒が集中する
他にも調べてみたが、繫殖牝馬はあまり偏りは大きくない(とは言っても少し偏りはある)
とはいえ、やはり調べると母より母父の方が大事だと分かった。
つまり、父と母父、この二つで血統の偏りが見られることから、
父系の血統を見るのが重要なのではないかと容易に想像できる。
3.父と母父に絞って様子を見る¶
まずは母父の情報を追加しないといけない
ので、以下の関数を作ります。
from typing import Literal
def blood_analyze(mode: Literal["s", "b", "sb", "bs", "ss", "bb"]):
index_map = {
"s": (0, "stallion", "gen1"), "ss": (0, "sStallion", "gen2"), "sb": (8, "sBreed", "gen2"),
"b": (16, "breed", "gen1"), "bs": (16, "bStallion", "gen2"), "bb": (24, "bBreed", "gen2")
}
idx, prefix, genCol = index_map[mode]
idf = dfblood.iloc[idx::32].reset_index(drop=True)
idf[genCol] = idf[genCol].str.split("\n", expand=True, n=1)[0]
horseId = "horseId"
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)[f"{genCol}"])
return df
以下のように使う。簡単に親たちの情報を出走情報についかできる
df = blood_analyze("bs")
df[["horseId", "stallionId", "stallionName", "bStallionId", "bStallionName"]]
レース条件ごとに産駒の分布に違いがあるのか確かめる
# とりあえずレース条件の種類を確認したい
df["raceDetail"].unique()
レースのクラス分けは、昔は獲得賞金で決まっていたそうだが、最近だと勝利回数でクラスが分けられる
詳しい内容は調べると早い
なので、以下の表のようにグレードを用意して対応付けを分かりやすくする
獲得賞金 | クラス | グレード |
---|---|---|
0 | 新馬, 未勝利 | 0 |
500万下 | 1勝クラス | 1 |
1000万下, 900万下 | 2勝クラス | 2 |
1600万下 | 3勝クラス | 3 |
それ以上 | オープン | 4 |
— | G3 | 5 |
— | G2 | 6 |
— | G1 | 7 |
グレード分けする関数を作る
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
}
# 以下のようにしてraceGradeカラムを追加する
idf = df["raceName"].map(map_race_grade)
idf = idf[idf.notna()]
df["raceGrade"] = pd.concat([idf, df[~df.index.isin(idf.index)]["raceDetail"].map(grade_mapping_dict)])
# G1のレースに絞ってみて、ちゃんとグレード分けできたか確認
df[df["raceGrade"].isin([7])]["raceName"].unique()
多分イケて走
4.重賞とレース条件に絞り込んで調査¶
ということでここから重賞クラスに絞って調べてみる
# 年度ごとに調べるようにする
df["year"] = df["raceDate"].dt.year
# レース条件ごとに調べる
field_dist = ["field", "dist_cat"]
dfg_fd = df[df["raceGrade"].isin([5,6,7])].groupby(field_dist)
レース条件ごとに年度別で重賞クラスに出走した競走馬の種牡馬と母父の分布を確認する
ワードクラウドを使って視覚的に確認する。
ワードクラウドは対象のテキストに含まれている単語の分布を文字の大きさから判断できるもの
文字が大きければ大きいほどたくさんその単語があることを意味する。
ワードクラウドを使うにはwordcloudというライブラリをインストールする必要がある。もうpoetry経由でインストール済
5.WordCloudを使って種牡馬産駒の出走回数の可視化¶
colormap = "RdYlBu" # 見やすそうなカラーマップなら何でもいい
# 日本語フォントを使う
font_path = "C:/Windows/Fonts/meiryo.ttc"
# linuxの場合は以下にあるかも(Macも同じ?)
# font_path="/usr/share/fonts/truetype/fonts-japanese-gothic.ttf"
group_list = []
for group in [(f, d) for f in ["芝", "ダ"] for d in "SMILE" if f!="ダ" or d!="E"]:
idf = dfg_fd.get_group(group)
print(",".join(list(group)+["重賞クラス"]))
plt.figure(figsize=(24,9))
year_list = idf["year"].unique()
nyear = idf["year"].nunique()
rows = sum([True, nyear > 8, nyear > 16])
cols = 8
for idx, (year, idfy) in enumerate(idf.groupby("year"), start=1):
group_list += [idfy[["year", "stallionName"] + ["field", "dist_cat"]].value_counts().sort_values().tail(1).reset_index()]
idfyg = idfy[["year", "stallionId", "stallionName"]].value_counts().sort_values().reset_index()
word_list = []
for row in idfyg.to_dict(orient="records"):
word_list += [row["stallionName"]]*row["count"]
random.shuffle(word_list)
plt.subplot(rows, cols, idx)
wordcloud = WordCloud(
background_color="white",
width=800,
height=800,
font_path=font_path,
colormap=colormap
).generate(" ".join(word_list))
plt.imshow(wordcloud, interpolation="bilinear")
plt.title(year)
plt.axis("off")
plt.show()
別で貯めこんでいた情報を以下で分析する。
各レース条件と年度に対して、もっとも産駒が出走した種牡馬を一覧で出してる
dfgp = pd.concat(group_list)
idfgp = dfgp.groupby(["field", "dist_cat"])
dflist = []
index_list = []
for field in ["芝", "ダ"]:
dist_list = "SMILE" if field == "芝" else "SMIL"
for dist in dist_list:
idfg = idfgp.get_group((field, dist))
dflist += idfg.set_index("year")[["stallionName"]].T.to_dict(orient="records")
index_list += ["-".join([field, dist])]
display(pd.DataFrame(dflist, index=index_list).T)
ぱっと見で、芝はサンデーサイレンスとディープインパクトが目立ち、ダートはかろうじてキングカメハメハが目立つ結果になっている
こうしてみると、芝は血統がかなり偏っている中、ダートはこれといった特徴的な血統が見れない
よって、ダートで活躍できる強い血統はなかなか出てこないように感じる。
6.WordCloudを使って母父の産駒の出走回数の可視化¶
同じように母父でも見てみる
roup_list = []
for group in [(f, d) for f in ["芝", "ダ"] for d in "SMILE" if f!="ダ" or d!="E"]:
idf = dfg_fd.get_group(group)
print(", ".join(list(group)+["重賞クラス"]))
# idf = idf[idf["label"].isin([1,])]
plt.figure(figsize=(24, 9))
year_list = idf["year"].unique()
nyear = idf["year"].nunique()
rows = sum([True, nyear>=9, nyear>=17])
cols = 8
for idx, (year, idfy) in enumerate(idf.groupby("year"), start=1):
group_list += [idfy[["year", "bStallionName"]+["field", "dist_cat"]].value_counts().sort_values().tail(1).reset_index()]
idfyg = idfy[["year", "bStallionId", "bStallionName"]].value_counts().sort_values().reset_index()
word_list = []
for row in idfyg.to_dict(orient="records"):
word_list += [row["bStallionName"]]*row["count"]
random.shuffle(word_list)
plt.subplot(rows, cols, idx)
wordcloud = WordCloud(
background_color="white",
width=800,
height=800,
font_path=font_path,
colormap = colormap,
).generate(" ".join(word_list))
plt.imshow(wordcloud, interpolation="bilinear")
plt.title(year)
plt.axis("off")
plt.show()
dfgp = pd.concat(group_list)
idfgp = dfgp.groupby(["field", "dist_cat"])
dflist = []
index_list = []
for field in ["芝", "ダ"]:
dist_list = "SMILE" if field == "芝" else "SMIL"
for dist in dist_list:
idfg = idfgp.get_group((field, dist))
dflist += idfg.set_index("year")[["bStallionName"]].T.to_dict(orient="records")
index_list += ["-".join([field, dist])]
display(pd.DataFrame(dflist, index=index_list).T)
芝で見るとサンデーサイレンスがほぼ一強、しかし直近だとディープインパクトが台頭してきている。
ディープインパクトは2019年没であるため、2020年以降からは2世とした産駒が多く出てくる可能性があり、母父にはディープインパクトといった流れが今後出てくるかもしれない。
またダートでみると、牡馬の時と違いMとの距離カテゴリでサンデーサイレンスが目立つ。
結果として
サンデーサイレンスの競馬界への影響がとてつもなく強い
サンデーサイレンスだけでも詳しく調べてみる価値はあるように思う。
コメント