「直前までの情報だけ」で実績と傾向を要約し、レース内の相対差で正規化した、リークに強い特徴量セットを構築
このコードは、["日付","レースID"]
の安定ソートを土台に、騎手・馬・コンビ(馬×騎手)の直前までの実績指標や過去平均をベクトル化して作成し、さらに同レース内の相対差に正規化したうえで、学習に不要な素性を整理する前処理です。
data = data.sort_values(["日付", "レースID"], kind="mergesort").reset_index(drop=True)
data["is_win"] = (data["着順"] == 1).astype("int8")
g_j = data.groupby("騎手ID", sort=False)
data["出走数"] = g_j.cumcount()
data["勝利数"] = (
g_j["is_win"].shift(1).fillna(0)
.groupby(data["騎手ID"], sort=False).cumsum()
.astype("int32")
)
den = data["出走数"].replace(0, np.nan)
data["騎手勝率"] = (data["勝利数"] / den).fillna(0.0)
data["走破速度"] = data["距離"] / data["走破タイム"]
cols_no_agari = ["走破速度", "単勝", "人気", "着順"]
for c in cols_no_agari + ["上り"]:
data[c] = pd.to_numeric(data[c], errors="coerce")
g_mb = data.groupby(["馬ID", "芝ダ"], sort=False)
sum_prev = g_mb[cols_no_agari].apply(lambda df: df.shift(1).fillna(0).cumsum()) \
.reset_index(level=[0,1], drop=True)
cnt_prev = g_mb.cumcount()
past = (sum_prev).div(cnt_prev.replace(0, np.nan), axis=0).fillna(0.0)
past.columns = [f"過去{c}" for c in cols_no_agari]
data[past.columns] = past
agari_sum_prev = g_mb["上り"].apply(lambda s: s.shift(1).fillna(0).cumsum()) \
.reset_index(level=[0,1], drop=True)
data["過去上り"] = (agari_sum_prev / cnt_prev.replace(0, np.nan)).fillna(0.0).to_numpy()
data[["過去走破速度","過去単勝","過去人気","過去着順","過去上り"]] = \
data[["過去走破速度","過去単勝","過去人気","過去着順","過去上り"]].astype("float32")
# ---- 生の上りはdrop ----
data.drop(columns=["上り"], inplace=True, errors="ignore")
data["脚質過去"] = data.groupby("馬ID", sort=False)["脚質"].shift(1).fillna("0")
data["履歴長"] = data.groupby("馬ID", sort=False).cumcount()
data["履歴長_norm"] = data["履歴長"] / max(int(data["履歴長"].max()), 1)
g_pair = data.groupby(["馬ID", "騎手ID"], sort=False)
data["騎手-馬ペア出走数"] = g_pair.cumcount()
data["騎手-馬ペア勝利数"] = (
g_pair["is_win"].shift(1).fillna(0)
.groupby([data["馬ID"], data["騎手ID"]], sort=False).cumsum()
)
data["騎手-馬コンビ勝率"] = (
data["騎手-馬ペア勝利数"] / data["騎手-馬ペア出走数"].replace(0, np.nan)
).fillna(0.0)
data.drop(columns=["騎手-馬ペア出走数", "騎手-馬ペア勝利数"], inplace=True)
race_means = data.groupby("レースID", sort=False)[["斤量", "騎手勝率", "過去走破速度"]].transform("mean")
data["相対斤量差"] = data["斤量"] - race_means["斤量"]
data["相対騎手勝率差"] = data["騎手勝率"] - race_means["騎手勝率"]
data["相対過去速度"] = data["過去走破速度"] - race_means["過去走破速度"]
final_drop = ["走破速度","走破タイム", "上り", "性齢", "単勝", "人気", "is_win", "脚質", "通過",
"過去単勝", "履歴長", "年齢", "天候", "father_id", "mother_id"]
data.drop(columns=final_drop, inplace=True, errors="ignore")
with open("final_drop_columns.json", "w", encoding="utf-8") as f:
json.dump(final_drop, f, ensure_ascii=False, indent=2)
print("✅ final_drop_columns 保存済み")
主なポイントは次のとおりです。
- 時系列の固定(リーク防止の基盤)
mergesort
で["日付","レースID"]
を安定ソート → 以降のshift(1)
・cum*
は**必ず「直前まで」**を参照。 - 勝敗フラグと騎手勝率(直前まで)
is_win = (着順==1)
をint8
で作成。騎手ごとに出走数 = cumcount()
(当該行含む回数)勝利数 = shift(1) → cumsum()
(当該行を除いた累計勝利)騎手勝率 = 勝利数 / 出走数(0割は NaN→0 補完)
とすることでリークのない勝率を得ます。
- 走破速度の算出と数値化
走破速度 = 距離 / 走破タイム
。あわせて["走破速度","単勝","人気","着順","上り"]
をpd.to_numeric(..., errors="coerce")
で型を厳密化(混在型や文字を排除)。 - 馬×芝ダ別の「直前までの過去平均」
g_mb = groupby(["馬ID","芝ダ"])
のもと、sum_prev = shift(1)→fillna(0)→cumsum()
(直前までの累積合計)cnt_prev = cumcount()
(当該行含む回数)過去◯ = 累積合計 / cnt_prev(0 は NaN→0 補完)
を用いて、過去走破速度・過去単勝・過去人気・過去着順
を一括生成。
上りは同じ要領で過去上り
を計算し、最終的にこれらの列をfloat32
に統一します。
- 直前脚質・履歴長
馬ごとの脚質過去 = shift(1)
、履歴長 = cumcount()
、履歴長_norm = 履歴長 / max(履歴長)
を作成し、一走前情報と経験量を表現。 - 騎手×馬コンビ勝率(直前まで)
groupby(["馬ID","騎手ID"])
でペア出走数 = cumcount()
ペア勝利数 = is_win.shift(1) → cumsum()
騎手-馬コンビ勝率 = ペア勝利数 / ペア出走数(0 は NaN→0 補完)
を算出し、中間列(出走数・勝利数)は削除してメモリを節約。
- 同レース内の相対特徴(平均との差)
groupby("レースID").transform("mean")
で 斤量・騎手勝率・過去走破速度 のレース平均を求め、相対斤量差 / 相対騎手勝率差 / 相対過去速度
を作成。当日コンディション等の共通要因をキャンセルし、比較可能性を高めます。 - 最終の列整理と記録
学習に不要・派生元の中間列をfinal_drop
に基づいて削除し、一覧をfinal_drop_columns.json
に保存。前処理の再現性を確保します。
コメント