はじめに
本シリーズでは、動画として解説しなかった前処理箇所の詳細な部分を解説しています。
ロードマップ2で決定したモデル作成時の前提一覧については以下のページを参照ください。
競馬予想プログラムソフト開発の制作過程動画リスト
このNoteBookの趣旨
前処理部分の開発にあたり、前提を実現するための処理である項目5番~項目8番のソースを公開します。
※今後も前提が増えていくと思います。前提が増えるたびに本ページに追記していくのは読みやすさの観点から良くないと思いますのでページを分けます。すみません。
Notebook形式で公開していますが、実際の本ソフトで使用する前処理箇所はBookersで公開しています。(チャンネル登録で250円で購入できます。)
本Notebookと有料ソースの違いはモジュール化しているかの違いしかないです。以下の解説ソースを参照して、自分でモジュール化できる方は自力でする方がいいと思います。
250円というのはモジュール化の手間賃だと思っていただければ。(実際時給換算すると250円払った方がリーズナブルと思います。)
Bookers記事一覧ページの『ロードマップ3 – 前処理』を参照ください。
前処理箇所の作成:項目5~8の処理内容解説
0. 必要なモジュールのインポートと競馬データのロード¶
一部有料ソースを使用しています。Bookersで公開していますので、一緒に競馬予想プログラムを開発したい方はぜひ購入ください。
Bookersリンク:https://bookers.tech/post/82ee8cf4-e95d-4f67-8cef-c038dbe0c957/
import pandas as pd
import numpy as np
import sys
import warnings
sys.path.append(".")
from src.core.db.controller import getDataFrame # noqa
warnings.filterwarnings("ignore")
# 競馬データのロード
dbpath = "./data/keibadata.db"
start_year = 2010
end_year = 2023
year_list = list(range(start_year, end_year+1))
# 前回実施分をロード
import pickle, pathlib
load_dir = pathlib.Path("./data/data_prep")
with open(load_dir / "process1_to_4.pkl", "rb") as f:
df: pd.DataFrame = pickle.load(f)
5.タイム関連データは分速に変換¶
以下のタイム関連データを分速または割合に変換します。
- レースのペース情報: “f3Ftol3F”, “rapTime”, “rapSumTime”
- お馬さんタイム情報: “last3F”, “time”
5-1. レースのペース情報の処理¶
レースのペース情報は、基本的にリストとして扱います。
あとでリストごとに処理などができるので、カラムに展開するのはやめます。
ただし、”f3Ftol3F”カラムについては、最初の3Fと最後の3Fの通過タイムで必ず2カラムになるので、
このカラムに関しては展開します。
展開の仕方は、str.extractメソッドを使うことで、正規表現として「();括弧」で括った場所を取り出す処理です。
あとは、各々の情報に関しては、距離をタイムで除算して、60を掛けて分速に変換してます。
# f3Ftol3Fは、最初の3Fと上り3Fのペースタイム情報が入っているので、expandで二つに分ける
df_l3Ftol3F = df["f3Ftol3F"].str.extract(
r"(\d+\.\d)-(\d+\.\d)", expand=True).astype(
{0: float, 1:float}).rename(columns={0: "f3F", 1: "l3F"})
# 速度情報の追加
df_l3Ftol3F[["f3F_vel", "l3F_vel"]] = 60*600/df_l3Ftol3F.values
# 元のDFに統合
df = pd.concat([df, df_l3Ftol3F], axis=1)
# ペース情報は、一旦リストのままで処理する
df["rapTime"] = df["rapTime"].apply(lambda data: [float(d.strip()) for d in data.split("-")])
df["rapSumTime"] = df["rapSumTime"].apply(
lambda data: [float(d.strip()) for d in data.split("-")])
df["rapTime_vel"] = df["rapTime"].apply(lambda data_list: (60*200/np.array(data_list)).tolist())
df["rapSumTime_vel"] = df["rapSumTime"].apply(
lambda data_list: [60*200*i/d for i, d in enumerate(data_list, start=1)])
5-2. お馬さんタイム情報¶
お馬さんのタイム情報は、単純に対象の距離で除算して60を乗じます。
また、新しいカラムとして、上り3Fに到達するまでの速度も算出します。
つまり、最後の600mに到達するまでの速度を意味します。
# 走破速度
df["velocity"] = 60*df["distance"] / df["time"]
# 上り3Fの速度
df["last3F_vel"] = 60*600 / df["last3F"]
# 上り3Fに至るまでの速度
df["toL3F_vel"] = 60*(df["distance"]-600) / (df["time"]-df["last3F"])
6.着順データは出走頭数で除算する¶
着順データに関しては、出走頭数で除算します。
こうすることで、18頭立てで5着になった場合と10頭立てで5着になった場合の度合いを比較することができます。
“passLabel”については、コーナー通過順位を表しているがレース距離ごとにコーナー数が違うため、
ペース情報と同様にリストのまま処理することとする。
また、追加で最終着順と最終コーナー通過順位との差も算出する。
# かなり強引だが以下のようなfor文で、最大4コーナー分の通過順位を出せる
# ポイントは、DFのfor文を回す前にnumpyのarrayに変換している
expand_pass_label_list = []
for hNum, passLabel in df[["horseNum", "passLabel"]].values:
insert_data = {f"label_{i}C": None for i in range(1, 5)}
# passLabelを文字列分割してinsert_dataのDictにデータを入れる。
# 4コーナー分ないものはNoneのまま入る
for num, d in enumerate(passLabel.split("-"), start=1):
insert_data[f"label_{num}C"] = int(d)/hNum
expand_pass_label_list += [insert_data.copy()]
# 統合できるようにDF化
dfpass = pd.DataFrame(expand_pass_label_list)
# 元のDFに統合
df = pd.concat([df, dfpass], axis=1)
# 最終着順(走破タイム順)も出走馬数で割る
df["label_rate"] = df.groupby("raceId")["time"].rank() / df["horseNum"]
# 最終コーナー通過時の値のみを取り出す
df["label_lastC"] = df[["passLabel", "horseNum"]].apply(
lambda row: int(row["passLabel"].split("-")[-1].strip())/row["horseNum"], axis=1)
# 最終コーナー通過時と最終着順との差を計算
df["label_diff"] = df["label_rate"] - df["label_lastC"]
7.レース距離はSMILE距離区分を用いてカテゴリ化¶
レース距離が細かく分けられているため、よりモデル作成に役立てられるように
レース距離をSMILE距離区分でカテゴリ化する。
SMILEとは?
ワールド・ベスト・レースホース・ランキングで競走馬の能力を数値化するために、
距離区分を5段階に分けてレーティングしている。
文字 | 元の語 | 距離(m) |
---|---|---|
S | sprint | 1000 – 1300 |
M | mile | 1301 – 1899 |
I | intermediate | 1900 – 2100 |
L | long | 2101 – 2700 |
E | extended | 2701 以上 |
# カテゴリ分けする関数を作成して、map関数で変換する
def sort_category(dist):
if dist > 2700:
return "E"
if dist > 2100:
return "L"
if dist > 1899:
return "I"
if dist > 1300:
return "M"
return "S"
df["dist_cat"] = df["distance"].apply(sort_category)
8.カテゴリ変数はエンコード¶
カテゴリに関連するカラムをすべて数字にエンコードしておく
カテゴリ化はカテゴリエンコーディングという分野であり、モデルによってはエンコードしてあげる必要がある。
カテゴリのエンコーディングには、ライブラリが用意されてたりするが、
いくつかのカラムは順序尺度になっているものもあるため、自力でエンコーディングする。
エンコーディング対象のカラム
- place
- field
- sex
- condition:順序尺度
- jockeyId
- teacherId
- dist_cat:順序尺度
以下の方法でエンコーディングする。
あとでデコードできるように、エンコード結果は別のカラムに分ける。
# 1. placeカラムのエンコード
df["pId"] = df["raceId"].apply(lambda d: int(d[4:6]))
place_encode = {key: val-1 for key,
val in df.set_index("place")["pId"].to_dict().items()}
del df["pId"]
df["place_en"] = df["place"].map(place_encode)
# 2. fieldカラムのエンコード
df["field_en"] = df["field"].map({"芝": 0, "ダ": 1,})
# 3. sexカラムのエンコード
df["sex_en"] = df["sex"].map({"牡": 0, "牝": 1, "セ": 2})
# 4. conditionカラムのエンコード:順序尺度
df["condition_en"] = df["condition"].map({"良": 3, "稍重": 2, "重": 1, "不良": 0})
# 5-6. jockeyIdカラムとteacherIdカラムをまとめてエンコード
for col in ["jockeyId", "teacherId"]:
encoder_map = {
val: num for num, val in enumerate(df[col].unique())}
df[col+"_en"] = df[col].map(encoder_map)
# 7. dist_catカラムのエンコード:順序尺度
df["dist_cat_en"] = df["dist_cat"].map({"S": 0, "M": 1, "I": 2, "L": 3, "E": 4})
# 最後にエンコードしたカラムをすべてcategory型に変換する
for col in ["place", "field", "sex", "condition", "jockeyId", "teacherId", "dist_cat"]:
target_col = col + "_en"
df[target_col] = df[target_col].astype("category")
実際にエンコードされたか確認
category_columns = [col+"_en" for col in ["place", "field", "sex", "condition", "jockeyId", "teacherId", "dist_cat"]]
df[category_columns].info()
<class 'pandas.core.frame.DataFrame'> RangeIndex: 659571 entries, 0 to 659570 Data columns (total 7 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 place_en 659571 non-null category 1 field_en 659571 non-null category 2 sex_en 659571 non-null category 3 condition_en 659571 non-null category 4 jockeyId_en 659571 non-null category 5 teacherId_en 659571 non-null category 6 dist_cat_en 659571 non-null category dtypes: category(7) memory usage: 5.7 MB
データの保存¶
# 次回以降も使うため、バイナリで残しておく
load_dir = pathlib.Path("./data/data_prep")
with open(load_dir/"process5_to_8.pkl", "wb") as f:
pickle.dump(df, f)
コメント