競艇AI予想に使う特徴量を423個まで増やした設計思想

PR本記事はアフィリエイト広告を含みます。リンク経由でのご購入により運営者に成果報酬が支払われることがありますが、読者への価格や条件は変わりません。

結論:特徴量423個に行き着いた理由は「人間の勘」を分解したから

競艇AIを作り始めた当初、私は出走表に書いてある数字(全国勝率・モーター2連率・展示タイム)をそのまま入れただけのモデルで満足していました。特徴量は約30個、3連単的中率は概算で6%前後。控除率25%を超えられず、回収率は80%台で停滞しました。

転機は、ベテラン予想家が「この選手は江戸川の追い風3m以上で外枠なら危ない」と語るのを聞いたときです。これは1つの判断ではなく「会場 × 気象 × コース × 選手特性」の4要素の組み合わせ。人間が無意識にやっている特徴量エンジニアリングをそのまま機械に教えれば良いと気づき、最終的に423個まで増やしました。以下、その内訳と設計思想を書きます。

出走表から抽出した基本特徴量127個の中身(選手・モーター・展示)

最も基礎となるのが、BoatRace公式サイトの出走表から抽出する127個です。私はBeautifulSoup + requestsで毎朝6時にスクレイピングし、SQLite(約1.2GB)に保存しています。

内訳の目安は以下の通りです。

  • 選手属性:級別(A1/A2/B1/B2)、年齢、体重、支部、登録番号、当地勝率、全国勝率、複勝率(ここまで約40個)
  • モーター・ボート:モーター2連率・3連率、ボート2連率、節間使用回数、整備士情報(約35個)
  • 展示情報:展示タイム、進入予想、スタート展示のST、周回展示の伸び・回り足の評価(約30個)
  • 直前情報:体重調整、部品交換有無、気象速報の最新値(約22個)

ここで失敗したのは、「全国勝率」を生数値のまま入れたことです。A1選手とB2選手の分布が違いすぎてLightGBMの分割が荒くなり、級別ごとに正規化(z-score)しただけで的中率が0.4ptほど改善しました。

過去成績から作った時系列特徴量98個と、リーク防止のために捨てた12個

選手の「調子」を捉えるには時系列が不可欠です。私は直近30走を3つの窓(直近5走 / 10走 / 30走)に分け、それぞれで平均ST・1着率・3連対率・コース別成績を計算しています。

特に効いたのは以下の特徴量でした。

  • 直近10走のコース別1着率(特に1コースと6コース)
  • 直近5走のスタート偏差(ST平均ではなく標準偏差)
  • 当該会場での直近1年の勝率トレンド(線形回帰の傾き)

一方、初期に入れていた12個はデータリークを起こして全部捨てました。具体例は「当日他レースの結果」を参照していたケース。これは時系列順にソートした上で同日同節のレース番号より前しか使えない、という制約をデータパイプライン側で強制する必要があります。私はpandasのgroupby + cummeanを使い、必ず「current_race_number未満」でフィルタしてから集計するように書き直しました。

会場・気象・潮位の環境特徴量76個 ― 競艇特有の「水面差」をどう数値化したか

競艇は競馬と違い、24会場すべてで水質・水面形状・風の通り方が異なります。江戸川は荒水面、平和島は海水で潮位の影響が大きく、住之江は淡水で静水面――この差を数値化するのに76個を使いました。

  • 会場マスタ:水質(淡水/海水/汽水)、1マークまでの距離、ピット離れ角度(24会場 × ダミー変数化)
  • 気象:風速・風向(8方位)、気温、波高、天候カテゴリ
  • 潮位:満潮/干潮の時刻差、潮位差(cm)、満ち潮/引き潮フラグ

特に「会場 × 風向」のクロス特徴量は重要で、たとえば「平和島で南東風4m以上」のときだけ6コース1着率が概算で2倍に跳ね上がるパターンを発見しました。Boatrace公式の気象データは1時間ごとの更新なので、レース開始時刻に最も近い値を補間して使っています。

