学習する(2:血統)

学習

血統データのマージ

 血統データを取得して、マージします。次にこれらの血統データから得意な特性をまとめていきます。

# -------------------------------------------------------------
# 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_idmother_id を付与。
  • 血統キーの生成: father_idmother_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)。以降の累積・シフト系はこの順序を前提に実行。
  • 勝敗フラグ着順==1is_win(int8) として軽量に保持。
  • 騎手勝率(直前まで)
    • 騎手ごとに cumcount()出走数(当該行を含む回数) を算出。
    • 勝利数は is_win1レース前に 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) と累積の組み合わせ)でデータリークを防ぎつつ、groupbycumsumtransform完全ベクトル化 で処理を高速化している点です。

コメント

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