「未来のデータが視えてしまっていた」─ 仮想馬柱9走に紛れ込んでいた"予想対象レース自身"を追った2週間の記録
=== get_virtual_past9()自己参照リーク修正+推しレースTOP5→TOP3化 ===
タグ: Python 個人開発 データ分析 競馬AI 統計
はじめに
競走馬の予想スコアを算出する自作Python予測システム(V5py / V58py)を個人開発しつつ、その週次の成果をYouTubeチャンネル「71才隠居ジジィの競馬新聞」で動画にして公開しています。
前回の記事(「ダークマターの領域に食い込みたい」)では、会場特徴×補正値の相関を検証する新ラインdarkmatterを立ち上げた3週間を紹介しました。今回はその検証データを実際に分析している最中に見つかった、もっと根の深い話です。
新潟会場で、本家V5pyとdarkmatter版の複勝的中率を比べたら16.4ptもの差が出ていました。「たまたま母数が違うから」で片付けかけたこの差を掘り下げていったところ、たどり着いたのは「まだ起きていないはずのレース結果を、予測システムが"未来から"参照してしまっていた」という、AIによる予測システムとしては致命的なバグでした。
対象期間は8月25日〜9月7日の2週間です。
今回作ったツール
| ツール | 役割 |
|---|---|
| whisper_rename.py | Whisperでナレーションwavを自動文字起こしし、ファイル名に内容プレビューを付与する補助ツール |
| n_reliability_flag.py | 各種判定条件のサンプル数(N)が統計的に信頼できる水準に達したかを自動判定し、比較検証マスターxlsxにフラグ表示する新ツール |
メインのV5py本体(No.103)・V58py(No.71)・darkmatter版(No.106)・スライド画像加工.py(No.87)も、この期間に重要な改修を経ています。
発端:新潟だけ16.4ptも違う
8月29日・30日分の検証結果が出たタイミングで、こんな指摘がありました。
yuji 「気になる点はV5とV58py及びV5とV5_darkmatterの新潟の比較において48.9(V5vsV58)・65.3(V5vsdarkmatter)16.4%も差があることです 確かに母体数が違うとは言えこの差の違いは気になるところです 考えられることといえば、取得データで空白覧があり正常に取れていなかったデータがそうとう影響しているのではないのだろうか」
新潟223レース全件、複勝的中列など2,230セルを点検しましたが、空白セルは実質0件。「空白データ汚染」説はここで棄却されました。
次に疑ったのは「見ている期間がそもそも違う」という仮説でした。V5⇔V58の比較(tekichuritu_master)は新潟223レース=5月開催143レース+8月開催80レースの通期合算だったのに対し、V5⇔darkmatterの比較(darkmatter_hikaku_master)はnew_darkmatter運用開始後の8月開催72レースのみ。実際、5月開催のみだと複勝的中率41.3%、8月開催のみだと62.5%と、期間によって21.2ptもブレていました(z検定でp<0.01の統計的有意差)。
季節要因で説明がつきそうに見えたところで、鋭い一言が飛んできます。
yuji 「V5は全く同じpyで計算されているのに、近づいて行かなければ、有る意味darkmatterではないのか」
たしかに、V5側はどちらの比較でも同じseiki_keibayosou_app_v5.pyが計算しているはずです。ここで「期間差」という説明だけでは足りないことに気づき、掘り下げを続けました。
決定的な2レース ─ 同じレースなのに判定が真逆
集計をやり直す過程で、8月の日付を集計する際に8/1・8/2・8/9の3日分を誤って5月側に含めていたラベル分類ミスを訂正。そのうえで、tekichuritu_master側とdarkmatter_hikaku_master側で共通するレースID(新潟72レース分)を1件ずつ突き合わせたところ、2レースだけ、◎(本命)の的中判定が正反対になっていました。
| レースID | tekichuritu側 | darkmatter側 |
|---|---|---|
| 202604020702(8/15 新潟2R 2歳新馬) | ─(外れ) | ◎(的中) |
| 202604020703(8/15 新潟3R 2歳新馬) | ─(外れ) | ◎(的中) |
この2レース分の差(+2/72=+2.8pt)を足すと、45/72=62.5%→47/72=65.3%とぴったり一致します。「65.3%」という数字を作っていた実体は、季節差でも8/8欠落でもなく、この2レースの「同じレースなのに違う判定」だったわけです。
両レースとも2歳新馬(出走馬全頭が初出走)で、通常なら「過去走データなし」の早期return分岐を通るため、"馬柱5走→9走"の蓄積機能とは無関係のはずでした。実際にレースごとの完全比較xlsxを取り寄せて突き合わせると、事態はもっと奇妙でした。
| 実行タイミング | ◎馬 | スコア | 結果 |
|---|---|---|---|
| tekichuritu側(8/16 23:30実行) | テリオスブギ | 0.89 | 9着で外れ |
| darkmatter側(8/17 17:37実行) | カナデルペガサス | 17.17 | 1着で的中 |
1位・2位が入れ替わっただけでなく、全馬のスコアの桁そのものが約8〜20倍に変わり、順位が総入れ替えになっていました。決定打は、8/17時点で出走馬全頭が「DB1走」を持っていたことです。2歳新馬なのに過去走が1走あるとしたら、その1走は「たった今予想しようとしているレースそのもの」以外にあり得ません。
正体は"未来からのリーク"
コードを確認すると、仮想馬柱9走を取得するget_virtual_past9()(2026年3月新設の関数)には、日付やレースIDによる絞り込みが一切ありませんでした。
# 修正前
def get_virtual_past9(horse_id: str, df_accum: pd.DataFrame) -> pd.DataFrame:
"""
蓄積DB(horse_database_accumulated_v35.csv)から
同一馬IDの過去レコードを日付降順で最大VIRTUAL_PAST_MAX件取得して返す。
"""
if df_accum.empty or horse_id not in df_accum['馬ID'].values:
return pd.DataFrame()
df_horse = df_accum[df_accum['馬ID'] == horse_id].copy()
df_horse = df_horse.sort_values('日付', ascending=False).reset_index(drop=True)
return df_horse.head(VIRTUAL_PAST_MAX)
「馬ID一致→日付降順→先頭9件」だけ。レース終了直後にDB同期処理が走ると、そのレース自身の結果が対象馬の「1走目」として蓄積DBに書き込まれます。予想を出した直後(tekichuritu側)はまだこの同期が済んでいないので影響を受けませんが、後日改めて同じレースを再実行(darkmatter側)すると、予想対象レースの実際の結果を、その馬の"過去の成績"として読み込んで予想するという、いわゆるデータリーク(data leakage)が起きていたのです。新馬戦だと「過去走0件のはずが1件ある」という形で矛盾がくっきり見えたため、今回発覚に至りました。
修正は、対象レース日付以降のレコードを除外するexclude_date引数の追加です。
# 修正後
def get_virtual_past9(horse_id, df_accum, exclude_date: str = None):
if df_accum.empty or horse_id not in df_accum['馬ID'].values:
return pd.DataFrame()
df_horse = df_accum[df_accum['馬ID'] == horse_id].copy()
if exclude_date:
df_horse = df_horse[df_horse['日付'] < exclude_date]
df_horse = df_horse.sort_values('日付', ascending=False).reset_index(drop=True)
return df_horse.head(VIRTUAL_PAST_MAX)
8/15新潟2Rを修正版で再実行すると、「未蓄積=15頭」「◎テリオスブギ(0.89)」に復帰。漏洩が止まったことを確認できました。「真」なのは漏洩前のtekichuritu_master側(48.9%・62.5%)であり、darkmatter_hikaku_master側の65.3%はこの自己参照バグによって不当に底上げされた数値だった、という結論です。
横展開とマスターファイルの是正
V5py本体を直しただけでは終わりません。V58py・darkmatter版V5にも同じ構造の関数があったため、行番号に依存しない内容ベースの一括パッチで横展開しました。
yuji 「バグで2重に登録されたデータを整理できるか」
調べると、CSV側の行が単純に重複していたわけではなく(254レース中、重複race_idは0件)、集計済みマスターファイル(darkmatter_hikaku_master.xlsx)の中に「まだ修正前の漏洩した結果」がそのまま残っている状態でした。幸い、両マスターとも「同一race_idの既存行を削除してから新しい行を追記する」自己クリーニング設計が既に入っていたため、該当2レースを修正済みコードで再実行するだけで両マスターが自動的に是正され、新潟の複勝的中率は**65.3%→62.5%**に収束しました。
中京・札幌など新潟以外の会場に同種のリークが残っていないかの再検証は、本記事執筆時点でまだ手つかずのまま残っています。
おまけ①:推しレースTOP5→TOP3化
これとは別の話として、YouTube動画のナレーション台本・音声テキスト生成がここ数週間セッション容量の制限(5時間)内に収まらなくなっていたため、「推しレースTOP5」を「TOP3」に変更しました。
yuji 「推しレースTOP5Rだと容量的に制限内にナレーション台本・textを作るのが厳しい 3Rにして状況により+話題の1レース(G1等)に内容を変えたほうが安定的に金曜夕方に投稿できると考える どう思うか?」
単純な件数変更のはずが、①推しレース表テーブルがキャンバス右端からはみ出す、②3段表がキャラクターに重なる、といった副作用が連鎖しました。特に②は、TOP5時代は行数が多く縦方向の自動縮小ロジックが副次的に働いていたため表面化していなかった潜在バグで、TOP3化で行数が減ったことで初めて顕在化した典型例でした。キャラクターの位置を動的に検出して表幅の上限を決めるロジックに統一する形で恒久修正しています。
おまけ②:N信頼性フラグという"物差し"
新潟データ差異の調査と並行して、推し馬券ランキングのBUY(購入推奨)/SHOW(参考)/SKIP(見送り)ゾーン判定の妥当性を、23日間・803レースの大規模データで検証しました。ゾーン分けの有効性(BUY 64.0% > SHOW 49.4% > SKIP 47.8%)は裏付けられた一方、SHOW区分の内訳を分解すると「指数差3〜5pt×騎手ランクS/B」という条件がSKIP全体より的中率が低いという逆転現象も見つかりました。
ただ、この手の知見をすぐ判定ロジックへ反映するのは危険です。条件を絞り込むほどサンプル数はあっという間にn=20〜30程度まで減ってしまうからです。
yuji 「そのNをカウントして目的の数値になったら、フラグを立て tekichuritu_master.xlsxとdarkmatter_hikaku_master.xlsxに 表示させる仕組み作れないか」
そこで、統計的信頼区間の式から「⚠️参考段階(±10%)」「✅確信段階(±7%)」の2段階の目標Nを算出し、build実行のたびに自動反映するn_reliability_flag.pyを新設しました。「手動にしたら失念するのが人間だ」という一言で、比較検証バッチの末尾に自動で呼び出す設計に落ち着いています。
まとめ
- 仮想馬柱9走の自己参照リーク:予想対象レース自身の結果が、DB同期のタイミング次第で「過去走」として紛れ込み、的中率を不当に押し上げていた。V5py・V58py・darkmatter版の3ファイルに横展開し、汚染された集計も是正済み。中京・札幌など新潟以外の会場の再検証は今後の課題。
- 推しレースTOP5→TOP3化:ナレーション制作の容量問題への対策。単純な件数変更のはずが、キャンバスはみ出し・キャラクターとの重なりという副作用の連鎖を招いた。
- N信頼性フラグの新設:見つかった知見をすぐ反映せず、サンプル数の信頼性を自動監視する仕組みを先に整えるという、慎重な運用方針をシステム側から支えるツール。
教訓
- 「たまたま母数が違うから」で片付けない。差の大きさに違和感があれば、期間・母集団を揃えて突き合わせるところまでやる。
- 新馬戦のような「本来ゼロであるべき値」が崩れているサンプルは、構造的バグの発見に直結しやすい。
- 一度直ったように見えても、条件を変えたサンプルで再検証しないと分からない─これはV7.0期間から続く共通の教訓でした。今回も新馬戦という特殊なサンプルの矛盾から、重大なバグが発覚しています。
- 便利な知見ほど、反映を急がない。サンプル数が語れる水準に達しているかを先に確認する仕組みを作る。
個人開発は課題を1つ潰すと次の課題が見える、という循環が今回も続いています。次回は、中京・札幌など残る会場の再検証と、hosei_mixerによる補正値探求の話を書く予定です。
@yujiiwadate0247 (岩舘 雄二)
"I'm a 71-year-old retiree enjoying my leisure time by developing Python programs on Ubuntu. I am currently focused on building a horse racing prediction system."