BoatraceCSV

穴予想(決まり手ベース)設計 — v10_kimarite

本命予想(v1_basic 系)とは別の生成原理で「的中率は低いが配当が大きい買い目」を 出す予想者の設計。決まり手を多クラス予測し、決まり手条件付きの着順分布から出目を作る 方式(検討時の案C)を採用する。

現在の状態: 設計レビューで挙がった Blocking 3 件は §11、 High / Medium 7 件は §12 で解消済み。 最終構成は **A案 v9_suji(スジ予想)を control、B案 v10_kimarite(穴予想)を 実験スロットとする 2 スロット運用(§13)。 判定は回収率ではなく log-loss で行う(回収率では 8.2 ヶ月かかる / §13.3)。決まり手は買い目 1 点ごとの注釈 として両案で表示する(§14)。**

【2026-08-22】A案 v9_suji は退役。2 スロット運用は 10 日で終了し、 B案 v10_kimarite の 1 スロットに戻した。理由は成績の劣化ではなくスロットの 重複 — 本番 1,511 レース(両案同一レース)で買い目の重なりは平均 2.70 点 / 5 点 ある一方、回収率は 70.7%(A)vs 68.8%(B)で §13.3 のとおり区別できない。 A案は確率モデルを持たず主判定の log-loss に載らないため、B案を残した。 体験指標では A案が上(平均配当 3,949 円 vs 3,035 円、万舟/1万円 0.133 vs 0.066) で、「穴らしさ」を優先するなら判断は逆になりうる。検定値は scripts/boatrace/predictors/registry.py 冒頭コメント。 なお B案は A案の資産(build_suji_table.py の決まり手注釈テーブルと build_suji_picks.py のヘルパー)に依存しているので、A案のコードは残置。

【2026-08-13】両案とも実装・投入済み(A案 2026-08-12 / B案 2026-08-13)。 本番のファイル構成・スキーマ・定数は docs/data/estimate.md を正とすること。 本書は設計と検証の記録で、§8 の実装計画は計画時のまま残してある (実際のパスとの差分は §8.1 に明記)。 ハイパラは本番構成で選び直しており、§4 以前に出てくる γ / β / w は古い値 (§4.3 の更新注記を参照)。


1. 結論(先に要点)

1.1 妥当だと確認できたこと

  1. 決まり手は穴そのもの。逃げの平均配当が 2,565 円・万舟率 3.6% に対し、 まくり 12,374 円・33.4%、まくり差し 14,728 円・36.7%。 「逃げ以外を当てる」= 穴を当てるとほぼ同義で、予測ターゲットと穴の定義が一致する。
  2. 決まり手 × 1着コース は 25 セルに収まり、セル条件付きの 2-3 着分布は強く偏る。 例: まくり差しの 1着3コース なら 2着は 1コースが 63.4%、上位 5 パターンで 68.9% を カバーする。日本語で説明できる構造がそのまま確率テーブルになる。
  3. 確率予測は既存モデルより良い。3連単 120 通りの log-loss(ホールドアウト 3,527 レース)は、強さpt の Plackett-Luce 4.0325 に対し、 決まり手モデル単体 3.9522 → 強さpt とのブレンド 3.9364。
  4. P(逃げ) はよく校正されている。予測 0〜100% を 8 帯に分けて実測と比べると ずれは概ね ±3pt、最大 -6.6pt(§5.5)。 この確率値は単体で商品になる。
  5. Stage1 は多項ロジスティック回帰が最良。GBM より log-loss が良く (1.831 vs 1.946)、係数を CSV に出せるので既存の運用パターンに載る。
  6. オッズは使わなくてよい。オッズ帯フィルタを重ねると回収率はむしろ下がった (85.5% → 76.3%)。

1.2 レビューで判明した欠陥と、その解決状況

# 内容 状態
B1 展開予想の 94.5% が「イン逃げ」。top-1 セル的中率 52.3% は「常に逃げと言う」ベースライン 52.4% を下回る。買い目は「1着≠1」のハード制約で作るため、予想者が自分の主予測と矛盾した買い目を出す 解決(§11.1)。ただし当初案の abstain は実測で有害と判明し、別解に差し替え
B2 control との比較がペアになっていない。95%CI [74, 97] は control を含み、差がないことすら示せていない 解決(§11.2)。同一レース 3,527 でのペア比較で -1.3pt / 95%CI [-13.1, +11.0] / p=0.817
B3 買い目ルール(1着≠1 / top5)を test の一覧表を見て選んでいる 解決(§11.3)。valid だけで選び直しても同じルールが選ばれ、test も 84.9% で不変

結論として、3 件とも解消できた。 ただし解決の過程で新しい事実が 2 つ出た:

回収率 100% 超えは目標にしないこと。 市場(オッズ)の log-loss は 3.7965 で、 ブレンド後の 3.9364 でもまだ 0.14 nat 負けている。控除率 25% を破るには log(1/0.75) = 0.288 nat 分の優位が要る。


2. 穴の構造と、決まり手を軸にする理由

2.1 穴は構造的に不利(favourite-longshot bias)

「毎レース人気 k 位の 3連単を 1 点買う」戦略の回収率(42,090 レース):

人気帯 点数 的中率 回収率
1〜3 位 3 26.4% 76.8%
4〜10 位 7 28.6% 77.9%
11〜20 位 10 17.7% 73.7%
21〜40 位 20 15.1% 69.2%
41〜80 位 40 10.0% 60.8%
81〜120 位 40 2.3% 43.6%

控除率 25% は全帯共通なので、この落ち方は投票者が人気薄を買いすぎているぶんの損。 穴に寄ること自体が回収率にマイナスで、大穴(100 倍超)は選別しても勝てない (§5.4)。狙うのは中穴(2,000〜30,000 円)に絞る。

2.2 それでも穴を狙う理由 — 配当の 2/3 はそこにある

3連単 配当 レース割合 払戻総額に占める割合
〜1,000 円 19.2% 1.9%
1,000〜3,000 円 36.9% 9.3%
3,000〜10,000 円 27.5% 21.1%
10,000〜30,000 円 11.7% 27.3%
30,000〜100,000 円 4.0% 27.0%
100,000 円〜 0.6% 13.3%

1 万円以上はレースの 16.3% にすぎないが、払戻総額の 67.6% を占める。

2.3 現行モデルは「荒れるかどうか」を持っていない

v1_basic の 1枠_強さpt − 他5艇の最大強さpt で 5 分位に分けた実測(14,333 レース):

分位 1号艇1着率 万舟率 1着が4〜6号艇
1(拮抗) 35.0% 17.9% 24.7%
3 56.5% 17.3% 17.9%
5(1号艇圧倒) 73.2% 14.3% 13.1%

1 号艇の勝敗はよく分ける(35% ⇔ 73%)のに、万舟率は 17.9% ⇔ 14.3% しか動かない。 「誰が強いか」と「どう決まるか」は別軸で、現行 5 成分は後者を持っていない。 この穴を埋めることが、買い目を出すより先に価値がある(§8)。

2.4 決まり手は穴そのもの(42,010 レース)

決まり手 割合 平均配当 中央値 万舟率 平均人気
逃げ 52.5% 2,565 1,430 3.6% 8.1
差し 11.9% 10,569 4,790 26.9% 25.0
まくり 15.6% 12,374 6,170 33.4% 28.9
まくり差し 11.8% 14,728 6,500 36.7% 31.2
抜き 6.2% 10,689 3,730 24.3% 22.9
恵まれ 0.7% 10,657 2,470 20.4% 18.7

逃げか否かで配当が 4〜6 倍違う。


3. 決まり手のデータ構造

⚠ 定義のずれ(実装時に作り直す): この節の集計は results/realtime の 実進入コース({c}コース_艇番)で作っている。一方 §4 のモデルは 賭け時点で使える previews/stt の展示進入コースで並べ替える。 1着コースの一致率は 96.8%(全 6 艇の進入まで見ると 87.9%)なので大枠は変わらないが、 この表の数値はモデル内部のテーブルとは厳密には一致しない。 §12.2 の結論どおり stt 基準に統一するので、実装時に作り直すこと。

