為什麼特徵工程比模型更關鍵?
在機器學習競賽圈有一句名言:「特徵決定上限,模型只是逼近這個上限。」對於賽馬 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 會被「全部預測為不勝」這個捷徑帶偏。
解法:
- 不放棄全部樣本——每場都用,每場 14 個樣本(每匹馬一個)。
- 焦點損失(focal loss)替代 log loss,對容易分錯的難樣本加權。
- 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 校正