ミキサーに3つの構造バグが潜んでいた話 〜発見・修正・函館係数変更の結果まで【競馬予想システム開発記 V6.4】
予想屋yuji ― 71歳・隠居・Ubuntu・Python独学。競馬予想AIを半年間開発中。
前回記事:競馬AIの係数を0.700から0.900に変えたら、的中率が7.6pt上がった話
はじめに
「函館のランクA係数を1.000から1.300に変えてみよう」
きっかけはその一言だった。ところがミキサー(hosei_mixer_v1.py)を操作し始めた瞬間、
3つの構造バグが連鎖的に発覚した。
6/12〜16の5日間、予定していた函館係数変更の前に、まずミキサーそのものの修理に追われた。
そしてその修理を終えて係数を変更し、結果まで確認できた。
一気に報告する。
今週の変更サマリー
| No. | 日付 | 内容 | 分類 |
|---|---|---|---|
| No.53 | 06/14 | [a]/[s]/[m]を確定項目のみに制限・[t]新設 | 構造修正(問題A) |
| No.54 | 06/15 |
_VK_PROPOSED_RANK_FACTORS廃止→動的計算に統一 |
構造修正(問題C) |
| No.55 | 06/15 | 会場効果%:ランク効果% の予算配分ロジック実装 | 新機能(問題B試算) |
| No.56 | 06/15 | バナーNo重複修正 | 修正 |
| No.57 | 06/17 |
_is_kakutei()のバグ修正([t]にダークマター混入) |
BUGfix |
| V58 No.49/50 | 06/15 | yosou_hikaku_v3.py・seiki_v5.pyへの横展開 | BUGfix横展開 |
| V5 No.72 | 06/16 | V58py No.41/42の除外馬処理をV5pyに横展開 | BUGfix横展開 |
| CSV変更 | 06/16 | 函館ランクA:1.000 → 1.300 | 係数変更 |
問題A:「推奨値は提案値であって確定値ではない。それを設計段階で入れるとはどういう事だ」
発覚の経緯
函館ランクAだけ変更しようと[m](競馬場を指定して個別選択)を実行した。
すると函館のランクAだけでなく、ダークマター・ランクCも一緒にpendingに入っていた。
函館 ダークマター: 1.000 → 0.766(差-0.234) ← 頼んでいない
函館 ランクA: 1.000 → 0.937(差-0.363) ← これだけ変えたかった
函館 ランクB: 1.000 → 1.000(差 ─ )
函館 ランクC: 1.000 → 1.000(差 ─ ) ← 頼んでいない
yujiの一言が核心を突いた。
「推奨値は提案値であって確定値ではない。それを設計段階で入れるとはどういう事だ」
原因
[3]の集計結果では確定(N≥10)と暫定(N<10)を表示上は区別している。
しかし[m]の保存処理は、その区別を一切参照しなかった。
会場全体を一括でpendingに入れるという設計が、属性別に信頼度が違うという現実と噛み合っていなかった。
修正(No.53)
# 修正前:全属性を一括pending
for key, p in proposals.items():
if venue == v_in:
pending[key] = p['factor']
# 修正後:確定項目(tentative=='確定')のみ
def _is_kakutei(p):
t = p.get('tentative', '')
return t == '確定' or t == '-' # ダークマターは'-'→確定扱い
for key, p in proposals.items():
if venue == v_in:
if _is_kakutei(p):
pending[key] = p['factor']
else:
skipped_tentative.append((key, p)) # [t]で個別確認へ
-
[a]/[s]/[m]:確定項目(N≥10)のみ自動でpendingに追加 -
[t](新設):暫定項目(N<10)を1件ずつy/Nで個別確認・反映
問題C:「.buildozerの残骸がいつまでも残っているような感じ」——ハードコードの罠
発覚の経緯
[4]検証モードで全件実行したところ、tekichuritu_master.xlsxの傾向と全く矛盾する「全会場で軒並み悪化」という結果が出た。
調査の結果、原因が判明した。
[4]検証モードが_VK_PROPOSED_RANK_FACTORSというハードコードされた5月時点の推奨値を使い続けていた。
# 問題のあったコード(No.28実装時にコピペして固定)
_VK_PROPOSED_RANK_FACTORS = {
'東京': {'A': 0.900, 'B': 1.000, 'C': 0.815, 'S': 1.028},
'阪神': {'A': 1.300, 'B': 1.111, 'C': 1.300, 'S': 1.246},
# ... 5月時点のスナップショットがそのまま残っていた
}
[3]は毎回最新データから動的に推奨値を計算していたのに、[4]だけが5月の古い値を抱え込んだまま動いていた。
yujiがAPKビルドの経験から即座に本質を掴んだ。
「ドットビルドザーの残骸がいつまでも残っているような感じだね」
.buildozerのキャッシュが、ソースを更新しても古いビルド結果を抱え込み続ける——全く同じ構造だった。
修正(No.54)
# 修正後:その場で動的に計算
def show_venue_proposals_v2(data, proposals=None):
"""[4]検証モード:毎回最新データから推奨値を動的算出"""
if proposals is None:
dm_data = auto_scan_venue(input_dir) # 最新データをスキャン
proposals = calc_venue_proposals(dm_data, data) # 動的に計算
# ハードコード辞書は完全廃止
_VK_PROPOSED_RANK_FACTORSを廃止し、[3]と[4]が常に同じ最新データから動くようにした。
さらに[4]はランク係数だけでなくダークマターも含む合成効果を検証できる設計に拡張した。
問題B:妥協を拒否——「後手のやり方では絶対抜ける」
問題AとCを修正した後、Claudeは提案した。
「問題Bは[4]検証で事前確認する運用でカバーできる」
yujiは即座に退けた。
「問題B(ダークマターとランク係数の独立算出)について、運用上は『適用前に検証する』という形でカバーできる状態では絶対抜ける。こういう後手のやり方では。計算段階での相互作用は解決しなければならない」
yujiの着想:「100を分配する」
「ダークマター+ランク係数を100として一つの大きいくくりで考え、100の各々の割合を振る。補正値によって与える影響を振るという考えはどうか?」
この発想は統計学の分散分析(ANOVA)の寄与度分解と全く同じ構造だった。
半導体製造での品質管理経験が、独立した発想として同じ地点に到達していた。
実装(No.55):会場個性配分ロジック
def calc_venue_proposals_v2(dm_data, rank_data, current_data):
"""
会場効果%:ランク効果% で予算配分した推奨値を算出
会場効果% = |DM-1.0| / (|DM-1.0| + |RANK-1.0|) × 100
ランク効果% = 100 - 会場効果%
"""
results = {}
for venue in VENUES:
dm_dev = abs(dm_data[venue] - 1.0)
rank_dev = sum(abs(v - 1.0) for v in rank_data[venue].values()) / 4
total = dm_dev + rank_dev
if total < 0.001:
venue_pct = 50.0
else:
venue_pct = dm_dev / total * 100
rank_pct = 100 - venue_pct
results[venue] = {'venue_pct': venue_pct, 'rank_pct': rank_pct}
return results
1014レースで試算した結果:
| 会場 | 会場効果% | ランク効果% | 傾向 |
|---|---|---|---|
| 東京 | 3.5% | 96.5% | ランク効果が支配的 |
| 福島 | 1.6% | 98.4% | ランク効果が支配的 |
| 函館 | 49.8% | 50.2% | ほぼ均等 |
| 阪神 | 59.8% | 40.2% | 会場効果優位 |
| 新潟 | 64.9% | 35.1% | 会場効果優位 |
直感と一致する。東京はジョッキーの腕が出やすく、函館・阪神は馬場・コース特性が強い。
ただし本採用は[4]検証で精度確認後。 現時点は試算段階。
[t]キーのバグ発覚——レポート作成中に見つけた
V6.4レポートの作成中、[t]の動作確認を改めて実施した。
📋 暫定項目(N<10)一覧(17件):
中山 ダークマター: N=179 現在1.271 → 推奨1.078 ← N=179なのに暫定!?
阪神 ダークマター: N=228 現在1.294 → 推奨1.161 ← N=228なのに暫定!?
東京 ダークマター: N=189 現在1.025 → 推奨1.011 ← おかしい
...
「N<10の真の暫定項目のみ」を表示すべき[t]に、ダークマター(N=24〜228)が大量に混入していた。
原因と修正(No.57)
# 修正前:'確定'かどうかしか見ていない
def _is_kakutei(p):
return p.get('tentative') == '確定'
# → ダークマターのtentativeは'-'(ランク係数なし)
# '確定'でもないので is_kakutei=False → [t]に混入
# 修正後:'-'も確定扱いに
def _is_kakutei(p):
t = p.get('tentative', '')
return t == '確定' or t == '-'
sed1行で修正し、バナーをNo.57に更新した。
「レポートは一発で仕上がらない、確認作業の中で問題が出る」——今回もその通りだった。
周辺py横展開——V5py No.72 除外馬修正
函館24Rの比較検証xlsxを確認したところ、V5py側でテリオスルミが9999番人気として計算され、指数1111.32で◎を占有していた。
V58pyでは既にNo.41/42(2026-06-04)で除外処理済みだった。
# V58py No.41/42で実装済み(V5pyには未反映だった)
_excluded_ids = extracted.get('excluded_ids', set())
if _excluded_ids:
extracted['entries'] = [e for e in extracted['entries']
if e['id'] not in _excluded_ids]
yujiのコメント:
「9999番人気問題はV58pyで修正している。その時横展開でV5pyもしていると記憶していたが」
確認するとV5pyには横展開ゼロだった。V5py No.72として即座に移植した。
函館ランクA係数変更の結果
全修正完了後、本題の函館ランクA係数変更を実施した。
変更内容: venue_correction.csv 函館ランクA:1.000 → 1.300
変更前(11:27スナップショット)と変更後(13:24スナップショット)で函館24レースを比較。
函館24レース比較
| 指標 | 変更前 V5 | 変更前 V58 | 変更後 V5 | 変更後 V58 | V58の変化 |
|---|---|---|---|---|---|
| 複勝的中率 | 29.2% | 33.3% | 41.7% | 41.7% | +8.4pt ✅ |
| ◎平均誤差 | 4.00 | 3.83 | 3.38 | 4.21 | +0.38(微増) |
⚠️ 「V5側の+12.5pt改善」の正体
V5の複勝的中率も29.2%→41.7%(+12.5pt)と大きく改善しているが、
これはvenue_correction係数変更とは一切無関係だ。
V5pyはvenue_correction.csvを参照しない。改善理由はV5py No.72の除外馬修正だ。
2つの別々の改善が同じ日に偶然起きた。
V5とV58を別々のベンチマーク軸として分離していなければ、混同していたところだった。
「何が何を改善したか」を系統別に厳密に追跡する。V5/V58の2系統分離運用の真価がここで発揮された。
まとめ
「函館の係数を動かす」という小さな動機から、ミキサーに潜んでいた3つの構造問題が連鎖的に発覚した5日間だった。
| 問題 | No. | ステータス |
|---|---|---|
| A:[m]が暫定も含めて一括pending | No.53 | ✅ 解決済み |
| C:[4]がハードコード推奨値を参照 | No.54 | ✅ 解決済み |
| B:ダークマターとランク係数の配分 | No.55 | 🔄 試算実装済み・[4]検証待ち |
| [t]にダークマター混入 | No.57 | ✅ 解決済み(レポート作成中に発見) |
急いで全会場に配分ロジックを適用せず、1会場・1係数の単点変更として検証結果を確認する。
このペースが、6ヶ月を超えた長期開発を支えている。
今週の気づき
「計算そのものは君や他AIにやって貰うしかないが、アルゴリズムだけはしっかり理解しておかないとね」
71歳の隠居ジジィがAIと組んで一からシステムを構築し続ける原動力は、
ブラックボックスにしないことへのこだわりにある。
次回V6.6では、No.55の配分ロジックの[4]検証と、函館24RのRゾーン別詳細分析を中心に進める。
制作者:yuji(@yujiiwadate0247)― 予想屋yuji 競馬予想システム V6.4期間 2026/06