📝 この記事は Zenn で先に公開した記事の再掲です。最新版とコメントは Zenn をご参照ください。
TL;DR
- 個人開発の競馬予想ML(UmaScore)で、地方競馬(NAR)全6場・16ヶ月・7,960レースを対象に手製 5 因子ルール(M1)と LightGBM(M2)を比較検証した。
- 評価は walk-forward OOS の月次ローリング(学習窓を毎月スライドし、直後の1ヶ月を予測 → 集約10ヶ月)で time-leakage を排除。
- 結果:M1 EV≥1.5 は 回収率 100.6%(対市場ベースライン +19.4pt・ただし 105% ゲート未達)、M2 本命は 84.3%(市場 82.0% と近い)、M2 EV≥1.5 は 69.0% で市場ベースライン以下に沈んだ。
- 「地方=小規模市場=非効率=妙味」仮説はローカル場(園田・高知)でも 102.4%(M1)/47.0%(M2 EV≥1.5)で 支持されず。
- 前作 第3本(中央 JRA の ablation 検証) と同じく EMH の競馬市場版が 別データセット・別モデル族で再現された記録。ネガティブ結果だが検証手順は他ドメインに転用可能。
1. はじめに:連載第4本の位置づけ
本記事は個人開発 ML 連載の第4本です。第1〜3本の流れ:
- 第1本:データリーケージ正攻法(時系列の未来参照を塞ぐ)
- 第2本:BT→FT 劣化の4構造(survivorship / look-ahead / regime shift / market efficiency)
- 第3本:効率的市場を自作MLで検証(中央競馬・rule-based・ablation で成分別因果検証)
第3本は中央競馬(JRA)で「自前モデルが市場を出し抜けているか」を ablation で検証しました。本記事は 同じ問いを、地方競馬(NAR)というより小さな市場に持ち込み、モデルも rule-based と LightGBM の2種で比べるという設計です。物語版として note 記事「地方も、賢かった」(結論のみ・一般読者向け)を先に公開しており、本記事はその 裏の技術中身(評価設計・モデル対比・過学習の実測署名)を実装者視点で書きます。
問いは2つ:
- 地方=小規模市場なら非効率で妙味が残っているのでは?
- 手製ルールより LightGBM のほうが強いのでは?
先に答えを言うと、どちらも支持されませんでした。以下、それをどう調べてどう反証されたかを書きます。
2. 対象データと評価設計
2.1 データ
- NAR 全6場(南関=大井・船橋・川崎・浦和/ローカル=園田・高知)
- 期間:2025-01-01 〜 2026-04-30(16ヶ月)
- 総レース数 7,960、総出走 86,246
- ≥5走カバレッジ 50.7%(過去5走以上の履歴がある馬の割合。B-3 ゲート閾値 50% を辛うじて超えた水準)
バックフィルは別工程で完遂しており、本記事は評価パイプラインに絞ります。データ品質チェックの規律は第1本を参照してください。
2.2 walk-forward OOS の月次ローリング設計
なぜ単純な train/test split では駄目か。時系列データで無作為分割すると、テスト月より 後の情報が学習に混じり得ます。これは第1本で扱った未来参照リーケージの一種です。
そこで採用したのが月次ローリング型の walk-forward:
学習窓(〜t月末) → 予測窓(t+1月)
[2025-01 〜 2025-06] → 2025-07
[2025-01 〜 2025-07] → 2025-08
[2025-01 〜 2025-08] → 2025-09
…
[2025-01 〜 2026-03] → 2026-04
- テスト月は 2025-07 〜 2026-04 の 10ヶ月(2025-06 までは学習ウォームアップ)
- 各テスト月は その月より前の全データで学習 → 当月を予測
- OOS 予測は 54,526 出走に及ぶ
コアループは以下の通りです(nar_b3_m2.py から抜粋・read-only 実装)。
TEST_MONTHS = ["2025-07", "2025-08", "2025-09", "2025-10", "2025-11", "2025-12",
"2026-01", "2026-02", "2026-03", "2026-04"]
def walk_forward(df: pd.DataFrame) -> pd.DataFrame:
df["ym"] = df["race_date"].dt.strftime("%Y-%m")
oos = []
for tm in TEST_MONTHS:
train = df[df["ym"] < tm]
test = df[df["ym"] == tm].copy()
if len(test) == 0 or len(train) < 1000:
continue
Xtr, ytr = train[FEATURES], train["target_win"]
ds = lgb.Dataset(Xtr, ytr, categorical_feature=CATEGORICAL)
model = lgb.train(PARAMS, ds, num_boost_round=300)
test["pred"] = model.predict(test[FEATURES])
# レース内で正規化 → 勝率 → EV
test["pred_sum"] = test.groupby("race_id")["pred"].transform("sum")
test["win_prob"] = test["pred"] / test["pred_sum"].replace(0, np.nan)
test["ev"] = test["win_prob"] * test["odds_win"]
oos.append(test)
return pd.concat(oos, ignore_index=True)
比較基準は市場ベースライン M0=人気1番を機械的に買う戦略。同一 OOS 窓(10ヶ月)で 82.0%(的中 45.6%・n=5,011)でした。控除率 25% の単勝市場で「本命買い続け」が 82% ということは、公開情報の多くは既に価格に織り込まれているという最初のサインです。
なお本データセットの全張り回収率は 70.3%(§3.2)で、控除率 25% から素朴に期待される 75% をやや下回ります。これは実オッズが刻み単位に丸められることや、結果・オッズが欠損したレースを集計から外していることによるもので、控除率そのものの推定値ではありません。以降の比較は理論値ではなく この実測 70.3% を「何も考えずに買った場合」の底として読んでください。
3. モデル1:手製 5 因子ルール(M1)
3.1 設計
第3本で登場した prediction_engine を NAR 向けに版固定(commit 75f50bb8)して呼び出します。5 因子(base / bias / training / aptitude / human_factor)+ odds_implied を合算して master_score を作り、レース内で softmax(温度 5.0)を掛けて勝率化、EV = 勝率 × オッズ で EV ≥ 1.5 を選別する UmaScore の実戦戦略です(nar_b3_m1.py / nar_b3_run_m1.py)。
M1 は学習を含まないルールベースで、過去走特徴は race_date < target で参照するため 16ヶ月全期間がそのまま OOS 読みになります。walk-forward 月次ローリングを掛けるまでもなく leak-safe です。
3.2 結果
| 分割 | 回収率 | 的中率 | n |
|---|---|---|---|
| M0 全張り単勝 | 70.3% | 9.4% | 84,448 |
| M0 人気1番 | 81.2% | 44.6% | 7,862 |
| M1 EV≥1.5 全 | 100.6% | 14.0% | 5,424 |
| M1 EV≥1.5 コア(オッズ≤20倍) | 101.7% | 23.5% | 3,151 |
| M1 EV≥1.5 尾(オッズ>50倍) | 101.2% | 0.8% | 2,161 |
| M1 EV≥1.5 南関 | 99.5% | 13.4% | 3,230 |
| M1 EV≥1.5 ローカル | 102.4% | 15.0% | 2,194 |
| 月別 CV | 0.421 | — | — |
出典:reports/nar_b3_m1_recheck_20260625.log。M0 の窓は M1 と同じ全16ヶ月(M2 は 10ヶ月 OOS 窓=M0 82.0% と別値になる点は §4 参照)。
対市場差分は +19.4pt。ただし収益ラインとして設定した 105% ゲート(控除率を吸収して実運用に踏み切れる最低水準)には届きません。ここまでは「地方でも一応 edge らしきものは見える/だが投票に踏み切れる水準ではない」というグレーな結論です。
そして注目すべきは ローカル 102.4% vs 南関 99.5% の差の小ささです。仮説「小さい市場(ローカル)ほど非効率で妙味」が正しいなら、ローカルが南関を大きく上回るはずでした。しかし実測はほぼ横一線。地方の中でさらに市場サイズを絞っても、非効率仮説は支持されないというのが1つ目の反証です。
月別 CV 0.421(回収率の月次標準偏差 / 平均)は「勝つ月と負ける月のバラつきが大きい」ことを示し、安定した edge とは呼びにくい水準です。
4. モデル2:LightGBM walk-forward OOS(M2)
4.1 設計
M1 と同じデータに、時系列で leak-safe な特徴量を足して LightGBM を回します。
- 数値特徴:距離 / 枠 / 馬番 / 馬体重 / 人気 / 単勝オッズ / 出走数 / 過去勝率 / 平均着順 / 休養日数
- カテゴリ特徴:場 / ダート区分 / 馬場 / 性
- 目的変数:
target_win = (finish_position == 1)
過去走系(h_past_races / h_win_rate / h_avg_rank)は cumcount() と expanding().mean() で 当該レース以前のみから計算しており、time-leakage は入りません。
PARAMS = {
"objective": "binary", "metric": "auc", "learning_rate": 0.05,
"num_leaves": 31, "min_child_samples": 50, "feature_fraction": 0.8,
"bagging_fraction": 0.8, "bagging_freq": 1, "verbosity": -1, "seed": 42,
}
ハイパーパラメータは既定寄りで、チューニングは行っていません(この点は §5 で改めて触れます)。EV の作り方は M1 と同じで EV = レース内正規化 pred × 単勝オッズ、選別も EV ≥ 1.5 で揃えています。
4.2 結果
| 指標 | 回収率 | 的中率 | n |
|---|---|---|---|
| M0 人気1番(10ヶ月 OOS 窓) | 82.0% | 45.6% | 5,011 |
| M2 本命 / 全 | 84.3% | 45.1% | 5,062 |
| M2 本命 / 南関 | 85.2% | 44.3% | 2,787 |
| M2 本命 / ローカル | 83.1% | 46.0% | 2,275 |
| M2 EV≥1.5 / 全 | 69.0% | 2.8% | 2,136 |
| M2 EV≥1.5 / コア(≤20倍) | 73.4% | 7.3% | 506 |
| M2 EV≥1.5 / 南関 | 88.8% | 3.2% | 1,123 |
| M2 EV≥1.5 / ローカル | 47.0% | 2.4% | 1,013 |
| EV≥1.5 月別 CV | 0.584 | — | — |
出典:reports/nar_b3_m2_recheck_20260625.log。同一 OOS 窓の M0 人気1番が 82.0% なのに対し、M2 本命は 84.3%(+2.2pt) =市場に肉薄しつつわずかに上回るだけ。ところが モデルが「価値あり」と判定した EV≥1.5 集合は 69.0% で市場ベースライン以下に沈みました。ローカルだけを取ると 47.0% =全張りベースライン 70.3% すら大きく下回る水準です。
なお南関の EV≥1.5 は 88.8% で市場並みの数字が出ています。全体平均 69.0% はローカル 47.0% との合算で押し下げられた結果であり、南関/ローカルの非対称は素朴な「モデル一律に強い/弱い」という読みを難しくします。ただし本記事の中心は南関だけで見た局所勝ちよりも 全体で市場以下に沈むこと自体にあるので、独立の考察は §4.3 の枠内に留めます。
4.3 考察:過学習の署名として読めるか、代替仮説とどう並置するか
「本命の予測は市場に近い(84.3% ≒ 82.0%)/モデルが独自に value と判定した集合は市場以下に沈む(69.0% < 82.0%)」という 非対称は、過学習の一つの読み筋と整合的です。
- 高確率帯(本命)は市場と重なる情報構造 → 学習が容易で汎化しやすい
- value 帯(オッズより高い勝率をモデルが独自主張する領域)は 学習データ固有ノイズを拾いやすく、OOS で崩れやすい
これは第3本 §3.3 で見た「BT で強かった ABLATED & odds≤20 = 116.7% が FT では 49.3% に崩れる」現象と同型に読めます。第2本の "market efficiency" と "overfitting" が同時発火しているとも言えます。
ただし断定は避けます。並置すべき代替仮説がいくつかあるためです:
- OOS 窓 10ヶ月は短い。EV≥1.5 集合の n=2,136 は的中率 2.8% の低頻度戦略としては薄い標本で、単に運の悪い10ヶ月だった可能性を排除できません。月別 CV 0.584 という高いバラつきはこの見方を支持します。
- ハイパーパラメータ最適化・特徴量エンジニアリングを行っていない現状の LightGBM 1 構成の結果です。この 1 事例から「GBDT 全般が rule-based に負ける」と一般化することはできません。より丁寧なチューニングや特徴量設計でこの結果が変わる可能性は残ります。
- NAR 特有のデータ疎性(≥5走カバレッジ 50.7% は決して厚くない)が LightGBM の value 側判定を不安定にしている可能性。
言えるのは「この構成では過学習の署名と整合的なパターンが観測された」までで、「過学習だと確定した」ではありません。誠実にはこう書くしかない、というのが本節の姿勢です。
5. 統合考察:仮説は 2 つとも支持されなかった
冒頭の 2 仮説について現時点で言えること:
- 仮説A「地方=非効率=妙味」 → M1 ローカル 102.4% は南関 99.5% とほぼ横並び、M2 EV≥1.5 ローカルは 47.0% で市場を大きく下回る。支持されず。
- 仮説B「LightGBM で複雑にすれば手製ルールに勝てる」 → M1 EV≥1.5 全 100.6% と M2 EV≥1.5 全 69.0% の比較で 支持されず。ただし §4.3 の通り LightGBM 1 構成・ハイパラ最適化なし・特徴量エンジニアリング最小の結果ゆえ、GBDT 全般への一般化は行いません。
第3本と併せると、効率的市場仮説の競馬市場版は 中央 JRA・地方 NAR の 2 データセット で、そして rule-based・GBDT の 2 モデル族 で、いずれも自作モデルによる出し抜きが実現しなかったという記録になります。ネガティブ結果ですが、検証手順そのものは他ドメイン(株・FX・スポーツベッティング・予測市場)に転用可能な雛形です。
6. 一般化:この検証手順は他ドメインで書き換え可能
再利用できる 3 点セット:
- walk-forward 月次ローリング:train/test 単純分割の代替。時系列に順序があるドメイン全般で有効。
- 手製ルールをベースラインに置く:GBDT の「良く見える」性能を、シンプルな解釈可能モデルとの比較で相対化する。手製が同等以下なら GBDT の複雑度は正当化されない。
- 本命 vs value 集合の性能非対称チェック:モデルが「市場より高い勝率」と判定した領域が OOS で市場以下に沈むなら、それは過学習の一つの読み筋として警戒シグナルになる。
7. 補足:個人投機目的では運用していません
技術検証の文脈の記事ですが、公営競技を扱っているため一言補足します。
UmaScoreは運営者個人のフォワードテスト記録の公開であり、購入推奨ではなく、利益保証もありません。FT結果は実投票ではなくスナップショット記録です。なお公営競技は20歳以上の自己責任であり、馬券購入の代理や投資助言は行いません。過去のデータは将来を保証しません。
NAR の本検証も私的な探索・検証であり、地方馬券の購入推奨・予想提供ではありません。
8. まとめ
- walk-forward OOS + 手製ルール vs LightGBM の対比で、地方競馬 NAR の効率的市場性を実測確認した
- M2(LightGBM)は本命予測では市場に肉薄しつつ、EV≥1.5 集合で市場以下に沈んだ(69.0% < 82.0%)=過学習の署名と整合的、ただし OOS 窓の短さ・ハイパラ未最適化を踏まえ断定はしない
- 「地方=妙味」も「複雑モデル=強い」もいずれも支持されず、連載第3本と併せて EMH の複数データセット・複数モデル族での再現になった
- 判定は 8月末〜9月の統合レビューまで留保。ネガティブ結果を含めて開示し続けるのが本連載の一貫方針
9. 参考
- 連載第1本:データリーケージ正攻法
- 連載第2本:BT→FT 劣化の4構造
- 連載第3本:効率的市場を自作MLで検証
- 物語版:地方も、賢かった(note)
- Fama, E. F. (1970). Efficient Capital Markets: A Review of Theory and Empirical Work.
- López de Prado, M. (2018). Advances in Financial Machine Learning.
- UmaScore