能力値と適性 — 戦績を 1 つの数字にする
名鑑の 5,861 頭につける「能力値 (45〜99)」の作り方を説明します。平方根と対数を使う理由、順位 (パーセンタイル) への変換、そして 3D シミュレーションで使う「スピード」への換算まで。
- 勝率・GI 勝利数・重賞勝利数・獲得賞金から「素点」を作る式
- 勝率に平方根、賞金に対数を使う理由 (限界効用の考え方)
- 素点を順位に変えてから 45〜99 に配る理由と、その計算
- 「能力値 99 は何頭いるか」を式だけで当てる
- 得意な馬場・距離の推定と、シミュレーション用のスピードへの換算
素点 — 4 つの実績を足す
名鑑の馬には、Wikipedia の記事から取った「通算成績 (N 戦 M 勝)」「勝ち鞍 (GI・GII・GIII)」「獲得賞金」があります。これらから 素点 を作ります。
private fun rawScore(h: HorseEntry): Double {
val starts = h.starts ?: 0
val winRate = if (starts > 0) (h.wins ?: 0).toDouble() / starts else 0.25
val top = h.bigWins.count { it.grade.endsWith("I") && !it.grade.endsWith("II") }
val graded = h.bigWins.size
val money = h.earningsManYen?.let { min(log10(1.0 + it) * 3.0, 15.0) } ?: 6.0
return 18 * sqrt(winRate) + min(top, 9) * 3.0 + min(graded, 15) * 0.6 + money
}| 記号 | 意味 | 不明なとき | 最大の寄与 |
|---|---|---|---|
| 勝率 = 勝ち数 / 出走数 | 0.25 | 18 | |
| GI (JpnI) の勝利数 | 0 | 27 (9 勝で頭打ち) | |
| 重賞 (GI〜GIII) の勝利数 | 0 | 9 (15 勝で頭打ち) | |
| 獲得賞金 (万円) | 項全体を 6 | 15 |
grade.endsWith("I") && !grade.endsWith("II") は「GI または JpnI」だけを数えるための条件です ("GIII" や "GII" も "I" で終わるため、"II" で終わるものを除いています)。
例: オグリキャップ
32 戦 22 勝、データ上の GI 勝ち 3、重賞勝ち 10、獲得賞金 9 億 1251 万円 (91,251 万円)。
なぜ平方根と対数なのか — 限界効用
勝率に平方根
のグラフは、最初は急で、だんだん緩やかになる曲線です (凹関数)。傾き (微分) は
で、 が小さいほど大きくなります。
| 勝率の変化 | 素点の増え方 |
|---|---|
| 0.1 → 0.2 | |
| 0.8 → 0.9 |
「ほとんど勝てない馬」と「たまに勝つ馬」の差は大きいが、「よく勝つ馬」と「とてもよく勝つ馬」の差はそれほどでもない、という感覚を表しています。経済学で「限界効用の逓減」と呼ぶ考え方と同じです。
賞金に対数
賞金は 100 万円の馬から 20 億円の馬まで、桁が 4 つ以上違います。そのまま足すと賞金の大きい馬だけで点数が決まってしまうので、 (10 を何乗すればその数になるか) で「桁数」に直します。
| 獲得賞金 | |
|---|---|
| 100 万円 () | 6.0 |
| 1,000 万円 | 9.0 |
| 1 億円 | 12.0 |
| 10 億円 | 15.0 (上限) |
賞金が 10 倍になると +3 点。 としているのは、賞金 0 のとき (マイナス無限大) にならないようにするためです。人間の感覚が刺激の「比」に反応する、という Weber–Fechner の法則とも似た発想です。
GI 9 勝、重賞 15 勝、賞金 10 億円で頭打ちにしているのは、極端な 1 頭が他の要素を無視して抜け出すのを防ぐためです。外れ値の影響を抑えるこの操作を、統計では ウィンソライズ (winsorize) と呼びます。
素点 → 順位 → 能力値
素点は 5〜50 くらいの、分布の偏った数です。そのまま見せると「上位の名馬がみな同じような点」になり、差が分かりません。そこで 全頭の中の順位 に変換してから、45〜99 に配ります。
val raw = list.map { rawScore(it.horse) }
val order = raw.indices.sortedBy { raw[it] }
order.forEachIndexed { rank, i ->
val p = if (list.size == 1) 1.0 else rank.toDouble() / (list.size - 1)
rating[i] = (45 + 54 * p * p).roundToInt()
}素点の小さい順に 0, 1, …, と順位 をつけ、
は「自分より下に何割の馬がいるか」(パーセンタイル) です。最下位で → 45、最上位で → 99 になります。
なぜ なのか
(直線) でも 45〜99 になります。2 乗にすると傾きが になり、上位ほど 1 段階の差が大きく 表示されます。名鑑を見る人が気にするのは「上位の名馬どうしの差」なので、上の方の解像度を上げているわけです。
能力値 99 は何頭いるか — 式だけで予言する
四捨五入で 99 になるのは のときです。
上位 です。名鑑は 頭なので、、つまり 〜 の 28 頭。実際のデータを数えると 28 頭で、一致します。
同じように中央の馬 () は で、実データの中央値も 59 です。
さらに深く能力値の分布
は 0〜1 にほぼ一様に散らばるので、「能力値が 以下の馬の割合」は
です。たとえば能力値 70 以下は 。半分以上の馬が 60 未満に集まり、上に行くほど少なくなる、「上位が細長い」分布になります。
Wikipedia に記事がある時点で、その馬はかなり活躍した馬です。能力値 45 は「弱い馬」ではなく「記事のある 5,861 頭の中で実績が最も少ない馬」です。また、海外の馬は賞金が円ではないので賞金点が仮の 6 になり、時代による賞金水準の違いも考慮していません。
得意な馬場と距離の推定
その馬が勝った重賞を、競走一覧のデータ (馬場・距離) と名前で突き合わせます。
- 勝った重賞に芝が含まれれば「芝」、ダートが含まれれば「ダート」、両方なら「芝・ダート」、記事のある重賞を勝っていなければ「不明」
- 得意距離 = 勝った重賞の距離の 平均 (オグリキャップは 1,980m)
平均を使うので、マイル (1,600m) と長距離 (2,500m) の両方を勝った馬は 2,000m 前後が「得意」と表示される、という限界があります。
シミュレーション用の「スピード」への換算
3D シミュレーションでは、能力値を モンテカルロ の (スピード指数) に換算して Zig に渡します。
function effectiveSpeed(f, distance, surface) {
if (f.speed) return f.speed;
let s = 70 + f.rating * 0.5;
if (f.surface && f.surface !== "不明" && !f.surface.includes(surface)) s -= 4;
if (f.bestDistance) s -= Math.min(6, Math.abs(distance - f.bestDistance) / 250);
return s;
}- 能力値 45〜99 → 92.5〜119.5。サンプル出馬表のスピード指数 (101〜118) とだいたい同じ範囲になるように選んだ 1 次式です
- は「条件が成り立てば 1、そうでなければ 0」(アイバーソンの記法)
- は得意距離。250m 離れるごとに −1、最大 −6
能力値 10 の差 = SI 5 の差 で、モンテカルロ の揺らぎの標準偏差 0.0635 は SI に直すと約 6.35 なので、「能力値 13 くらいの差が、揺らぎ 1 つ分」という感覚です。
- 素点 =
- 平方根と対数は「増えるほど 1 単位の価値が下がる」(限界効用の逓減) を表す
- 素点を順位 に直し、 で 45〜99 に配る。上位ほど差が大きく見える
- 99 になるのは上位 0.464% で、5,861 頭中 28 頭 (実データと一致)
- 3D では 能力値 − 適性の減点 で Zig に渡す
20 戦 8 勝、GI 1 勝、重賞 4 勝、獲得賞金 3 億円の馬の素点を求めてください ()。
答えを見る
、、、。合計 。
すべての馬の が一様に散らばっているとして、能力値 (四捨五入) が 80 以上になるのは全体の何% ですか。
答えを見る
より 。上位 約 20.1% です。
得意距離 1,600m、能力値 90 の芝馬が、芝 2,400m を走るときの SI はいくつですか。
答えを見る
。距離の減点は 。馬場は得意なので減点なし。。