競艇(ボートレース)の予測AIを作るとき、最初に用意すべきなのはモデルや自動購入ではなく、「なぜこの艇を候補にしたか、なぜ見送ったか」を後から再現できる紙上(ペーパー)運用の判断台帳です。あわせて、条件が足りないときは候補にしない「フェイルクローズ」のゲートを最初から組み込みます。

この記事では、シリーズ第1回として、判断台帳の列設計、フェイルクローズの実装例、stake=0で運用する理由、Macとサーバーの役割分担をまとめます。競艇は1レース6艇で1日のレース数も多く、データ検証の題材に向いています。

判断台帳を中心にした競艇AI

この記事の結論

  • 「確率が高い」「期待値が高い」「締切前に実際に買える」は別々の条件として記録する。
  • 台帳には候補だけでなく、対象レースすべての判定と見送り理由を残す。
  • 候補CSVは上書きせず、日付・モデル版・戦略版ごとに別ファイルにする。
  • オッズ欠損・締切直前・期待値不足・上限到達・HALTのどれかで候補にしない(フェイルクローズ)。安全条件はモデルの外に置く。
  • 現在は stake=0・mode=paper の完全なペーパー運用。

背景:なぜ台帳から始めるのか

競艇AIは、出走表、選手・モーター・ボートの成績、展示情報、締切前のオッズから各艇の勝率と期待値を計算します。しかし、どんなに良い予測でも、使ったオッズの取得時刻が分からなければ本番に近い検証とは言えません。締切30秒前に計算が終わっても、実際には間に合わない可能性もあります。

筆者は学生時代、競馬でスクレイピングから自動購入まで作り、精度不足で買うほど損をする結果になりました。その後「競艇のほうが勝ちやすいらしい」という噂をきっかけに競艇で作り始めましたが、同じ失敗を避けるため、記録の仕組みから作っています。

判断台帳の列設計

列例目的
race_id / selectionレースIDと艇番結果と1対1で結ぶ
created_at / close_time判定時刻と締切時刻締切前の判断だったと示す
odds / odds_timestamp実際のオッズと取得時刻市場情報の鮮度を確かめる
probability / market_probabilityモデルの確率と市場の確率どう比べて判断したか
expected_value / decision_reason期待値と判定理由どの条件で落ちたかを追う
stake / mode0円・paper実際のお金を使っていないことを明示する

モデル名とバージョンも記録し、1つの候補を後から再計算できるようにします。

見送りも含めて全レースを記録する理由

候補が少ない日に「モデルが慎重だった」と決めつけないためです。実際には次のような運用上の理由で減っていることがあります。

  • オッズが足りなかった
  • 取得が遅れた
  • 締切まで時間がなかった
  • 1日の上限に達した

理由の割合を見れば、モデルの問題か運用の問題かを分けて考えられます。

候補CSVを上書きしない

上書きすると、改良後のモデルが昔から正しかったように見えてしまいます。日付・モデル版・戦略版ごとに別ファイルにし、結果確定後に精算の表と結びます。

フェイルクローズ:条件が足りなければ候補にしない

フェイルクローズとは、条件が足りないときに推測で進めず止まる設計です。次のどれかに当てはまれば候補にしません。

  • オッズが6艇分そろっていない
  • 取得時刻が締切に近すぎる
  • 期待値が足りない
  • 1日の上限に達した、または停止指示(HALT)が出ている
if not market_ready:
    return skip("market_odds_incomplete")
if minutes_to_close < MINUTES_TO_CLOSE:
    return skip("too_close_to_deadline")
if expected_value < ev_threshold:
    return skip("expected_value_below_threshold")
if halted or daily_limit_reached:
    return skip("risk_gate")
return candidate(stake=0, mode="paper")

ポイントは、安全条件をモデルの中に混ぜず、モデルの外側に独立した関所として置くことです。後から付け足すと抜け道が残りがちなので、最初から入れました。

フェイルクローズにすると候補は減りますが、締切後の情報や時刻不明のオッズ、適当に埋めた値で件数を増やすほうが、検証をはるかに大きく壊します。「データが足りないときに買わない」ことも戦略の一部です。

stake=0(ペーパー運用)にしている理由

モデル・データ収集・締切処理のどこで失敗したかを切り分けるための、開発の順番として選んでいます。将来実際のお金を扱うなら、少なくとも次が必要だと考えています。

  • 本番を有効にする明示的なフラグ
  • 1レース・1日の上限
  • 連敗時に止まる仕組み
  • エラーやオッズの急変で止まる仕組み
  • 購入・見送り・例外をすべて残すログ

現時点ではこれらを自動購入機能としてではなく、ペーパー候補の品質を測るチェック項目として扱っています。

収集はサーバー、開発はMacに分ける

  • 継続的なデータ収集はサーバー、開発と可視化はMacで行う。
  • Macで本番の候補化をうっかり動かさないよう、MAC_COLLECTION_ONLY のような安全フラグを用意した。
  • 収集・集計・候補化を別ジョブにし、どの段階でデータが失われたか追えるようにする。
  • モデルを更新しても、過去の台帳を新しい確率で計算し直さない。変更前の判断を残し、モデルの違いと市場の変化を混同しない。

まとめ:「準備ができた」と言えるチェックリスト

  • 任意の1日について、予定表 → オッズ一覧 → 予測出力 → 判断台帳 → 結果の精算を順にたどれる
  • 候補がゼロの日でも、なぜゼロかを台帳だけで説明できる
  • 全行にモデル版と判定理由がある

考え方は競馬AIの第1回と共通ですが、競艇は1日のレース数が多いぶん、記録の仕組みがないとすぐに何が起きたか分からなくなります。

注意:本記事は個人開発・データ分析の記録です。的中や利益を保証するものではなく、舟券の購入を勧めるものでもありません。