3.1 決まり手 × 1着コース(全体に対する %)

決まり手 1c 2c 3c 4c 5c 6c 計
逃げ 53.32 — — — — — 53.32
差し — 8.07 1.46 1.88 0.38 0.20 11.99
まくり — 3.78 5.09 4.98 1.30 0.61 15.77
まくり差し — — 4.78 2.64 3.69 0.82 11.93
抜き 2.29 1.15 1.26 0.71 0.60 0.24 6.23
恵まれ 0.11 0.16 0.15 0.18 0.12 0.04 0.77

「逃げ = 1コース」「まくり差しは 2コースからは出ない」といった制約が自然に入る。 n < 60 のセルは その他_{コース} に畳んで 25 クラスにする。

3.2 セル条件付きの 2着・3着 コース分布

セル n 2着コース分布 上位2-3着ペア
逃げ 1着1c 22,398 2c:34.3 3c:29.2 4c:18.9 5c:12.2 6c:5.4 2-3(13%) 3-2(10%) 2-4(10%)
差し 1着2c 3,389 1c:59.8 3c:16.3 4c:12.0 5c:7.9 6c:4.0 1-3(22%) 1-4(17%) 1-5(13%)
まくり 1着3c 2,140 1c:22.6 2c:20.5 4c:27.1 5c:20.9 6c:8.9 4-5(8%) 4-1(7%) 4-2(7%)
まくり 1着4c 2,094 1c:22.4 2c:19.4 3c:10.6 5c:33.0 6c:14.6 5-1(11%) 5-6(8%) 5-2(8%)
まくり差し 1着3c 2,006 1c:63.4 2c:16.1 4c:9.9 5c:7.4 6c:3.2 1-4(21%) 1-5(18%) 1-2(15%)
まくり 1着2c 1,588 1c:10.8 3c:36.6 4c:27.0 5c:17.7 6c:7.9 3-4(12%) 3-5(11%) 4-3(9%)
まくり差し 1着5c 1,550 1c:41.4 2c:14.6 3c:14.3 4c:22.6 6c:7.0 1-2(12%) 1-4(10%) 1-3(10%)
まくり差し 1着4c 1,110 1c:38.1 2c:15.3 3c:24.7 5c:14.8 6c:7.1 1-5(14%) 1-2(10%) 3-5(9%)

セルが当たっている前提での 2-3 着カバー率(条件付き的中率):

セル n top3 top5 top8 top12
逃げ 1着1c 22,398 33.4% 49.0% 67.2% 83.5%
差し 1着2c 3,389 52.3% 68.0% 81.3% 90.1%
まくり差し 1着3c 2,006 54.5% 68.9% 81.9% 91.5%
まくり 1着4c 2,094 27.9% 42.2% 59.0% 76.6%
まくり 1着3c 2,140 21.7% 34.6% 52.9% 75.0%

差し・まくり差しは 2-3 着が絞れる(top5 で 68%)が、まくりは散る(top5 で 35〜42%)。


4. モデル設計

       ┌ Stage1: P(決まり手, 1着コース)  … 25 クラス多項ロジスティック回帰
       │           ↓
出目 ← ┤ Stage2: P(2着コース, 3着コース | セル)  … 収縮済み経験表 × 強さpt 変調
       │           ↓
       └ 合成: P(120通り) = Σ_セル P(セル) × P(2着,3着|セル) → 艇番に写像
                            ↓
            ブレンド: w × 決まり手モデル + (1-w) × Plackett-Luce(強さpt)

4.1 Stage1: セル多クラス予測

4.2 Stage2: セル条件付き 2-3 着分布

セルごとの 6×6 経験カウント表 pair[cell][c2][c3] を、周辺分布の積へ収縮:

tab[cell] = (pair[cell] + k · m2[cell] ⊗ m3[cell]) / 正規化      … k = 1000 (valid で選択)

これに 強さpt による変調を掛けてレースごとに個別化する:

P(c2, c3 | cell) ∝ tab[cell][c2][c3] · exp(γ·z_{c2}) · exp(γ·z_{c3}/2)   … γ = 1.0 (valid で選択)
z_c = (コース c に入る艇の 強さpt − 50) / 10

1着コースと重複する枠、および c2 == c3 は 0 にして正規化する。 γ が「同じ決まり手なら強い艇が上に来やすい」を表し、tab が「決まり手の型」を表す。

4.3 合成とブレンド

P(c1,c2,c3) = Σ_cell:1着=c1  P1[cell] · P(c2,c3 | cell)
最終 = w · P(決まり手モデル) + (1−w) · P(Plackett-Luce, β)

【2026-08-13 更新】ハイパラは本番構成で選び直した。 本節に元々あった k=1000 / γ=1.0 / β=1.4 / w=0.8 は 28 クラス・実験窓 (train 6,956 レース)の値で、32 クラス・全履歴の本番構成では選ばれない。 §12.1 で見つけた 28→32 クラスの食い違いと同じ構造の問題なので、 同じ手続き(valid の 3連単 log-loss)で取り直した。

  実験時(28 クラス) 本番(32 クラス・全履歴)
k(Stage2 の収縮) 1000 150
γ(強さpt 変調) 1.0 0.5
β(PL の強さ倍率) 1.4 2.4
w(決まり手モデルの重み) 0.8 0.7

valid (2026-06-25〜07-17, 3,696 レース) の感度は平坦で、γ は 0.25〜0.75 / β は 1.8〜2.8 / w は 0.6〜0.8 の範囲なら差 0.005 nat 以内。 k は Stage2 側の条件付き log-loss で選んだ 150 を据え置く (120 通りの log-loss は k に対してほぼ平坦 —— 50〜2000 で 0.0034 nat しか 動かない —— のに対し、条件付き log-loss は Stage2 の質そのものを直接測る)。

β には注意: PL 単体の最良は実験時と同じ 1.4 (valid 4.0258) だが、 ブレンド成分としての最良は 2.4。混合の相手として使うときは PL を 鋭くしたほうが効く。kimarite_blend.py では BETA(2.4)と PL_BASELINE_BETA(1.4、A/B の対照用)を別定数にしてある。

実装は scripts/boatrace/kimarite_blend.py、再選定は notebooks/ana_prediction/kimarite_backtest.py。 選定記録は notebooks/ana_prediction/report.md。

両者は補完的で、混ぜると単独より良くなる(§5.1)。 最後にコース → 艇番へ写像して出目にする。

強さpt への依存: Stage2 の γ 変調とこのブレンドで 強さpt を使うので、 v10_kimarite は index を必要とする(§12.3)。

【実装時に解消】 当初は「data/estimate/v1_basic を直接読む = 予想者間の 依存」を想定していたが、実装では v10_kimarite が自分の index (data/estimate/v10_kimarite/)を持つ形にした。成分・重みが control と同一 なので値は v1_basic と一致するが、参照は自分のファイルに閉じているため 「予想者どうしは独立」という既存の前提を崩していない。A案 v9_suji も同じ形。

残る依存は運用上の 1 点だけ: 初月の weights を v1_basic からコピーして ブートストラップする(翌月以降は monthly-weights が自前で生成する)。


5. 検証結果

学習 2026-05-01〜06-24(6,956)/ 検証 06-25〜07-17(3,689、ハイパラ選択のみに使用)/ テスト 2026-07-18〜08-11(3,527、報告用)。

5.1 確率予測の精度

予測 test log-loss(3連単 120 通り)
一様(1/120) 4.7875
Plackett-Luce(強さpt, β=1.4) 4.0325
決まり手モデル単体 3.9522
ブレンド(決まり手 0.8 + PL 0.2) 3.9364
(参考)市場 = od3 の正規化逆オッズ ※別サンプル 3,352 レース 3.7965

