LGBM 特徵工程:賽馬模型的特徵怎麼建?

為什麼特徵工程比模型選擇更關鍵?本文拆解我們內部模型的四層特徵構造。

為什麼特徵工程比模型更關鍵?

在機器學習競賽圈有一句名言:「特徵決定上限,模型只是逼近這個上限。」對於賽馬 LGBM 模型,這句話更為真實——因為賽事數據噪聲大、信號弱、結構不穩定。 再精細的模型也救不了糟糕的特徵;反過來,即使簡單的 GBDT 配上好特徵,也能跑贏複雜神經網絡。

我們的特徵分層

我們把賽馬特徵切成四層:

層級內容更新頻率
靜態層馬匹性別、年齡、血統、馬主、練馬師、騎師賽事開始前固定
歷史層近 5 / 10 / 30 場表現均值、勝率、練馬師勝率每場後更新
即時層當日賠率變動、騎師近期狀態、天候、草地狀況每分鐘
衍生層隱含機率、速度評分、配速合適度即時計算

第一層:靜態特徵

這層沒什麼花活,重點是別遺漏閘位——閘位對草地短途影響巨大(內欄 vs 外欄)。

性別    = one-hot(雄 / 雌 / 閹)
年齡    = 連續值, 標準化
負磅    = 連續值
閘位    = 連續值, 標準化 (1-14)
班次    = one-hot(G1, G2, G3, 1-4)
距離    = 連續值
場地    = one-hot(草地 / 沙地)

第二層:歷史特徵(最重要)

這一層是信號的主要來源。我們用滑動窗口計算多個統計量:

# 近 5 場勝率
runner.recent5_winrate = sum(won[-5:]) / 5

# 近 10 場名次均值(越低越好)
runner.recent10_avg_pos = mean(pos[-10:])

# 練馬師近 30 場勝率
runner.trainer_30d_winrate = trainer_stats[runner.trainer].winrate_30d

# 騎師-練馬師組合近 30 場勝率
runner.jt_combo_winrate = jt_stats[(jockey, trainer)].winrate_30d

# 同班次近 10 場勝率
runner.same_class_recent10 = class_stats[runner.class].winrate_10
為什麼要分班次? 因為一匹 G1 馬放到 4 班賽事,勝率會被錯估。同班次回測的樣本更乾淨。

冷啟動問題:新馬(首次出賽)沒有歷史怎麼辦?我們 fallback 到 練馬師勝率 × 騎師勝率的幾何平均,作為先驗。

第三層:即時特徵

這層是 live 預測的核心。我們每分鐘從 HKJC GraphQL 抓:

# 當前獨贏賠率(隱含機率倒數)
runner.implied_win = 1 / win_odds

# 賠率變動速率(近 5 分鐘)
runner.odds_momentum = (current_odds - odds_5min_ago) / odds_5min_ago

# 騎師今日戰績
runner.jockey_today_winrate = today_results[jockey].winrate

# 騎師近 3 日疲勞度
runner.jockey_fatigue_3d = sum(runs_3d[jockey])
賠率變動是最強即時信號之一。當大額資金湧入,賠率會內含公開資訊 (場地變化、騎師狀態、退賽可能性)。

第四層:衍生特徵(feature crosses)

這層做交叉,把低層特徵組合出新訊號:

# 騎師勝率 × 練馬師勝率(協同效應)
runner.jt_synergy = runner.jockey_winrate * runner.trainer_winrate

# 隱含機率 vs 模型機率差
runner.value_signal = runner.implied_win - runner.model_prob

# 配速合適度(負磅 / 距離)
runner.pace_fit = runner.weight / runner.distance

# 同主馬匹近期集體表現
runner.owner_cluster_winrate = owner_avg_winrate_30d

衍生層是黑魔法層——同樣兩個特徵,交叉和不交叉模型表現可能天差地別。

標籤與時間切割

標籤很簡單:這場跑第 1 名 = 1,其他 = 0。

時間切割很關鍵:

# 訓練集:2025-06 ~ 2026-05
train = df[mask_train]

# 驗證集:2026-06 ~ 2026-07
val = df[mask_val]

# 測試集:2026-08 ~ 2026-09
test = df[mask_test]
我們永遠不打亂時間順序。打亂會導致「未來洩漏」(data leakage)——模型會把未來結果當訓練特徵,表現虛高。

重要:類別不平衡

賽馬 14 匹馬,只有 1 個第一名。直接訓練 LGBM 會被「全部預測為不勝」這個捷徑帶偏。

解法

  1. 不放棄全部樣本——每場都用,每場 14 個樣本(每匹馬一個)。
  2. 焦點損失(focal loss)替代 log loss,對容易分錯的難樣本加權。
  3. Pairwise ranking — 直接優化 NDCG(排名指標),而不是分類正確率。

我們選第 3 種,因為最終用戶關心的是 top-3,不是 top-1。

我們的 LGBM 超參數

params = {
    "objective": "lambdarank",
    "metric": "ndcg",
    "ndcg_eval_at": [1, 3],
    "learning_rate": 0.02,
    "num_leaves": 31,
    "min_data_in_leaf": 50,
    "feature_fraction": 0.8,
    "bagging_fraction": 0.8,
    "bagging_freq": 5,
    "lambda_l2": 1.0,
    "verbose": -1,
}

num_leaves=31 配上 min_data_in_leaf=50 是保守選擇——賽事樣本量有限,過擬合風險高。

為什麼我們公開特徵工程?

特徵工程是純方法論——沒有任何「獨家內幕」。我們相信,公開特徵工程反而凸顯了我們做事的嚴謹。

更重要的是,香港法律框架下,我們只能以研究為定位——而研究的核心就是「方法論透明、數據可重現」。 把特徵寫出來,正是這種精神的體現。


延伸閱讀:Brier Score · 隱含機率 · Dixon-Coles 校正

感謝閱讀。如對研究方法有疑問,歡迎透過會員中心的「聯絡研究團隊」與我們交流。

← 回到研究隨筆列表