「最初に取った人気を全員で使う」── pre_race仕様の完成形、"早い者勝ちの共有キャッシュ"への設計転換とN管理の是正
タグ:競馬予想 / Python / データ分析 / デバッグ / 設計
対象期間:2026年9月22日〜10月5日 制作者:yuji
はじめに
前回の記事(9/8〜9/21)では、レース後のオッズが予想に紛れ込む「post_race汚染」を見つけ、pre_raceを唯一の正基準とする方針を確定した。ただし、その時点では2つの宿題が残っていた。
- 対策(スナップショットモード)が比較検証の片側(V5 vs darkmatter版)にしか入っていない。V5 vs V58側は手つかずだった。
- pre_raceの供給元がV5py一本に固定されており、V5のbuildが漏れた日の保険が無い。
本記事は、この2つを片付けた14日間の記録である。途中、「確認したい結果が全部空になる」遠回りや、N(サンプル数)管理ツールの不具合、同じレースを再実行すると日付が消える問題など、いくつもの副産物が出てきた。
プロジェクトサマリー
| 項目 | 数値 |
|---|---|
| 対象期間 | 14日間(9/22〜10/5) |
| 新規 | 共有ファイル1(prerace_snapshot.csv)、マスターxlsx 1(v58_hikaku_master.xlsx)、一回きりの手入れpy 1 |
| 改修したpy | 7本(予想本体3・比較検証2・N管理1・バッチ1) |
| 修正履歴 | 30件超 |
| 主な恒久修正 | 3系統:共有pre_raceスナップショット(本体3本+比較検証2本)/N管理の是正(締切日・状態判定・V5のNの読み込み元)/実行履歴列の追加 |
| 廃止 | tekichuritu_master.xlsxの生成・バックアップ |
| 特記事項 | 実機で4パターンの相互共有を確認。未解決は「V5pyのbuildが実際に漏れた日」の実地確認 |
1. 発端:V58のNが、どこにも存在しなかった(9/22)
きっかけは「V5 vs V58の比較検証xlsxは、darkmatter系のコピー版になっているか」という確認だった。調べると状況は逆で、darkmatter側だけが新しいN管理ツール(n_tracker.py)に切り替わり、V58側は退役したはずの旧ツールに取り残されていた。V58の的中率のNは、どこにも集計されていなかったことになる。
「darkmatter版ですでに完成しているので同じ仕様にしてくれれば何をするにも分かりやすいと思わないか」
この方針で、darkmatter_hikaku_master.xlsxと同一仕様の v58_hikaku_master.xlsx を新設した。新規ロジックはゼロで、既存のヘルパー関数を再利用しただけである。
ところが1レース確認すると、実機にこう出た。
V58 の計算に失敗しました: yosou_hikaku_data_v58.csv に
実行フェーズ='pre_race' の行が1件もありません。
分離作業のバグではない。V7.4で積み残した「V58側にはスナップショットモードが無い」という穴が、そのままエラーメッセージとして表に出てきたのだ。
2. 「V5が唯一の供給元」は誤りだった(9/25〜9/27)
私は最初、「V5pyを唯一の供給元にして、比較検証側は保存済みの結果を読むだけにする」という案を出した。ところがYujiの依頼は、その案より広かった。
「全部やらなければ、今まで行った各pyの修正が意味をなさない。ここで全ての結果検証システムを完全な物に仕上げる」
さらに、要件を次のように言い換えられた。
「簡単に書けば V5py build実施 → pre_raceスナップショットデータ取得 V58py build実施 → pre_raceスナップショットデータ取得 V5_darkmatterpy build実施 → pre_raceスナップショットデータ取得 ただしどれか1つのpyで取得されたpre_raceスナップショットデータは他の2つをbuildしても取得しないで、共有される」
つまり正しい設計は「V5が唯一の供給元」ではなく、3本のうち最初にbuildされた1本が供給元になり、残りは共有して使うものだった。自分の理解に寄せて解釈してしまった、という教訓がここで残った。
設計は次の通りにまとめた。
- 共有ファイル
04_hikaku/prerace_snapshot.csvを新設(race_id・馬名・馬ID・市場人気・記録日時・取得元の6列だけ) - 各本体py(V5・V58・darkmatter版)は、起動時にまず共有ファイルを見る。あればそれを使い、result.html照会はスキップしてpre_race固定
- 無ければ従来どおり照会する。確定オッズが取れたらpost_race(共有ファイルには書かない)、取れなければpre_race確定として共有ファイルに追記する
運用上の位置づけも、改めて確認した。人気の基準時点はレース前日の正規出馬表公開直後(10:15〜12:00)で、正規の取得元はV5py。V58py・darkmatter版の取得能力は、V5が漏れたときの保険である。オッズは常に暫定値なので、最初に取った値をそのまま正とする。10/3の確認では、10/2(金)10:15〜11:53にV5pyが保存した23レースの人気と、10/3夜の比較検証で使われた人気が全レース一致していた。
実機確認:4パターンの相互共有
| パターン | 内容 | 結果 |
|---|---|---|
| ① V5py→V58py | V5が先にbuildした中山4Rを、V58でbuild。照会されず共有分が適用 | 合格 |
| ② V58py自己参照 | 同じレースをV58で2回。1回目は取得・保存、2回目は適用のみ | 合格 |
| ③ darkmatter版自己参照 | ②と同様 | 合格 |
| ④ darkmatter版→V5py | darkmatter版が先にbuildした中山7Rを、V5で適用 | 合格 |
この確認の中で、1回目と2回目の自動取得を比べると、ある馬の人気が12番人気から11番人気にずれていた。オッズが常に動く暫定値だという前提の裏付けであり、共有スナップショットなら1回目の値で固定されることも確認できた。
3. 比較検証py側の見落とし(9/27〜9/28)
4パターンを確認し終えたところで、Yujiから重要な指摘があった。
「現状で、比較検証pyを実行したらprerace_snapshot.csvここに入っていないpre_raceデータ(入っているのは動作確認のためにbuildしたデータ)計算だったらどうなる?」
調べると、darkmatter_hikaku.pyの load_prerace_snapshot() は新しい共有ファイルを一切見ておらず、旧来のV5単独CSVだけを参照していた。V5が漏れても他の2本が保険になるはずが、肝心の比較検証の入り口がその恩恵を受けていなかったのだ。
「いまやっている修正の根本は 比較検証xlsxが正常なデータ計算されクリーンな結果を得るためにしている。よってyosou_hikaku_v3.py darkmatter_hikaku.py この2本をそれに対応したものにしないと意味がない」
修正は「①共有ファイルを最優先で参照 → ②無ければ従来のV5 CSVにフォールバック」の2段構えにした。②を残したのは、9/15〜9/27に旧方式で蓄積済みのpre_race行を無駄にしないためである。同じ変更をyosou_hikaku_v3.pyにもクローンとして入れ、レース後のモードの開始直後にpre_raceデータの有無を確認するガードも付けた。
4. テストデータがNを汚す(9/28)
設計・実装・テストを繰り返す間に、動作確認用のbuildデータがCSVに混ざった。このままだとNが実態を反映しない見せかけの数字になる。そこで締切日 N_RESET_CUTOFF(2026-09-28)を3ファイルに同時導入し、前回記事の CLEAN_CUTOFF(8/5)との新しい方を実際の集計締切日にした。
- 9/27の中山8レースは、テストではなく当日朝の人気で固定された正規のpre_raceデータ。Yujiの判断は「レース前の時間差でありレース後ではないのでOK」で、Nに含める
- n_tracker.pyに書き込み先の限定(write_to)を追加。V58のマスターに紛れ込んでいた「Nトラッカー_DM」シートは、開くたびに自動掃除する
結果として、比較は「V5 vs V58」「V5 vs darkmatter」の2つ、マスターxlsxもそれぞれ自己完結の形に整理された。
5. 「確認したい結果が全部空」── 9/30の遠回り
9/30朝、Yujiは「今あるデータで2つの比較検証pyの確認ができれば時間を非常に有効に使える」と、csv内の8レースでの実機確認を申し出た。ここで私は、今日の確認実行をNに入れたくないはずだと思い込み、確認せずに締切日を10/2へ動かした。
結果は、V5が7980行→0行、darkmatterが832行→0行、V58が140行→0行。3つとも集計0件でNトラッカーの計算が失敗した。ところがログの最後には、固定文の「入っているはずです」が出ていた。さらに、集計0件でループが1周も回らず、直後で参照する変数 ri が未定義になるyosou_hikaku_v3.pyの潜在バグも顔を出した(締切日変更で初めて顕在化)。
Yujiの意図は逆だった。今日の8レースをNに入れて、マスターとNトラッカーに反映されるかを確認したかったのだ。
「普通、現状の作業結果を反映される確認をするのではないのか? Nをリセットするだけならこんな面倒くさいことしないだろ?」
「進め方のとして『どういう結果が欲しいか』これが基本中の基本だろ」
締切日は9/28に戻し、進め方を3点に改めた。
- 修正前に、欲しい結果を1行で書く
- 設定値や日付を決めたら、その結果で目的が確認できるかを実行前に検討する
- 目的の読み取りに自信がなければ、手段を決めずに先に聞く
この日の確認で出た不具合は、集計0件時のクラッシュ、終了メッセージの誤表示、差し替えpyの置き忘れ(No module named 'n_tracker' でNトラッカーが作られなかった)、そして次節の状態判定の誤りだった。
6. Nが0でも「確信段階」── 目標Nの計算を直す
同じxlsxで、V5のSHOW②はN=0、V58のSHOW①はN=1なのに、「目標N=0・✅確信段階」と表示されていた。当初は「Nが少ないうちはこの表示を信用しないで」と案内したが、Yujiの返答は明快だった。
「信用するしないの問題でない 修正しなければならない項目だろ」
原因は、実測的中率pが0%か100%のとき、目標Nの式の分散が0になって目標Nが0になり、n >= 目標N が常に真になる構造だった。少数Nでpが極端になりやすい条件ほど、「達成済み」と誤判定されていた。補正p(Agresti-Coull法)に切り替えて解消した(コードは9章)。N=0〜399の全パターン(約8万通り)で、状態と実際の誤差幅が矛盾しないことも確認した。
7. tekichuritu_master.xlsxの廃止と、V5のNが1少ない件
廃止:この日のはじめ、バックアップの意味を問うた私の説明は目的の取り違えだった。Yujiがバックアップを残す理由は「土日毎の比較検証レース結果を残して、先週と今週を比較するため」だ。バックアップは残す。一方、V5専用のtekichuritu_master.xlsxは、V5のシートがv58_hikaku_master.xlsxにも同内容で入っているため不要になり、生成・バックアップを止めた。参照している現役pyをgrepで洗い出し、書き込み2本・読み込みで壊れる1本・コメントのみ4本と影響範囲を確認してから、3本を修正した。
Nのずれ:10/3のdarkmatter比較(東京1R)の後、Nトラッカーの数字がV5=8、DM=9と1つずれた。原因は、V5のNを常にv58_hikaku_master.xlsxの「レース別推移_V5」から読んでいたことで、V5 vs V58を実行しない間は、darkmatterマスター側のV5のNが遅れる構造だった。ログの「データ元: yosou_hikaku_data.csv」は予想CSVの名前で、的中の読み込み元ではなかった(見たものだけを根拠にする、の再確認)。
修正は、V5のNを書き込み先の各マスター自身のシートから数えるようにするものだ(n_tracker.py v1.11)。10/4に提供したバックアップxlsx 6本で、各マスターともV5と相手のNが揃っていること、2つのマスターのV5のシートが54レース分すべて一致していることを確認した。
8. 「実行履歴」列:消える1回目の日付
同じレースを再実行すると、古い行が消えて新しい行が下に追加され、日付の列は最新の実行日に変わる。1回目の実行日が残らない。「日付だけ履歴として残せないか」というYujiの要望に対し、日付列そのものは変えない設計にした。N集計の判定がこの列を使っているため、初回日に固定するとNに入るレースが変わりうるからだ。代わりに、右端に「実行履歴」列を追加した。
同じ日に複数回実行した場合は「2026/10/5 1、2026/10/5 2」のように回数を付けて表示する(CSVには日付だけ保存するので、集計には影響しない)。darkmatter側(10/5 15:16)とV58側(15:24)の京都11R再実行で、どちらも二重カウントなしにN=54のまま動くことを実機で確認した。
9. 工夫した設計・誇れるコード
① 共有pre_raceスナップショット ──「最初に取った者が正」
説明用に簡略化したものを示す。
def resolve_prerace_ninki(race_id, entries):
shared = load_shared_prerace_snapshot(race_id) # prerace_snapshot.csv
if shared:
# 誰かが先に取っている → それを使う。result.htmlは見に行かない
apply_snapshot_ninki(entries, shared)
return 'pre_race'
# 自分が最初の1本
if maybe_update_ninki_from_result(entries): # 確定オッズが取れた=レース後
return 'post_race' # 共有ファイルには書かない
save_shared_prerace_snapshot(race_id, entries, source=MY_NAME)
return 'pre_race'
V5・V58・darkmatter版のどれが先でも同じ挙動になるので、供給元の優先順位を持たずに済む。呼び出し側が配線する方式から、本体が自律的に持つ方式へ移したことで、壊れにくくなった。
② 補正pによる目標N ──「0%でも100%でも壊れない」
import math
Z = 1.96
def target_n(hits, n, err):
# 実測pに仮想試行(的中 Z²/2、試行 Z²)を加えた補正p(Agresti-Coull法)
p = (hits + Z**2 / 2) / (n + Z**2)
return math.ceil(Z**2 * p * (1 - p) / err**2)
補正pは0にも1にもならず(N=0なら50%)、目標Nが0になることがない。Nが増えるほど実測pに近づく。例えばクリーン全体のN=8・的中3、誤差±10%では、目標Nが91から94になった。
③ 締切日は集計時だけ ──「元データには触らない」
CLEAN_CUTOFF = '2026-08-05' # 列ズレバグ対策
N_RESET_CUTOFF = '2026-09-28' # テストデータ混入対策
cutoff = max(CLEAN_CUTOFF, N_RESET_CUTOFF)
df = df[df['日付'] >= cutoff] # 絞るのは集計用のdfだけ。CSVは書き換えない
3ファイル(n_tracker・yosou_hikaku_v3・darkmatter_hikaku)で同じ書き方に揃えた。「基本システム内は同じ仕様にする」という方針の実践でもある。
10. 得られた教訓
- 引き継ぎ資料は最後まで読み切る ── セッション上限で引き継いだ際、Chatログの後半を読み落とし、実機確認まで済んでいた実装を未着手と誤認しかけた
- 設計の言い換えは、自分の理解に寄せず、そのまま受け取る ── 「V5が唯一の供給元」案は、言い換え一つで別の設計だと分かった
- 「目的」に立ち返って通しで確認する ── 本体3本を直しても、使う側の入り口(比較検証py)が新しい共有ファイルを見ていなかった
- 設計変更やテストの副産物が、別の指標(N)を汚す ── 作業そのものが既存の集計に影響しないか、都度確かめる
- 「どういう結果が欲しいか」を先に決める ── 手段(日付)を先に決めて、確認したい結果が全部空になった
- バナー・冒頭コメントの更新は基本ルール ── 言われなくても必ず更新し、更新しない場合に限って理由を書く
- 差し替えたpyは、置いたあとにバナーの番号で確認する ── 置き忘れでNトラッカーが作られなかった
- おかしな表示は利用者に回避させず、直す ── 「信用しないで」はバグへの回答にならない
- 見ていないものは断定しない ── ログが途中なのに「動いていない」と決めた、保存先をコードの指定と推測した、といった外れが何度かあった
未解決・要確認事項
- V5pyのbuildが実際に漏れた日の実地確認(V58py・darkmatter版が保険として機能するか)。現状は「正規出馬表でV5pyがbuildされていないレースは無い」ため、来週末以降にV5をbuildし、レース終了後の比較検証を行う以外に確認の方法が無い
- ◎が取消・除外・中止のレースの実機確認と、V5 vs V58側での表示確認。10/5に東京5Rで予想6位の馬が除外になったレースは通ったが、◎ではなかったため、◎除外時のNからの除外はまだ試されていない
おわりに
前回は、pre_raceを唯一の正基準とするところまでが範囲だった。今回の14日間は、その基準を「誰がbuildしても、いつ実行しても同じ値になる」仕組みに仕上げる作業だった。N=54まで積み上がった今も、複勝の的中数はV5が26、V58が27、darkmatter版が26で、補正の良否を語れる段階ではない。物差し(Nとpre_race)を先に正しくしておいたのは、そのためである。
「失敗を消さない」という方針は今回も変わらない。修正履歴の番号は、また増えた。次の宿題は、来週末のV5pyのbuildと、◎が除外されたレースである。