PL 比 0.096 nat の改善。市場との差は 0.236 → 0.140 nat に縮まったが、まだ負けている。

5.2 買い目の実測(テスト期間 3,527 レース)

この節の数字は当初 B2 / B3 の疑いがあったが、§11 で検証し直して据え置きとなった。 買い目ルールは valid だけで選び直しても 同じ 1着≠1 / top5 が選ばれ(§11.3)、 control とは同一レースでペア比較して -1.3pt / p=0.817(有意差なし)を確認した (§11.2)。 以下の control 列は別期間の参考値なので、判定には §11.2 の表を使うこと。

「最終確率の上位 k 点を 1 点 100 円で買う」戦略:

戦略 点数 的中率 回収率 95%CI 平均配当 万舟的中
確率top3(制限なし) 3.0 20.98% 79.3% [73, 86] 1,134 0
確率top12(制限なし) 12.0 51.40% 78.9% [74, 84] 1,842 19
穴: 1着コース≠1 の top5 5.0 12.39% 84.9% [74, 97] 3,427 21
穴: 1着コース≠1 の top3 3.0 8.70% 82.5% [71, 95] 2,842 7
穴: 1着コース≠1 の top12 12.0 20.78% 81.6% [74, 90] 4,709 78
逃げ確率 下位35% のレース × 1着≠1 top8 8.0 23.32% 93.4% [80, 108] 3,206 10

参考値としての control(v1_basic ≒ 86%・11.7 点・的中率 46%・平均配当 ≒ 2,200 円) との対比。同一レースでの比較ではないので、方向感の確認までにとどめること:

  control v1_basic(別期間) v10_kimarite(1着≠1 top5)
回収率 ≒ 86% 84.9%(95%CI [74, 97])
的中率 ≒ 46% 12.4%
点数/レース 11.7 5.0
購入額/レース 1,170 円 500 円
平均配当 ≒ 2,200 円 3,427 円

95%CI の幅が 23pt ある。この期間の長さでは control との差はどちら向きにも示せない。

5.3 モデル差による振れ(重要)

Stage1 を GBM にした版でも同じ評価をした。安定するものと、しないものがある:

戦略 GBM 版 ロジスティック版 判定
1着≠1 top5 85.2% 84.9% 安定
1着≠1 top12 83.0% 81.6% 安定
逃げ確率 下位35% × top8 98.4% 93.4% 方向は同じだが振れる
逃げ確率 下位20% × top8 107.9% 92.1% 振れる。信用しない
「最有力セルが逃げでない」レース(5.5%)× top3 122.4% 78.8% ノイズ。n≈200

100% を超えて見えたセルはいずれもサンプルが小さく、モデルを替えると消えた。 「安定」に分類したものも B3(test を見て選んだ)の影響を受けているので、 採用前に valid で選定ルールを固定してから test を引き直すこと。

5.4 オッズを重ねても改善しない

od3(集計中オッズ)が取れる 3,352 レースで、オッズ帯フィルタを重ねた結果:

戦略 回収率 平均配当
1着≠1 top5(オッズ不使用) 85.5% 3,462
1着≠1 top5 × 30〜100 倍 76.3% 4,762
1着≠1 top5 × 20〜200 倍 82.1% 4,299

オッズ帯で絞ると、モデルが上位に置いた出目を捨てて下位を買うことになるため悪化する。 v10_kimarite はオッズを使わない。オッズを主役にする案(検討時の案A: EV = 確率 × オッズ の上位を買う)は 30〜100 倍帯で 87.4% を出したが、生成原理が「市場との乖離」で 人間に説明しづらいため今回は採らない。将来 od3 の履歴が貯まったら別スロットで再検討する。

5.5 ナラティブの実測 — argmax は使えない、確率は使える

この節が B1 の根拠。 test 3,527 レースでの Stage1 の挙動:

指標 実測
top-1 セル的中率 52.3%
「常に 逃げ_1 と言う」ベースライン 52.4%
top-2 セルに正解が入る率 62.3%
決まり手のみ(コース無視)の top-1 的中率 52.9%

モデルの argmax は「常にイン逃げ」と言うのに勝てていない。 top-1 に選ばれるセルの分布:

top-1 セル レース数 割合
逃げ_1 3,332 94.5%
差し_2 71 2.0%
まくり_2 66 1.9%
まくり_3 42 1.2%
まくり_4 9 0.3%
まくり差し_5 6 0.2%
まくり差し_3 1 0.0%

一方で P(逃げ_1) の校正は良い:

P(逃げ) 予測帯 n 予測平均 実測 差
0.0–0.2 167 15.6% 15.0% -0.6pt
0.2–0.3 320 25.7% 27.5% +1.8pt
0.3–0.4 423 35.1% 31.4% -3.7pt
0.4–0.5 526 45.1% 38.4% -6.6pt
0.5–0.6 609 55.0% 57.0% +2.0pt
0.6–0.7 671 65.0% 67.4% +2.4pt
0.7–0.8 596 74.8% 71.8% -3.0pt
0.8–1.0 215 83.4% 80.9% -2.4pt

使えるのは argmax ではなく確率値。この一点が §6 と §8 の方針を決めている。


6. 出力の見せ方(再設計済み)

6.1 当初案とその破綻

当初は「セル確率の上位 3〜4 件を『{コース}コースが{決まり手}』の文で表示する」設計だった。 実データからの出力例:

=== 202608112310 (2026-08-11) ===
展開予想:
   73.7%  3コースがまくる (= 3号艇)
    6.4%  3コースがまくり差す (= 3号艇)
    5.8%  1コースがそのまま逃げる (= 1号艇)
穴の買い目: 3-1-5 (6.2%) 3-4-5 (5.8%) 3-5-2 (5.6%) 3-5-1 (5.3%) 3-5-4 (5.1%)
結果: 3-4-5  ★的中

⚠ この例は代表的な出力ではない。 上の 2 例は P(逃げ) が最小の 5 レース(3,527 中の下位 0.1%)から選んだもの (narrative.py が argsort(P1[:, 逃げ]) の先頭を出す)。 §5.5 のとおり、 典型的な出力は 94.5% の確率でこうなる:

展開予想:
   68.0%  1コースがそのまま逃げる (= 1号艇)    ← 主予測
   ...
穴の買い目 (1着が1コース以外):
   3-1-5 / 3-4-5 / 3-5-2 / 3-5-1 / 3-5-4      ← 主予測と矛盾

「1着コース≠1」が確率モデルの外側にあるハード制約なので、 予想者が自分の主予測を否定する買い目を出す状態になる。 案C の目的(人間が理解しやすい)を正面から損なうため、この形では出せない。

6.2 再設計の方針(検証済み)

検証の詳細は §11.1。結論として 「最有力の決まり手を宣言する」のをやめ、当たる要素だけで語る。

要素 出す / 出さない 根拠
荒れ度 = 1 − P(逃げ_1) 主表示 校正誤差が概ね ±3pt(§5.5)
「荒れるなら何コース頭か」 出す 45.1% 対ベースライン 30.1%(+15pt)
「荒れるならどの決まり手か」(レース単位) 出さない 33.7% 対ベースライン 33.7%(情報量ゼロ)。1着コースを与えた条件下でもベースレート -1.1pt(§14.2)
買い目 1 点ごとの「この出目はこういう決まり手」 出す 63.2% 対ベースライン 48.7%(§14.1)
セル(決まり手 × 1着コース)の argmax 出さない 52.3% 対ベースライン 52.4%(ベースライン以下)
abstain(荒れそうなレースだけ買う) 入れない 回収率が下がる。valid で 79.6% → 65.6%

矛盾は「隠す」のではなく「立場を明示する」ことで解く。 穴予想は定義上、本命が来ない方に賭けるものなので、次のように書けば 「イン逃げ 68%」と「1着が 1 コース以外の買い目」は両立する:

