JvLink To Importer

単勝確率から馬連・三連複の確率を計算する — Harville の式と Henery 補正

単勝確率から馬連・三連複の確率を計算する — Harville の式と Henery 補正

予測モデルやオッズから各馬の単勝確率 p_i が手に入ったとき、「この 2 頭の馬連が当たる確率」「この 3 頭の三連複が当たる確率」はどう計算すればよいでしょうか。この記事では、最も基本的な Harville の式(逐次抽出モデル)と、その既知の欠点を補う Henery/Stern 補正を、numpy 実装と検算テストまで含めて紹介します。


問題設定: 単勝確率しか手元にない

連系馬券の的中確率を直接予測するモデルを作るのは大変です。組み合わせの数が爆発する(18 頭立ての三連単は 4,896 通り)うえ、1 点 1 点の的中サンプルが少なすぎて学習が安定しません。

そこで実務では、単勝確率 p_i から連系の確率を解析的に組み立てるのが定石です。単勝確率は

  • 予測モデルの出力(各馬の勝率)
  • オッズから逆算した市場の確率

のどちらでも構いません。必要なのは「全馬の p_i の和が 1 になっている」ことだけです。


Harville の式: 逐次抽出モデル

Harville の式は「1 着を確率 p で抽選し、当選馬を除いた残りで同じ比率の抽選を繰り返す」というモデルです。馬 i が 1 着、馬 j が 2 着になる確率は

P(i→j) = p_i × p_j / (1 − p_i)

「i が勝つ確率」×「i を除いた残り全体の中で j が一番強い確率」という意味です。3 着まで拡張すると

P(i→j→k) = p_i × p_j / (1 − p_i) × p_k / (1 − p_i − p_j)

これが三連単 1 点の確率です。順序を問わない馬券は順列を足し合わせます。

  • 馬連 (i, j) = P(i→j) + P(j→i) の 2 通りの和
  • 三連複 {i, j, k} = 6 通りの順列(三連単 6 点)の和

numpy で実装する

import numpy as np
from itertools import permutations

def trifecta_harville(p, i, j, k):
"""三連単 i→j→k の確率 (Harville)"""
return p[i] * p[j] / (1 - p[i]) * p[k] / (1 - p[i] - p[j])

def umaren_harville(p, i, j):
"""馬連 {i, j} の確率 = 2 順列の和"""
return p[i] * p[j] / (1 - p[i]) + p[j] * p[i] / (1 - p[j])

def sanrenpuku_harville(p, i, j, k):
"""三連複 {i, j, k} の確率 = 6 順列の和"""
return sum(trifecta_harville(p, a, b, c)
for a, b, c in permutations((i, j, k)))

p = np.array([0.30, 0.20, 0.15, 0.12, 0.10, 0.08, 0.05])
print(umaren_harville(p, 0, 1))        # 1-2 番手の馬連
print(sanrenpuku_harville(p, 0, 1, 2)) # 上位 3 頭の三連複

p は和が 1 に正規化済みであることが前提です。レース内の全馬分をまとめて計算するときも、この関数を組み合わせ分ループするだけで足ります。


Harville の既知の欠点

Harville の式はシンプルで便利ですが、実データと突き合わせると強い馬が 2 着・3 着に「来すぎる」予測になることが昔から知られています。

逐次抽出モデルは「1 着馬を取り除いたあと、残りの馬の力関係は単勝確率の比率のまま」と仮定しています。しかし実際のレースでは、勝ち切る力と 2〜3 着に粘る力は同じではありません。展開や位置取りの影響で、2 着・3 着の決まり方は単勝確率が示すほど鋭くない(=もっと混戦的)のです。

結果として、Harville のままだと本命馬を含む連系の確率を過大に、人気薄が絡む組み合わせを過小に見積もる傾向が出ます。


Henery/Stern 補正: 指数 τ で混戦化させる

この欠点への実務的な対処が、条件付き確率の指数を τ < 1 で割り引く方法です(Henery や Stern のモデルに由来する近似としてよく使われます)。2 着の条件付き確率を

P(j 2着 | i 1着) = p_j^τ / Σ_{m≠i} p_m^τ

に置き換えます。τ = 1 なら Harville に一致し、τ を小さくするほど分布が平らになります。直感的には「先頭が抜けたあとの残りは、単勝確率が示すよりも混戦化する」という観察を τ ひとつで表現したものです。

def trifecta_tau(p, i, j, k, tau=0.8):
"""指数 τ で 2着以降を割り引いた三連単確率 (τ=1 で Harville)"""
pt = p ** tau
p1 = p[i]                                # 1 着は素の単勝確率
p2 = pt[j] / (pt.sum() - pt[i])          # 2 着は τ 乗の世界で抽選
p3 = pt[k] / (pt.sum() - pt[i] - pt[j])  # 3 着も同様
return p1 * p2 * p3

2 着と 3 着で別々の τ(3 着側をさらに小さく)を使う流儀もあります。τ の値は、過去レースで「Harville/補正後の予測確率」と「実際の 2 着・3 着率」を突き合わせて決めます。0.7〜0.9 あたりから試すのが目安です。


検算用の不変量をテストに書く

この種の確率計算は添字のバグが混入しやすいので、数学的に必ず成り立つ不変量をテストにしておく習慣をおすすめします。

from itertools import combinations, permutations

def test_invariants():
rng = np.random.default_rng(0)
p = rng.dirichlet(np.ones(12))   # 和が 1 のランダムな単勝確率
n = len(p)

# (1) 全三連単の和 = 1
total = sum(trifecta_tau(p, i, j, k, tau=0.8)
for i, j, k in permutations(range(n), 3))
assert abs(total - 1.0) < 1e-9

# (2) 全馬連の和 = 1
total_umaren = sum(umaren_harville(p, i, j)
for i, j in combinations(range(n), 2))
assert abs(total_umaren - 1.0) < 1e-9

# (3) 三連複 = 対応する三連単 6 通りの和 (実装を分けた場合の整合性)
combo = sanrenpuku_harville(p, 0, 1, 2)
six = sum(trifecta_harville(p, a, b, c)
for a, b, c in permutations((0, 1, 2)))
assert abs(combo - six) < 1e-12

(1) と (2) は正規化のバグ、(3) は三連複と三連単で実装を分けたときの食い違いを検出します。τ を変えても (1) が成り立つことを確認しておくと、補正版の実装ミスにもすぐ気付けます。


オッズから p_i を作るときの注意

市場の確率を入力にする場合、1/オッズの和は控除率のぶん 1 を超えています。そのまま Harville に入れると全体が水増しされるので、和で割って正規化(比例配分の de-vig)してから使います。

inv = 1.0 / odds          # オッズは控除率込みなので和が 1 を超える
p = inv / inv.sum()       # 和を 1 に正規化してから Harville に渡す

mykeibadb 系のスキーマでは tansho_odds が 10 倍値で格納されている点にも注意してください(5.2 倍なら 52)。


まとめ

単勝確率さえあれば、Harville の式で馬連・三連複・三連単の確率を解析的に組み立てられます。ただし Harville は強い馬が 2〜3 着に来すぎる方向に歪むので、指数 τ による Henery/Stern 補正で混戦化させるのが実務的です。全順列の和が 1 になるなどの不変量テストを書いておけば、添字バグにも安心して立ち向かえます。

この記事をシェア

Post