血統データのマージ
血統データを取得して、マージします。次にこれらの血統データから得意な特性をまとめていきます。
# -------------------------------------------------------------
# 3. 血統データマージ
# -------------------------------------------------------------
print("✅ 血統データマージ")
bloodline_file = "bloodline_id_data.csv"
final_df = pd.read_csv(bloodline_file, encoding="utf-8-sig").drop_duplicates(subset='horse_id')
# 血統データをマージ
data = (
data.merge(
final_df[['horse_id', 'father_id', 'mother_id']],
left_on='馬ID',
right_on='horse_id',
how='left'
)
.drop(columns='horse_id')
)
# 血統キー
data['血統キー'] = data['father_id'].astype(str) + '_' + data['mother_id'].astype(str)
# 日付順にソート
data = data.sort_values('日付').reset_index(drop=True)
# 好成績データだけを対象に履歴集計用のフラグを作成
for col in ['距離', '脚質', '馬場']:
data[f'{col}フラグ'] = data['好成績'] * 1
def cumulative_feature(df, key_col, date_col, category_col, flag_col, result_col):
# ピボットテーブル
pivot_df = (
df.pivot_table(
index=[key_col, date_col],
columns=category_col,
values=flag_col,
aggfunc='sum',
fill_value=0
)
)
# 累積和(数値列のみ)
pivot_df = pivot_df.groupby(level=0).cumsum()
# 血統得意カテゴリの抽出
pivot_df[result_col] = pivot_df.idxmax(axis=1)
# indexを戻す
pivot_df = pivot_df.reset_index()[[key_col, date_col, result_col]]
return pivot_df
# 3種類まとめて集計
距離集計 = cumulative_feature(data, '血統キー', '日付', '距離カテゴリ', '距離フラグ', '血統得意距離')
脚質集計 = cumulative_feature(data, '血統キー', '日付', '脚質', '脚質フラグ', '血統得意脚質')
馬場集計 = cumulative_feature(data, '血統キー', '日付', '馬場', '馬場フラグ', '血統得意馬場')
# まとめてマージ(1回だけ)
for df_merge in [距離集計, 脚質集計, 馬場集計]:
data = data.merge(df_merge, on=['血統キー', '日付'], how='left')
# NaN を "不明" で補完
data[['血統得意距離', '血統得意脚質', '血統得意馬場']] = (
data[['血統得意距離', '血統得意脚質', '血統得意馬場']].fillna("不明")
)
このコードは、血統情報をレースデータに結合し、血統(父×母の組合せ)ごとに 過去の好成績傾向(距離・脚質・馬場)を累積的に集計して「得意カテゴリ」を推定する処理です。主なステップは以下の通りです。
- 血統データ読込&重複排除:
bloodline_id_data.csv
を読み込み、horse_id
重複を除去してクリーンな血統テーブルを作成。 - 左外部結合(merge): レース側の
馬ID
と血統側のhorse_id
を突合し、father_id
・mother_id
を付与。 - 血統キーの生成:
father_id
とmother_id
を連結して血統キー
を作成(父母ペア単位で集計可能に)。 - 時系列整備:
日付
昇順にソートして、時系列の一貫性(リーク防止)を確保。 - 履歴集計用フラグ:
好成績
(3着以内)を基準に、距離/脚質/馬場
それぞれのフラグ列を作成(好成績のみカウント)。 - 累積集計ロジック(
cumulative_feature
):血統キー×日付
を行、カテゴリ(例:距離カテゴリ)を列、フラグ合計を値とする ピボットテーブル を作成。- 血統キー単位で 累積和(cumsum) を取り、日付時点までの通算好成績回数を算出。
- 各時点で 最も累積回数が多いカテゴリ(
idxmax
) を「血統得意◯◯」として抽出。
- 3種類の得意カテゴリを作成:
血統得意距離
(距離カテゴリ)血統得意脚質
(逃げ/先行/差し/追込)血統得意馬場
(馬場状態)
それぞれを 1 回のループで まとめてマージ。
- 欠損補完: 推定できない箇所は
"不明"
で補完。
結果として、各レース時点で その血統が歴史的に好成績を上げやすい条件(距離・脚質・馬場)を特徴量として付与でき、モデルの説明力向上やハンディキャップ設計に活用できます。
他の特徴量の追加と不要となた列の削除
print("✅ 騎手勝率 ほか特徴量(高速+dtype対策)")
# ---- 0) 一度だけ安定ソート(以降の累積系はこの順で実行)----
data = data.sort_values(["日付", "レースID"], kind="mergesort").reset_index(drop=True)
# ---- 1) 勝敗フラグ(軽量型)----
data["is_win"] = (data["着順"] == 1).astype("int8")
# ---- 2) 騎手勝率(直前まで): 完全ベクトル化 ----
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)
# ---- 3) 走破速度 ----
data["走破速度"] = data["距離"] / data["走破タイム"]
# ---- 4) 過去n走の平均(直前まで): dtype整えてから一撃 ----
cols = ["走破速度", "上り", "単勝", "人気", "着順"]
# ★ ここがポイント:cumsum 対象を数値化(object混入を排除)
data[cols] = data[cols].apply(pd.to_numeric, errors="coerce").astype("float32")
# 同じ順序のままキー指定 groupby で cumsum
cs = data[cols].groupby([data["馬ID"], data["芝ダ"]], sort=False).cumsum()
cnt = data.groupby(["馬ID", "芝ダ"], sort=False).cumcount()
past = (cs - data[cols]).div(cnt.replace(0, np.nan), axis=0).fillna(0.0)
past.columns = [f"過去{c}" for c in cols]
data[past.columns] = past
# ---- 5) 脚質過去 / 履歴長 ----
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)
# ---- 6) 騎手-馬コンビ勝率(直前まで)----
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)
# ---- 7) 相対特徴量(transform まとめて1回)----
race_means = data.groupby("レースID", sort=False)[["斤量", "騎手勝率", "過去走破速度"]].transform("mean")
data["相対斤量差"] = data["斤量"] - race_means["斤量"]
data["相対騎手勝率差"] = data["騎手勝率"] - race_means["騎手勝率"]
data["相対過去速度"] = data["過去走破速度"] - race_means["過去走破速度"]
# ---- 8) 不要列削除(そのまま)----
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 保存済み")
このコードは、レース前時点の情報だけを使って 騎手・馬の実績系特徴量 を高速に生成し、最後に不要列を整理する処理です。リーク防止のために 安定ソート+shift(1)
+累積演算 を徹底し、dtype
を軽量化して計算負荷を抑えています。主な内容は次のとおりです。
- 時系列の確定:
日付, レースID
で 安定ソート(mergesort)。以降の累積・シフト系はこの順序を前提に実行。 - 勝敗フラグ:
着順==1
をis_win(int8)
として軽量に保持。 - 騎手勝率(直前まで):
- 騎手ごとに
cumcount()
で 出走数(当該行を含む回数) を算出。 - 勝利数は
is_win
を 1レース前にshift(1)
してから騎手ごとにcumsum()
→ 直前までの累計勝利数 を得る。 - 分母 0 を除外して 騎手勝率 = 勝利数 / 出走数(直前まで) を計算(欠損は 0 で補完)。
- 騎手ごとに
- 走破速度:
距離 / 走破タイム
を算出(後段の移動平均に使用)。 - 過去 n 走の平均(直前までの平均):
走破速度, 上り, 単勝, 人気, 着順
をfloat32
に統一(object 混入排除)。- 馬×芝ダごとに
cumsum()
とcumcount()
を取り、(累積合計 − 現行値)/(回数 − 1) の形で 当該行直前までの平均 を一括算出(分母 0 は 0 で補完)。 - 生成列は
過去走破速度, 過去上り, 過去単勝, 過去人気, 過去着順
。
- 履歴系特徴:
脚質過去
:馬ごとに 1 走前の脚質 を付与(欠損は “0”)。履歴長
とその正規化:馬ごとの 出走回数(0 始まり) とmax
で割った 正規化値。
- 騎手-馬コンビ勝率(直前まで):
- 馬×騎手の組み合わせで
cumcount()
と、is_win.shift(1)
→cumsum()
を用い、直前までの勝率 を算出。 - 中間列(出走数・勝利数)は削除してメモリ削減。
- 馬×騎手の組み合わせで
- 相対特徴(同レース内の基準差):
- レース単位で
斤量, 騎手勝率, 過去走破速度
の 平均 をtransform("mean")
で取得。 - 自身の値から平均を引き、相対斤量差 / 相対騎手勝率差 / 相対過去速度 を作成。
- レース単位で
- 不要列の整理と記録:
- 学習に不要・重複する派生元などを一括削除。
- 削除カラム一覧を
final_drop_columns.json
に保存して、前処理の再現性 を確保。
ポイントは、「直前まで」しか使わない 設計(shift(1)
と累積の組み合わせ)でデータリークを防ぎつつ、groupby
+cumsum
+transform
の 完全ベクトル化 で処理を高速化している点です。
コメント