このレースは イン逃げが 68% 濃厚です。
穴予想は、残り 32% ——「1 コースが飛ぶ側」に賭けます。
   荒れ度 32%   荒れるなら 3コース頭 (荒れた場合の 41%)
   買い目: 3-1-5 / 3-4-5 / 3-5-2 / 3-1-2 / 3-5-1

決まり手は レース単位の予測としては出さないが、買い目 1 点ごとの注釈としては出す。 「このレースはまくりになる」とは言えないが、「この出目 3-1-4 はまくり差しの形」とは 言える(§14)。前者は当てられておらず、後者は当たるため。

補足: 買い目 5 点の 1着コースが 1 種類に収まるのは 12.4% のレースだけで、 2 種類が 62.4%、3 種類以上が 25.2%。「3コース頭の 5 点」と言い切れることは少ないので、 「荒れるなら 3コース頭(41%)、次点 4コース頭(22%)」のように確率つきで 2 つまで出す。

進入コース順で語るので、前付けがあったレースでは「4コース(= 2号艇)がまくる」のように コースと艇番がずれる。UI では必ず両方出すこと。

7. KPI と A/B の設計

回収率だけを control と比べる現行の検定では、この予想者の価値が測れない。 平均配当・平均点数・万舟的中数(賭け金あたり)を集計に足す必要がある。 abstain は採用しないため(§11.1)、 発火率は KPI に入れない(全レースで買い目を出す)。

KPI 目標 test 実測(§11)
回収率 control と同等(有意に下回らない) 84.9% vs 86.2%、差 -1.3pt、p=0.817 → 合格
平均配当 control の 1.5 倍以上 3,427 円 vs 2,166 円 = 1.58 倍 → 合格
購入点数 control の 半分以下 5.0 点 vs 11.5 点 = 43% → 合格
万舟的中数 / 1 万円賭け control より多い 0.12 vs 0.10 = 1.2 倍 → 合格
買い目パターンの異なり数 control と同水準(下記) A案は 1 日 20 種類(上位 4 つで 83%)→ 要観察
P(逃げ) の校正誤差 各予測帯で実測との差が ±5pt 以内 8 帯中 7 帯で ±3pt 以内、最大 -6.6pt → ほぼ合格

買い目パターンの異なり数について(実装時に判明): A案は 2-3 着がスジ表由来の 定数で、per-race の情報は「1着に誰を選ぶか」だけ。実データ(2026-08-10、 156 レース)では 20 種類しか出ず、上位 4 パターンが 83% を占めた (残りは前付けで艇番が入れ替わったもの)。設計どおりの挙動だが、 「毎回同じ買い目が出る」とユーザーには見える。

B案(v10_kimarite)はレースごとに確率分布が変わるのでこの性質は無い。 A案 vs B案 の体験上の差として、log-loss や回収率とは別に観察する。 集計は fun-site predictor-stats.ts の averagePayoutYen / averageBetCount / bigHitPer10kYen で見る。

万舟 KPI は「本数」ではなく「1 万円賭けあたりの本数」で見ること。 素の本数では 穴予想 21 本に対し control 40 本で負けているが、これは control が 2.3 倍の金額を賭けているため。賭け金あたりに直すと逆転する。 点数を増やすほどこの指標は伸びる(穴 top5 0.12 / top8 0.14 / top12 0.18)一方、 回収率は k に対してほぼ横ばい(valid で 73〜85% を上下、test で 80〜85%)なので、 k は回収率ではなく「体験」の KPI で決めるべき。ただし top12 は点数 KPI を落とす。

v1_basic との比較は既存の退役判定と同じ方式に揃えること(直前買い目が組めた 確定レースのみ、control と同一レースで突き合わせたペア bootstrap 20,000 反復 + ペア並べ替え検定)。ただし A案 と B案 の比較は回収率では決着しないため、 主判定を log-loss に置き換える。§13.3 を参照。 この方式での実測は §11.2 に載せた。

判定は 3 ヶ月以上のデータが貯まってから行う。テスト期間 3,527 レースでも ペア差の 95%CI が [-13.1, +11.0] と幅 24pt あり、それより短い期間では何も言えない。

8. 実装計画

レビューによる順序変更: 当初は Step 1〜6 を通しで進める計画だったが、 BettingPicks の型変更(fun-site 側で最も重い改修)を、まだ差が示せていない 予想者のために先に払う構造になっていた。B1 の再設計も未了。 そこで Phase 1 として「荒れ度メーター」だけを先に出す順序に変更する。

Phase 1: 荒れ度メーター(買い目を出さない)

Stage1 の 1 − P(逃げ_1) だけを既存のレース詳細に表示する。

【2026-08-13】Phase 1 / 1’ / 2 とも実装完了。 以下の表は計画時のもので、 実際に作られたファイル名・パスは一部違う。現物は docs/data/estimate.md を正とすること (この節は「なぜその順で作ったか」の記録として残す)。

Step 内容 実際の成果物
1-1 Stage1 の学習スクリプトと係数テーブル scripts/build_kimarite.py / data/estimate/kimarite/tables/cell_coef_{daily,realtime}.csv
1-2 直前バッチでの推論と CSV 出力(セル確率のみ) scripts/build_kimarite_probs.py(推論)+ scripts/boatrace/kimarite.py(特徴量を学習と共有)。preview-realtime.py から呼ぶ
1-3 UI 表示 fun-site UpsetMeter.astro
1-4 校正モニタリング(予測帯別の実測ずれ) scripts/build_kimarite_calibration.py → data/estimate/kimarite/tables/calibration.csv(fun-site ではなく boatracecsv 側で月次集計)

Phase 2: 買い目を出す予想者 v10_kimarite

Phase 1 の校正が確認でき、B1〜B3 が解消してから着手する。

Step 内容 対象
2-1 B3 の解消 完了(§11.3)。実装時は選定ルールを 1着≠1 / top5 / abstain なし に固定 notebooks/ana_prediction/
2-2 B2 の解消 完了(§11.2)。ペア比較スクリプトを notebooks に移設 同上
2-3 H2 の対応: §3 の条件表と Stage2 のペア表を stt 基準で作り直す(§12.2) 同上
2-4 集計の拡張 完了(averagePayoutYen / averageBetCount / bigHitCount / bigHitPer10kYen) fun-site predictor-stats.ts
2-5 Stage2 テーブルの生成と出目出力 完了。scripts/build_kimarite_pairs.py → data/estimate/kimarite/tables/pair_table.csv、scripts/build_kimarite_picks.py → data/estimate/kimarite/picks/
2-6 レジストリ登録 完了。predictors/registry.py(slot=10)、fun-site predictors.ts、infra/run-*.sh
2-7 出目リスト型への対応 完了(Phase 1 で先行実装)。fun-site one-mark-distance.ts / bet-hit.ts / bet-payout.ts / BettingPicks.astro
2-8 ハイパラの再選定 完了。notebooks/ana_prediction/kimarite_backtest.py。γ=0.5 / β=2.4 / w=0.7(§4.3)
2-9 log-loss の A/B 集計(主判定) 完了。scripts/build_kimarite_logloss.py → data/estimate/kimarite/tables/logloss.csv
2-10 3 ヶ月 A/B 運用中(2026-08-13〜)

8.1 新規データファイル

以下は実装後の実際のパス(計画時から 2 点変わっている。理由は表の下)。

パス 内容 更新頻度 Phase
data/estimate/kimarite/tables/cell_coef_daily.csv Stage1(daily 版・race_cards のみ)の多項ロジスティック係数(凍結した 32 クラス × 特徴量)+ 補完 median と標準化の中心 / 尺度 月次 1
data/estimate/kimarite/tables/cell_coef_realtime.csv Stage1(realtime 版・preview 込み)の同上 月次 1
data/estimate/kimarite/YYYY/MM/DD.csv レース 1 行。状態 + 荒れ度 + セル確率 32 列 日次 + 直前 1
data/estimate/kimarite/tables/calibration.csv 荒れ度の校正監視(予測帯別の実測ずれ) 月次 1’
data/estimate/kimarite/tables/pair_table.csv セル × 2着コース × 3着コース の収縮済み確率(32 × 20 = 640 行) 月次 2
data/estimate/kimarite/picks/YYYY/MM/DD.csv レース 1 行。買い目 5 点 + 決まり手注釈 日次 + 直前 2
data/estimate/kimarite/tables/logloss.csv 主判定。PL 対 ブレンドの 3連単 log-loss(月次 + 累計) 月次 2
data/estimate/v10_kimarite/YYYY/MM/DD.csv 既存形式の index(N枠_強さpt ほか)。買い目はここではなく picks/ 日次 + 直前 2