選手間の相互作用を表す関係性特徴量122個(枠なり率・進入隊形・追い風適性の組み合わせ)

ここが423個の中で最も時間をかけた領域で、合計122個あります。1人の選手の能力ではなく、6人並んだときに何が起きるかを表現する特徴量です。

代表的なものを挙げます。

  • 各選手の枠なり進入率(直近100走)
  • レース全体の進入隊形予測(イン屋がいるか、まくり屋がいるか)
  • 自艇の前に入る選手のST平均との差分
  • 追い風適性スコア(過去の追い風レース勝率 − 向かい風勝率)の6選手分

この領域を作り込んだことで、3連単的中率は8%台から10%台前半に上がりました。組み合わせ特徴量は爆発しがちですが、事前にドメイン知識でフィルタ(明らかに無関係な組み合わせは作らない)してから生成しないとメモリが持ちません。私は最初200個ほど作って、SHAP値が低い78個を削った結果が122個です。

423個まで増やして分かった「効く特徴量」TOP20とSHAP値で見た寄与度

LightGBMで学習し、SHAP値で寄与度をランキングした結果、TOP20の傾向は以下のようになりました(順序は概算)。

  1. 1コース選手の当地1着率
  2. 展示タイム(コース別ランキング)
  3. 1コース選手のST偏差(直近10走)
  4. モーター2連率
  5. 階級差(1コース vs 2コース)
  6. 風速 × 1コース勝率
  7. 1マーク先取り予測スコア
  8. 平均年齢差
  9. 2コース選手のまくり差し成功率
  10. 当地勝率トレンド

意外にも「年齢」単体は寄与度が低く、年齢差(チルト差・経験差)として相対化したほうが効きました。逆にSHAP値ほぼゼロだったのは「選手の血液型」「登録番号下2桁」など、入れてみたものの全く寄与しなかった迷信系特徴量です。

次に追加予定の特徴量と、増やしすぎて精度が落ちた失敗パターン

現在追加検討中なのは、以下の3カテゴリです。

  • 音声データから抽出した実況のテンション(スタート前ピット情報)
  • TwitterのレースID言及数(人気の代理指標)
  • 節間の選手疲労度(睡眠時間・移動距離の推定)

ただし闇雲に増やすと過学習します。私は460個まで増やした時点で、テストデータ的中率が逆に0.6pt下がりました。原因は「直近1走のSTを5回違う窓で集計した」など、相関0.95以上の冗長特徴量を入れすぎたこと。Borutaで重要度が低い特徴量を機械的に落とし、最終的に423個に戻したらピーク性能に戻りました。

特徴量は「多ければ良い」ではなく、「人間の判断プロセスを過不足なくカバーしているか」で評価するのが私の結論です。

まとめ

競艇AIの特徴量を30個から423個に増やす過程で学んだのは、ドメイン知識を持たないと特徴量設計は迷子になるという当たり前の事実でした。出走表127 + 時系列98 + 環境76 + 関係性122という配分は、私のデータと会場では機能していますが、別の競技や別の会場特化なら配分は変わるはずです。次回は、この423個をLightGBMとTabNetでアンサンブルした結果と、回収率が100%を超えた瞬間の話を書きます。

関連ツールを見る

この記事で紹介したツール・サービスをまとめてチェック。

おすすめ

エックスサーバー

国内シェアNo.1のレンタルサーバー。WordPressブログをすぐに始められる。このブログも実際にXserverで運営しています。

Xserverを見てみる →

ムカイ
この記事を書いた人

ムカイ

個人事業主エンジニア。C#フルリモート案件に参画しながら、Claude Codeを使ってAI×副業の自動化・コンテンツ制作を実践中。「稼ぐ仕組みを作るのが好き」がモットー。

コメントを残す