v9_suji(スジ予想)— Step 0: スジ表の本番構成を決める設計は docs/design/ana_prediction.md(§13 A案)。
本ディレクトリは、実装に入る前にスジ表の 学習窓 と 収縮強度 を決めるための検証記録。
全履歴・収縮なし(python scripts/build_suji_table.py の既定)を本番構成に採用する。
| 項目 | 決定 | 根拠 |
|---|---|---|
| 学習窓 | 全履歴(2025-11-01〜) | log-loss が窓長に対して単調改善し 4 ヶ月で飽和。valid / test で順序が完全一致 |
| 収縮 | なし(k=0) | k=50/200 は k=0 と小数 4 桁まで同一。k=1000 でも -0.0001 nat |
| 場の次元 | 持たない(全場プール) | 場別はどれだけ収縮してもプールに追いつかない。§場別スジ表 |
| 選定指標 | P(2着,3着 \| 真の1着) の log-loss |
回収率は分散が大きすぎて選定に使えない(下記) |
本番構成の test 実測(A案・上位 5 点):
| 指標 | 値 |
|---|---|
| 回収率 | 80.6%(95%CI [68, 94]) |
| 的中率 | 10.25% |
| 平均配当 | 3,931 円 |
| 点数 | 5.0 |
| 万舟的中 | 27 本 |
| 万舟 / 1 万円賭け | 0.153 |
→ 設計書 §13.2 に載せていた 82.0% は「train 窓のみ・収縮なし」の暫定値だったので、 この 80.6% に差し替える。
設計書 §13.2 の 82.0% は scratchpad の実装が出した値。本番コードパス
(scripts/build_suji_table.py + notebooks/ana_prediction/)で同じ構成
(train 窓 2026-05-01〜06-24・収縮なし)を再現できることを先に確認した。
| scratchpad(設計書 §13.2) | 本番コードパス | 差 | |
|---|---|---|---|
| 回収率 | 82.0% | 82.5% | +0.5pt |
| 的中率 | 10.12% | 10.16% | +0.04pt |
| 平均配当 | 4,052 円 | 4,056 円 | +4 円 |
| 点数 | 5.0 | 5.0 | — |
| 万舟 | 29 本 | 29 本 | — |
差分の原因は既知で、previews/stt が取れないレースの扱い。scratchpad は
そのレースを落としていたが、本番コードは直前バッチの既存規約に合わせて
枠なり(枠番 = コース)にフォールバックする。対象は 95 レース(0.7%)。
本番の挙動として正しいのはフォールバック側なので、この差は許容する。
窓長を変えたときの回収率:
| 窓 | valid 回収率 | test 回収率 |
|---|---|---|
| 1 ヶ月 | 77.9% | 78.3%(最下位) |
| 2 ヶ月 | 77.1% | 83.6% |
| 3 ヶ月 | 71.7% | 84.1% |
| 4 ヶ月 | 73.2% | 80.0% |
| 6 ヶ月 | 73.2% | 82.9% |
| 全履歴 | 71.3%(最下位) | 80.6% |
valid の最良(1 ヶ月)が test の最下位、valid の最下位(全履歴)が test では中位。 95%CI は ±12〜14pt あり、全構成が互いの CI に収まる。回収率で選ぶと valid のノイズに過学習する — 設計書 §13.3 で A案 vs B案 の判定に回収率を使えないと 結論したのと同じ理屈が、ここでも当てはまる。
真の 1着コースを与えたときの P(2着, 3着 | 1着) の log-loss。
1着の選び方(強さpt)から独立に、スジ表そのものの質だけを測る。
| 構成 | valid | test |
|---|---|---|
| 一様(20 通り) | 2.9957 | 2.9957 |
| 1 ヶ月 | 2.8257 | 2.8386 |
| 2 ヶ月 | 2.8204 | 2.8311 |
| 3 ヶ月 | 2.8177 | 2.8287 |
| 4 ヶ月 | 2.8146 | 2.8275 |
| 6 ヶ月 | 2.8144 | 2.8275 |
| 全履歴 | 2.8144 | 2.8274 |
| 全履歴 + k=1000 | 2.8144 | 2.8273 |
飽和しているので、運用上は窓を切る理由が無い。パラメータを 1 つ減らせること、
build_course_rate.py も全履歴から再生成していることから、全履歴を採る。
「イン天国と波乱場でスジは違うのでは」という仮説を検証した。
build_suji_table.py --by-stadium で 場別セルを 全場プール分布へ k で収縮した表
(course_win_rate.csv と同じ考え方)を作り、同じ条件付き log-loss で比較:
| 構成 | valid | test |
|---|---|---|
| 全場プール | 2.8144 | 2.8274 |
| 場別 k=0(収縮なし) | 2.9981 | 3.0096 |
| 場別 k=20 | 2.8385 | 2.8490 |
| 場別 k=50 | 2.8299 | 2.8410 |
| 場別 k=150 | 2.8209 | 2.8330 |
| 場別 k=400 | 2.8162 | 2.8288 |
| (参考) 一様分布 | 2.9957 | 2.9957 |
どの k でもプールに追いつかない。 k→∞ で場別はプールに収束するので、これは 「場の次元を足しても情報が増えず、ノイズだけ増える」ことを意味する。 収縮なしの場別は 一様分布より悪い(1 場 1,700 レース ÷ 6 コース ÷ 20 ペア ≒ 14 件/セルで過学習)。
理由は分解して見ると明快で、場差は「誰が 1着になるか」に出て、 「1着が決まった後の 2-3 着の並び」には出ない:
| 戸田(波乱) | 平和島(波乱) | 福岡 | 徳山(イン天国) | 大村(イン天国) | 全場 | |
|---|---|---|---|---|---|---|
| 1コースの1着率 | 42% | 46% | 59% | 62% | 62% | — |
| P(2着=1c | 1着=3c) | 29.8% | 30.9% | 33.2% | 38.4% | 45.0% | 39.5% |
| その n | 245 | 178 | 193 | 185 | 151 | 4,382 |
1着率は 20pt も違うのに、条件付きの 2-3 着のばらつきは n=150〜245 の標本誤差 (二項の 2SE ≒ ±7pt)にほぼ収まる。3 コースがまくったらインが残りやすい、という ターンの物理は場によらない。
そして A案では 1着コースを 強さpt で選んでおり、強さpt の waku 成分は
場×季節×コース勝率、重みも場別に学習されている。つまり場の情報は
すでにパイプラインの 1着選択側に入っていて、スジ表に重ねて持たせる必要がない。
コードは残す:
build_suji_table.py --by-stadiumと CSV の場コード列は 残してある(本番は全場プール = 場コード00のみ)。build_suji_picks.pyは 場別行があればそちらを優先して読むので、データが増えて結論が変わったときは フラグを立てるだけで切り替わる。
# 本番構成のテーブルを生成 (既定 = 全履歴・収縮なし)
python scripts/build_suji_table.py
# 構成を振って比較する (--from-date は以上、--to-date は未満)
python scripts/build_suji_table.py --from-date 2025-11-01 --to-date 2026-06-25 --k 0 \
--suji-out /tmp/all_suji.csv --kimarite-out /tmp/all_kim.csv
# バックテスト (name=path を複数渡せる)
python notebooks/ana_prediction/suji_backtest.py --tables "全履歴=/tmp/all_suji.csv"
| ファイル | 内容 |
|---|---|
dataset.py |
results/realtime × previews/stt × results/payouts × estimate/v1_basic を突き合わせ、進入コース順のパネルを作る |
suji_backtest.py |
A案の買い目のバックテストと条件付き log-loss |
kimarite_backtest.py |
B案の γ / β / w の再選定(下記) |
v10_kimarite のハイパラ再選定(2026-08-13)設計書 §4.3 の γ=1.0 / β=1.4 / w=0.8 は 28 クラス・実験窓(train 6,956 レース) で選んだ値。本番は 32 クラス・全履歴なので、同じ手続きで選び直した。 選定は valid (2026-06-25〜07-17, 3,696 レース) の 3連単 log-loss のみ。 test (2026-07-18〜, 3,614 レース) は報告にしか使っていない。
学習窓は train = 2025-11-01〜2026-06-24 の 34,222 レース(Stage1)/ 35,016 レース(Stage2)。強さpt は 2026-05-01 以降しか無いが、 Stage1 の学習は強さpt を要求しないので全履歴を使える(評価側だけ絞る)。
| 実験時(28 クラス) | 本番(32 クラス) | |
|---|---|---|
| k | 1000 | 150 |
| γ | 1.0 | 0.5 |
| β | 1.4 | 2.4 |
| w | 0.8 | 0.7 |
valid log-loss の感度(他の 3 つを固定して 1 軸ずつ動かす):
γ 0:3.9193 0.25:3.9097 0.5:3.9065 0.75:3.9095 1:3.9184 1.5:3.9519
β 0.8:3.9518 1:3.9380 1.2:3.9271 1.4:3.9188 1.6:3.9129
1.8:3.9091 2:3.9069 2.4:3.9065 2.8:3.9098
w 0:4.2394 0.3:3.9690 0.5:3.9205 0.6:3.9097 0.7:3.9065 0.8:3.9108 1:3.9477
いずれも内側に最小があり、周辺は平坦(γ 0.25〜0.75 / β 1.8〜2.8 / w 0.6〜0.8 で差 0.005 nat 以内)。動かすときは valid で選び直すこと。
120 通りの log-loss で k を選ぶと単調に大きい方が良くなる
(50:3.9071 → 2000:3.9037)が、40 倍動かして 0.0034 nat しか変わらない。
これはブレンド全体の改善幅 (0.12 nat) の 3% で、実質フラット。
一方 Stage2 側の条件付き log-loss(P(2着,3着 | 真のセル))は
Stage2 の質を直接測っており、そちらでは k=150 に明確な最小がある。
測っている対象が鋭い方の選定を採る。
PL 単体の最良は実験時と同じ β=1.4(valid 4.0258 / test 4.0382)。
ところがブレンド成分としては β=2.4 が最良で、単体では悪化する
(β=2.4 単体は test 4.2758)。混合の相手として使うときは PL を鋭くした方が効く。
そのため kimarite_blend.py では BETA(2.4、ブレンド用)と
PL_BASELINE_BETA(1.4、A/B の対照用)を別の定数にしてある。
| 予測 | valid | test |
|---|---|---|
| 一様 (1/120) | 4.7875 | 4.7875 |
| Plackett-Luce (β=1.4) | 4.0258 | 4.0382 |
| 決まり手モデル単体 | 3.9477 | 3.9644 |
| ブレンド (w=0.7) | 3.9065 | 3.9196 |
改善 (PL 単体 → ブレンド) = +0.1186 nat [+0.1003, +0.1376](n=3,614)。
| 戦略 | 点数 | 的中率 | 回収率 | 95%CI | 平均配当 | 万舟/1万円 |
|---|---|---|---|---|---|---|
| B案 1着≠1 top3 | 3.0 | 8.85% | 83.8% | [72, 96] | 2,839 | 0.055 |
| B案 1着≠1 top5 | 5.0 | 12.65% | 87.1% | [76, 98] | 3,442 | 0.111 |
| B案 1着≠1 top8 | 8.0 | 16.85% | 80.9% | [73, 90] | 3,842 | 0.135 |
| B案 1着≠1 top12 | 12.0 | 20.61% | 78.0% | [71, 86] | 4,542 | 0.171 |
| (参考) 制限なし top5 | 5.0 | 30.35% | 77.3% | [72, 83] | 1,273 | 0.006 |
| A案 v9_suji (同一レース) | 5.0 | 10.27% | 80.6% | [69, 93] | 3,927 | 0.149 |
点数を valid で選び直しても top5 は「勝たない」。 valid では top3 が 80.5%、top5 が 76.3% で、順位が test と逆になる(どちらも CI ±10pt 級)。 回収率では点数すら決められないので、top5 は A案と同じ 5 点・同じ 500 円でペア比較できることを理由に固定した。 これは事前に決めたルールであって、valid の最良ではない。
本番の合成は stdlib のみの scripts/boatrace/kimarite_blend.py、この検証は
numpy 版。同じ入力(2026-08-12 の 180 レース)で 120 通りすべてを突き合わせ、
最大差 3.3e-16(倍精度の丸め)を確認済み。ここで報告した数値は
本番が実際に計算する分布のものと言える。
python notebooks/ana_prediction/kimarite_backtest.py --k-grid 150