計画時からの変更点:

  1. 静的テーブルは tables/ サブディレクトリに置いた。 日次 CSV と同階層に 置くと Cloud Run の cone-mode sparse-checkout が日次ファイルの全履歴まで checkout してしまうため。suji 側も同じ構造にしてある。
  2. 買い目は data/estimate/v10_kimarite/ ではなく data/estimate/kimarite/picks/。 data/estimate/{predictor_id}/ は index(強さpt)の置き場という既存の規約が あり、そこに別形式のファイルを混ぜない。A案 v9_suji(index は estimate/v9_suji/、買い目は estimate/suji/)と同じ分け方。 abstain フラグは実測で有害と判明したため列自体が無い(§11.1)。

月次再生成は既存の monthly-weights ジョブに相乗りする(build_course_rate.py と同じ扱い)。

8.2 既存アーキテクチャとの差分(注意点)

9. 再現方法

【2026-08-13】下の表のスクリプトは検討時の scratchpad にあったもので、 リポジトリには入っていない。 実装で必要になった部分は notebooks/ana_prediction/ と scripts/ に書き直してある(対応は下記)。 数値を引き直すときは下の「現在のスクリプト」を使うこと。

現在のスクリプト(リポジトリにあるもの)

パス 内容
notebooks/ana_prediction/dataset.py 進入コース順のパネル(results × stt × payouts × estimate/v1_basic)
notebooks/ana_prediction/suji_backtest.py A案の買い目バックテストと条件付き log-loss(§13.2)
notebooks/ana_prediction/kimarite_backtest.py B案の γ / β / w 再選定と test 報告(§4.3 / §5)
notebooks/ana_prediction/report.md 選定記録(スジ表の窓・収縮、B案のハイパラ)
scripts/build_kimarite.py --report Stage1 のホールドアウト log-loss
scripts/build_kimarite_calibration.py 荒れ度の校正(§14.3)
scripts/build_kimarite_logloss.py 運用中の A/B(§13.3 の主判定)

実行には .venv(scikit-learn / pandas / numpy)が要る。ただし scripts/ 配下の 推論・集計系は意図的に stdlib のみで、venv 無しでも動く。

検討時のスクリプト(scratchpad。現存しない)

以下は「どう検証したか」の記録。再実行はできない。

スクリプト 内容
kimarite.py 決まり手の分布・配当プロファイル・条件付き 2-3 着表(§2.4, §3)
build_dataset.py index × stt × sui × tkz × race_cards × results を進入コース順に結合
model_c3.py Stage1(ロジスティック回帰)+ Stage2 + ブレンド。3 分割で valid 選択 → test 報告(§4, §5.1)
model_c2.py 同上の GBM 版(§5.3 の比較用)
eval_c3.py 買い目戦略の回収率 / 的中率 / 平均配当 / 万舟数(§5.2, §5.4)
narrative.py 人間向け出力例(§6)。P(逃げ) 昇順で並べた先頭を出すので代表例ではない
probe_ev.py / probe_strategy.py 検討時の案A(オッズ EV 型)の検証(§5.4 の参考値)
resolve_blockers.py B1/B2/B3 の検証。control の買い目再現とペア bootstrap を含む(§11)
b1_options.py B1 の代替案と 万舟 KPI の再定義(§11.1, §7)
build_long.py index 非依存の拡張データセット(2025-11-01〜、38,227 レース)を作る(§12)
exp_h_m.py H1/H2/M1〜M4 の比較実験(§12)
kimarite_display.py 決まり手の表示可否の検証(§14)
(§13 の比較) A案の再現・両案のペア比較・検出力の逆算。resolve_blockers.py のヘルパーを再利用したワンライナーなので、実装時に notebooks/ana_prediction/ へ関数化して移すこと

lightgbm は libomp.dylib が無いため現状このマシンでは動かない (そのため Stage1 は sklearn の多項ロジスティック回帰にした。§5.3)。

10. レビュー指摘と対応

2026-08-11 の設計レビューで挙がった指摘。Blocking は Phase 2 着手の前提条件。

# 重大度 指摘 対応
B1 Blocking 展開予想の 94.5% が「イン逃げ」。top-1 セル的中率 52.3% は「常に逃げ」の 52.4% を下回り、買い目と主予測が矛盾する 解決 §11.1。荒れ度 + 1着コース + 立場の明示。abstain は有害と判明したため不採用
B2 Blocking control との比較が非ペア。95%CI [74, 97] で差が示せていない 解決 §11.2。同一レース 3,527 で -1.3pt / p=0.817
B3 Blocking 買い目ルールを test を見て選んでいる 解決 §11.3。valid 固定でも同じルール、test 84.9% で不変
H1 High make_cells を分割前の全期間で呼んでおりクラス集合が test に依存。月次再学習でスキーマが変わる問題も同根 解決 §12.1。train で凍結(32 クラス)。実害は 0.0007 nat で無かったが運用上必須
H2 High §3 の条件表は results の実進入コース、モデルは stt の展示進入コース。定義がずれている 解決 §12.2。stt に統一。1着コース一致率 96.8%、情報利得も stt が上
H3 High Stage2 の γ とブレンドが 強さpt を使うため、v1_basic への依存と、Stage1 だけ学習窓を伸ばせる非対称が生じる 解決(依存は受容) §12.3。index 非依存の代理は 0.35 nat 劣化。非対称を仕様として明記
H4 High 全レースで必ず 5 点買う設計。P(逃げ)=85% のレースでも穴を買う 指摘が誤り。abstain は回収率を下げる(§11.1)。全レース発火のまま据え置く
M1 Medium 欠損補完が全場共通の median 指摘が過大。§12.4 現行が最良。欠損率は最大 3.14%。別途、死に特徴量のバグを発見
M2 Medium 学習窓の長さが未定義 解決 §12.5。全履歴。ローリングにしない
M3 Medium 場/風向/天候のエンコーディングが不適切 → 7k レースでは効かず 解決(採用に反転) §12.6。31k レースでは -0.007 nat 効く
M4 Medium daily 非対応の波及(母数定義・UI・集計)が未決 解決(方針変更) §12.7。daily でも realtime の 79% の情報利得。朝から出す

11. Blocking の解決案と検証結果

§1.2 の B1 / B2 / B3 について、 実データで解決案を検証した記録。スクリプトは resolve_blockers.py / b1_options.py。

11.1 B1 の解決 — argmax を捨て、荒れ度と1着コースで語る

当初の解決案(abstain)は実測で否定された。 「荒れそうなレースだけ買う」ために P(逃げ_1) ≤ しきい値 のレースに絞ると、valid の回収率は下がる一方だった:

abstain しきい値 発火率 回収率 平均配当
なし 100.0% 79.6% 3,438
P(逃げ) ≤ 0.70 80.6% 73.9% 2,989
P(逃げ) ≤ 0.60 61.3% 76.4% 2,841
P(逃げ) ≤ 0.50 44.0% 74.5% 2,685
P(逃げ) ≤ 0.40 28.1% 65.6% 2,392

理由は明快で、「予測しやすく荒れるレース」は配当も安い(平均配当が 3,438 → 2,392 円へ 単調に低下)。市場も同じことを知っているため、絞るほど妙味が消える。 §2.1 の favourite-longshot bias が 別の形で出たもの。H4 の指摘(全レース買いは意図的な負け筋)は誤りだった。

さらに abstain では矛盾自体が解けない。しきい値を厳しくしても、 発火したレースの top-1 セルは依然として大半が 逃げ_1 のままだった:

abstain 発火率 発火レースで top-1 が逃げ 実際に 1着≠1 だった率
なし 100.0% 94.5% 45.3%
≤ 0.60 58.0% 90.5% 58.9%
≤ 0.45 33.1% 83.3% 69.2%
≤ 0.35 19.8% 72.1% 74.2%

採用する解: 「何が起きるか」を宣言するのをやめ、当たる要素だけを表示し、 穴予想という立場を明示する(§6.2)。 表示要素の候補を、実際に荒れた 1,598 レースでの的中率で比較した:

表示候補 的中率 同じことを常に言うベースライン 差
セル(決まり手 × 1着コース) top1 21.7% 18.0% +3.7pt
セル top2 のどちらか 38.4% — —
決まり手のみ(コース無視) 33.7% 33.7% ±0.0pt
1着コースのみ(決まり手無視) 45.1% 30.1% +15.0pt

決まり手は当てられていない(常に「まくり」と言うのと同じ)。 当てられているのは「荒れるなら何コース頭か」。したがって展開の文言は コース主体で書き、決まり手は内部構造として使う。

11.2 B2 の解決 — control を再現して同一レースでペア比較

fun-site の computeBettingPicks(1マーク走行距離 1 − 予測ST + 強さpt/50 − 1.6、 全着 ±0.10 窓、デッド候補の除去)を Python で再実装し、同じ test 期間に適用した。 再現の妥当性は既存ドキュメントの control 実績とよく一致する:

  既存ドキュメントの control 本再実装(test 3,527 レース)
点数/レース 11.7 11.5
的中率 46.1〜46.6% 45.6%
回収率 ≒ 86% 86.2%

同一レース 3,527 でのペア比較(ペア bootstrap 20,000 反復):

  穴予想(1着≠1 top5) control v1_basic
回収率 84.9% 86.2%
点数/レース 5.0 11.5
的中率 12.4% 45.6%
平均配当 3,427 円 2,166 円
万舟的中 21 本 40 本
万舟 / 1 万円賭け 0.12 0.10

差 -1.3pt、95%CI [-13.1, +11.0]、p = 0.817。 v6_course / v7_aggregate / v8_aionly が退役した -6.9〜-10.6pt(p < 0.01)とは 明確に違い、control と有意差なし。KPI の「回収率は control と同等」は満たす。

ただし CI の幅が 24pt あるので、「同等」と言えるのは「有意に劣らない」までの意味。 §7 のとおり判定には 3 ヶ月以上の運用データが要る。

11.3 B3 の解決 — valid だけでルールを固定

valid(2026-06-25〜07-17、3,689 レース)だけで、 1着制約 {あり, なし} × top-k {3,5,8,12} × abstain {なし, 0.70, 0.60, 0.50, 0.45, 0.40} の 48 通りを評価し、KPI(平均配当 ≥ 3,300 円 / 点数 ≤ 6 / 発火率 ≥ 15%)を満たすものから 回収率最大を選んだ。

KPI を満たしたのは 1 通りだけだった: 1着≠1 / top5 / abstain なし(valid 回収率 79.6%)。 1着制約なしの系は平均配当が 1,187〜2,079 円で 3,300 円に届かず、top8 以上は点数 KPI で落ちる。 選定の自由度がほとんど無かったため、B3 の懸念(test を見て選んだ)は結果として 実害が無かったことになる。

この 1 ルールで test を 1 回だけ評価した結果が 回収率 84.9% / 的中率 12.39% / 平均配当 3,427 円 / 万舟 21 本 / 5.0 点で、 §5.2 の値と一致する。当初の数字は据え置いてよい。

k の選び方について: 回収率は k にほぼ依存しない(valid: top3 84.6% / top5 79.6% / top8 75.9% / top12 73.1% / top16 75.7% / top20 78.8%、test: top5 84.9% / top8 79.9% / top12 81.6%)。順序が valid と test で入れ替わるのでノイズと見るべき。 一方 万舟 / 1 万円賭けは k に対して単調に増える(valid 0.05 → 0.20、test 0.12 → 0.18)。 よって k は回収率ではなく体験で決める。top12 は点数 KPI(control の半分以下)を落とすため、 Phase 2 は top5 で開始し、3 ヶ月の運用後に k を再検討する。 なお top12 の test 値は既に見てしまっているので、k を変える判断は valid と実運用データで行うこと。


12. High / Medium 指摘の解決案と検証結果

H1〜H3 / M1〜M4 を実測で検討した記録。検証は estimate/v1_basic に依存しない 拡張データセット(2025-11-01〜2026-08-11、38,227 レース。build_long.py) で行い、 分割は既存と同じ(valid 2026-06-25〜07-17 / test 2026-07-18〜08-11)。 評価は Stage1(セル多クラス)の test log-loss。スクリプトは exp_h_m.py。

12.1 H1 — クラス集合の凍結(採用)

min_n = 60 を train 期間だけで判定してクラスを凍結する。実測コストはゼロ:

クラス決定 クラス数 valid test
全期間で決定(リークあり) 29 1.8344 1.8208
train で凍結 28 1.8342 1.8201

差分は 恵まれ_4 の 1 クラスだけで、log-loss 差は 0.0007 nat(誤差)。 リークの実害は無かったが、運用上は凍結が必須(月次再学習でクラスが増減すると cell_coef.csv のスキーマが変わるため)。凍結したクラス集合をコードに定数として持ち、 サンプルが減ったクラスは収縮で潰す。

実装時の確定値は 32 クラス(2026-08-12)。上の 28 はこの検証で使った train 窓(約 7,000 レース)で n >= 60 を判定した数。本番の学習窓 (全履歴 41,740 レース)では 逃げ_2(n=188)/ 抜き_6(122)/ 差し_6(104)/ 恵まれ_2〜5(60〜71)も閾値を超えるため、26 セル + 受け皿の その他_{1着コース} 6 個 = 32 になった。一覧は scripts/boatrace/kimarite.py の CELLS。 逃げ_2 は 1着コースを展示進入基準で取っていることの帰結で (§12.2)、展示と本番で進入が変わる 12% のレースで定義どおり起きる。

12.2 H2 — 進入コースの定義は「展示進入(stt)」に統一(採用)

賭け時点で使えるのは展示進入だけなので stt 基準に統一する。ずれの実測:

指標 一致率
全 6 艇の進入が一致 87.9%
1着コースだけの一致 96.8%
 うち 枠なりレース(85.3%) 98.4%
 うち 前付けありレース(14.7%) 88.0%

ターゲット定義を変えたときの Stage1 の情報利得:

ターゲットの 1着コース test log-loss ベースレート 改善
展示進入(stt)基準 1.8201 2.0009 +0.1809 nat
実進入(results)基準 1.8037 1.9578 +0.1541 nat

実進入基準は絶対値の log-loss こそ低いが、ベースレートも低い(実進入のほうが 分布が集中している)ため、モデルが稼いでいる情報量は展示進入基準のほうが大きい。 賭け時点で使えるかどうかを差し引いても展示進入が有利。 §3 の集計表は stt 基準で作り直す(実装時)。

12.3 H3 — v1_basic への依存は残す(除去を試して断念)

強さpt を index 非依存の代理指標に置き換えられれば、パイプライン全体を 全履歴で学習できて依存も消える。実際に試した結果:

ブレンド相手の Plackett-Luce test log-loss(3連単 120 通り)
PL(強さpt) — v1_basic 依存 4.0323
PL(全国勝率 0.7 + 当地勝率 0.2 + M2率 0.1) — index 非依存 4.3814

代理指標では 0.35 nat も劣化する。 強さpt は場別に学習した重みで枠番pt (コース強度)を含んでおり、選手成績だけの線形結合では代替できない。 代替を作り込むことは v1_basic を作り直すことに等しいので、依存は受け入れる。

したがって学習窓は次の非対称になる。これを仕様として明記する:

構成要素 学習に使えるデータ 依存
Stage1(セル多クラス) 2025-11-01〜(全履歴) results / previews / race_cards のみ
Stage2(γ 変調)・最終ブレンド 2026-05-01〜(index のある期間) estimate/v1_basic

運用上の含意: v1_basic の月次 weights が更新されると v10_kimarite の出目も動く。 v1_basic を退役させる場合は v10_kimarite も同時に見直しが要る。 レジストリと docs/data/estimate.md に依存関係を明記すること。

12.4 M1 — 欠損補完は現行のまま(指摘が過大だった)

レビューで「全場共通 median は無意味」と指摘したが、実測では現行が最良だった:

補完方式 valid test
全体 median(現行) 1.8342 1.8201
場別 median + 欠損フラグ 1.8365 1.8240
レース内 z 化 1.8471 1.8308

前提が違っていた。欠損はほとんど起きない:

特徴量 欠損率
ボート2連対率 3.14%
モーター2連対率 2.89%
展示タイム 1.29%
スタート展示 ST 0.01%
何か 1 つでも欠損があるレース 5.22%

「preview 欠損が本番で常時起きる」という想定が誤りで、補完方式を凝る価値は無い。

ただし 1 件バグを発見: 検証コードで race_cards の体重列名を取り違えており、 c{1..6}_体重c が 100% 欠損の死に特徴量になっていた。実装時は列名を確認して 復活させるか、特徴量から外すこと。

12.5 M2 — 学習窓はローリングにせず全履歴(採用)

train の開始日だけを振った実測(index 非依存の特徴量、test は固定):

train 開始 レース数 valid test ベースレート比改善
2025-11-01(全履歴) 30,865 1.8342 1.8201 +0.1809 nat
2026-01-01 22,526 1.8370 1.8277 +0.1731
2026-03-01 14,695 1.8475 1.8387 +0.1628
2026-05-01 7,639 1.8612 1.8558 +0.1463
2026-06-01 3,316 1.8934 1.8890 +0.1144

単調に改善し、古いデータで劣化する兆候が無い。 既存 weights の 6 ヶ月ローリングとは 違い、Stage1 は全履歴で学習する。季節性は特徴量(気温・水温・風)で吸収されている とみなす。データが増えて過学習が問題になったら改めて窓を検討する。

12.6 M3 — エンコーディングは採用に反転(データ量が増えたため)

7,000 レースでは差が出なかった one-hot 化が、31,000 レースでは効くようになった:

構成 学習データ valid test
数値のまま 7,000 1.8465 1.8306
one-hot 場/天候 + 風向 sin/cos 7,000 1.8513 1.8334
数値のまま 31,000 1.8342 1.8201
one-hot 場/天候 + 風向 sin/cos 31,000 1.8301 1.8132

正則化が緩められるようになり、場ごとの差を使えるようになったため。 §12.5 の全履歴学習とセットで採用する。

12.7 M4 — 朝バッチ(daily)でも出せる(方針変更)

「preview が必須だから daily では出せない」としていたが、preview 抜きでもかなり効く:

特徴量セット test log-loss ベースレート比改善 realtime 比
realtime(展示進入順・全特徴) 1.8201 +0.1809 nat 100%
daily(枠なり仮定 + race_cards のみ) 1.8586 +0.1423 nat 79%
daily + 気象 1.8603 +0.1406 nat 78%

朝の時点で realtime の情報利得の 79% が取れる。 枠なり率が 85.3% あるため、 枠なり仮定のコストが小さいのが効いている。気象を足しても改善しない (朝は previews/sui が未取得なので、そもそも使えない)。

したがって:

12.8 まとめ — 実装で採る構成

項目 決定
Stage1 の学習窓 全履歴(2025-11-01〜)。ローリングにしない
Stage1 のクラス train 期間で凍結した 32 クラス。定数としてコードに持つ
進入コースの定義 展示進入(previews/stt)に統一。§3 の表も作り直す
エンコーディング 場・天候は one-hot、風向は sin/cos
欠損補完 全体 median のまま。死に特徴量(体重)は修正または削除
Stage2・ブレンド v1_basic 依存を受け入れる(学習窓は 2026-05〜)
daily 対応 daily 版モデルを別に持ち、朝から出す

これらを反映した Stage1 の想定 test log-loss は 1.8132(ベースレート 2.0009 比で +0.188 nat)。当初設計の 1.8306 から 0.017 nat の改善。


13. A案 / B案 の 2 スロット構成(採用)

§12 までの設計(B案)に対し、 「1着 = 1コース以外で強さpt 最大」+「スジ表 P(R2,R3 | R1)」だけの簡易版(A案)が どこまで迫るかを実測したところ、回収率では区別できなかった。 そこで A案を control、B案を実験スロットとして 2 つ並行運用する。

13.1 2 案の定義

  A案 v9_suji「スジ予想」(control) B案 v10_kimarite「穴予想」(実験)
1着の決め方 1コース以外で 強さpt 最大の艇(決め打ち 1 つ) Stage1 の P(決まり手, 1着コース) から 120 通り確率を作り、その中で決まる
2-3着の条件 P(R2,R3 \| R1)(1着コースだけで条件付け。6 × 20 = 120 行) P(R2,R3 \| 決まり手, R1)(セルで条件付け。32 × 20 = 640 行)+ 強さpt 変調
強さpt の使い方 1着の選定に直接使う Stage2 の γ 変調と最終ブレンドのみ
学習が要るもの スジ表 1 枚だけ Stage1 の係数(daily / realtime 2 本)+ ペア表
依存 estimate/v1_basic のみ v1_basic + 新規学習パイプライン

A案は §12 で決めた実装項目のほとんどを不要にする — クラス集合の凍結(H1)・欠損補完(M1)・学習窓(M2)・エンコーディング(M3)・ daily/realtime の 2 本モデル(M4)がすべて消え、残るのは条件付きテーブル 1 枚。

13.2 実測比較(test 3,527 レース、同一レース)

A案の数値は本番構成のもの(全履歴・収縮なし)。当初この節に載せていた 82.0% は 「train 窓のみ・収縮なし」の暫定値だった。学習窓と収縮の選定記録は notebooks/ana_prediction/report.md。

戦略 的中率 回収率 95%CI 平均配当 万舟 万舟/1万円
A案(スジ予想) 10.25% 80.6% [68, 94] 3,931 円 27 0.153
B案(穴予想) 12.39% 84.9% [74, 96] 3,427 円 21 0.119

【2026-08-13 更新】本番構成で引き直した B案の実測(test 3,614 レース、 A案と同一レース。γ=0.5 / β=2.4 / w=0.7、Stage1 は 32 クラス・全履歴):

戦略 的中率 回収率 95%CI 平均配当 万舟 万舟/1万円
A案(スジ予想) 10.27% 80.6% [69, 93] 3,927 円 27 0.149
B案(穴予想) 12.65% 87.1% [76, 98] 3,442 円 20 0.111

傾向は実験時と同じ(B案が回収率と的中率で上、A案が平均配当と万舟率で上)。 どちらも CI が 22pt あり、この差では優劣を言えないのも同じ。 3連単 log-loss では PL 単体 4.0382 に対しブレンド 3.9196、 改善 +0.1186 nat [+0.1003, +0.1376](実験時は +0.0961)。

買い目 5 点のうち 両案で共通するのは平均 2.62 点(完全一致は 1.2%)。 別物の買い目を出しているが、成績は区別できない:

比較 差 95%CI p
回収率(B − A) +4.2pt [-9.4, +17.2] 0.518
万舟/1万円(B − A) -0.045 [-0.108, +0.017] —
3連単 log-loss(PL単体 → B案ブレンド) +0.0961 nat [+0.0795, +0.1126] <0.0001

13.3 判定基準 — 回収率では決着しない

これが本節の要点。 上の CI から必要サンプル数を逆算すると:

判定指標 現在の CI 半幅 観測された効果 効果を検出するのに必要なレース数 所要期間(141 レース/日)
回収率 ±13.3pt +4.2pt 34,700 約 8.2 ヶ月
回収率(2pt を検出) 同上 2.0pt 153,000 約 36 ヶ月
log-loss ±0.0166 nat +0.0961 nat 418 約 0.1 ヶ月

回収率で A案 と B案 を比べても、8 ヶ月かけないと差が出ない。 一方 log-loss は n = 3,527 で既に効果の 5.8 倍の精度があり、決着済み。

したがって A/B の判定は次のように役割を分ける:

指標 役割 判定
3連単 log-loss 主判定。決まり手レイヤーが確率の質を上げているか 数週間で決着。B案が有意に上回らなければ B案を退役
回収率 ガードレール。破滅的な劣化の検知のみ control(v1_basic)比で -7pt 級の劣化が出たら退役。A案 vs B案 の微差は判定に使わない
万舟/1万円賭け・平均配当 体験指標。穴予想として成立しているか 目標値を置くが、有意差判定はしない(検出力が足りない)
P(逃げ) の校正誤差 B案のみ。荒れ度メーターの品質 各予測帯で ±5pt 以内

既存の退役判定との違い: v6_course / v7_aggregate / v8_aionly は回収率で -6.9〜-10.6pt という大きな劣化だったため 13〜20 日で検出できた。 A案 vs B案 の +4.2pt はその半分以下なので、同じ方法では決着しない。 予想者レジストリに log-loss を主判定とするスロットが登場するのは初めてなので、 docs/data/estimate.md に判定方法の違いを明記すること。

13.4 実装順序への反映

§8 の Phase 構成を次のように読み替える。

Phase 内容 備考
1 A案 v9_suji を先に出す スジ表 1 枚 + 既存の強さpt だけ。§12 の実装項目は不要。買い目まで出せる
1’ B案の Stage1 を 荒れ度メーターとして出す 買い目は出さない。校正を 3 ヶ月モニタする。§12.8 の構成一式が要る
2 B案 v10_kimarite を買い目つきで投入し、A案と log-loss で比較 2026-08-13 実装完了。build_kimarite_picks.py / build_kimarite_logloss.py

A案が先になるのは、Stage1 の学習パイプラインが要らず実装が軽いため。

BettingPicks の出目リスト型対応は A案(Phase 1)の時点で入れる。 A案は 1着が 1 艇に決まるのでフォーメーションでも近似できるが、 スジ表の「上位 5 ペア」を厳密に表すにはフォーメーションでは足りず、 最小フォーメーションで近似すると実際に買う点数が 5 点より増えて回収率が変わる。 §13.2 の A案の実測値(80.6% / 5.0 点)は 厳密な 5 点前提なので、近似すると数字が再現しない。B案でも同じ型が要るため、 先に入れておくほうが二度手間にならない。

型変更の内容(§8.2 も参照):

// 現行
type BettingPicks = { first: number[]; second: number[]; third: number[] };

// 拡張後 (直和型)
type BettingPicks =
  | { kind: "formation"; first: number[]; second: number[]; third: number[] }
  | { kind: "combos"; combos: [number, number, number][] };

countFormationCombinations / isFormationHit / computeBetPayout / BettingPicks.astro を kind で分岐させる。既存 4 予想者は kind: "formation" の まま挙動を変えないこと(回収率の連続性を壊さない)。バッチと web が 必ず同じヘルパー経由で解決する現行の規約は維持する。

予想者 ID: v9_suji(表示名「スジ予想」)/ v10_kimarite(表示名「穴予想」)。 kimarite は「決まり手でセル分解した確率モデル」という内部構造を指す。 決まり手の表示は §14 のとおり両案共通のレイヤーで行う ので、ID が表示内容を約束するものではない点に注意。


14. 決まり手の表示方式

§11.1 では「決まり手は 情報量ゼロ」と判定したが、あれはレース単位の周辺 argmax を測ったもの。 表示形式を変えて測り直したところ、出せる形と出せない形がはっきり分かれた。 検証は kimarite_display.py(test 3,527 レース)。

14.1 出せる形 — 出目ごとの決まり手注釈

買い目 1 点ごとに「この出目はどの決まり手を想定した形か」を添える。

穴の買い目
  3-1-4   3コースの まくり差し ── 内を差して 1コースが残る形
  3-4-5   3コースの まくり     ── 内を全部かぶせて外が続く形
  3-1-5   3コースの まくり差し

的中した買い目 437 点で、注釈の決まり手が実際の決まり手と一致した率:

注釈の作り方 一致率
1着コース別の最頻決まり手(コースしか見ない) 48.7%
出目(3 コースの並び)別の最頻決まり手 — 静的テーブル 63.2%
モデルの P(セル \| その出目) の最有力セル 60.6%

+14.5pt の情報がある。 効く理由は明快で、決まり手を特定しているのは 事前情報ではなく「2着・3着の並び」だから。1コースが 2着に残っていれば まくり差し、外が続いていればまくり —— 出目の形そのものが決まり手を語っている。

重要: この注釈にモデルは要らない。 静的テーブル(出目 → 最頻決まり手、120 行)が モデルの P(セル|出目) と同等以上(63.2% vs 60.6%、n=437 なので差は誤差範囲)。 したがって 決まり手の注釈は A案 v9_suji でも同じように出せる。 予想者ごとの差別化要素ではなく、両案共通の表示レイヤーとして実装する。

14.2 出せない形 — レース単位の決まり手予測

「このレースはまくりになる」という予測は、1着コースを与えた条件下でも 経験ベースレートを上回らない:

実際の 1着コース n モデルの的中率 ベースレート(そのコースの最頻決まり手) 差
1コース 1,929 95.9% 95.9%(逃げ) ±0.0pt
2コース 478 58.2% 60.0%(差し) -1.9pt
3コース 481 37.8% 37.8%(まくり) ±0.0pt
4コース 356 39.3% 44.9%(まくり) -5.6pt
5コース 212 57.5% 57.5%(まくり差し) ±0.0pt
6コース 71 45.1% 57.7%(まくり差し) -12.7pt
加重平均 3,527 73.8% 74.9% -1.1pt

どのコースでもベースレートを超えない。 レース単位で「まくりが来ます」と 言い切る表示は、事実として当てられていないので出さない。

14.3 確率としてなら出せる

argmax は使えないが、確率値としての校正は良好なので、 「まくり 16% / まくり差し 10% / 差し 12% …」という分布の提示は成立する:

決まり手 予測平均 実測 差 予測上位20%での 予測 / 実測
逃げ 53.6% 52.4% -1.2pt 78.0% / 74.6%
まくり 16.0% 15.3% -0.7pt 30.2% / 27.5%
まくり差し 10.4% 12.7% +2.3pt 18.9% / 19.7%
差し 11.9% 12.2% +0.3pt 22.8% / 22.4%
抜き 6.2% 5.9% -0.3pt 9.1% / 7.8%

ずれは -1.2〜+2.3pt。「まくりが来る」ではなく「まくりの確率は 30%」なら正しい。 ただし §14.2 のとおり argmax を取ると ベースレートに負けるので、必ず全決まり手の確率を並べて出し、1 つを強調しないこと。

14.4 まとめ — 表示の可否

表示 可否 根拠 どちらの予想者で
買い目 1 点ごとの決まり手注釈 ○ 63.2% 対 48.7% A案・B案とも(静的テーブル)
決まり手の確率分布(全種類を並べる) ○ 校正誤差 -1.2〜+2.3pt B案のみ(Stage1 が要る)
荒れ度 1 − P(逃げ) ○ 校正誤差 概ね ±3pt B案のみ
「荒れるなら何コース頭か」 ○ 45.1% 対 30.1% B案のみ
「このレースは○○が決まる」(argmax) × 条件付きでもベースレート -1.1pt —

参考