大リーグポストシーズン組み合わせの謎をプログラムを走らせて分析する
大リーグでポストシーズンが始まりました。この仕組みがなかなか覚えられませんが、よく見てみると次のようになっています。

これは片方のリーグの頂点を決める組み合わせです。
なぜ3位から6位の配置はこのやり方なのか疑問に感じたのでプログラムを走らせて検証します。
検証する組み合わせ
1と2を固定すると組み合わせは以下の6つになります。
- [1, [3, 4]] [2, [5, 6]]
- [1, [3, 5]] [2, [4, 6]]
- [1, [3, 6]] [2, [4, 5]]
- [1, [4, 5]] [2, [3, 6]]
- [1, [4, 6]] [2, [3, 5]]
- [1, [5, 6]] [2, [3, 4]]
やり方
各チームは勝率というものもつ。強さを表すものはこの値のみ。
勝率の順が6チームのポストシーズン開始前の順位となる(実際はもっと複雑だが検討を単純にする)。
勝率がaのチームAと勝率がbのチームBが対戦したとき
Aが勝つ確率は a * (1 - b) / (a * (1 - b) + b * (1 - a))
Bが勝つ確率は b * (1 - a) / (a * (1 - b) + b * (1 - a))
とする。

この計算は直感的には、AとBの四角内で0から1の範囲でランダムに値を選び、Aの左の範囲とBの右の範囲か、Bの左の範囲とAの右の範囲が選ばれるとき、どちらの勝ちが選ばれるかというもの。
下の方に具体的な値にしたときの表を追加している。
で、検証のやり方は
- 0.5から0.6のあたりでランダムに6つの値を取得して、それを6チームの勝率とする。
- 6チームの組み合わせ方を決める
- それぞれのチームの優勝する確率を求める。
- すべての組み合わせで2と3を行う。
- 1から4を勝率を変えながら100パターン試して最後に「組み合わせごとの平均」を取る。
イメージ図
| 6チームの勝率 勝率をランダムに設定して 100個やる |
[1, [3, 4]], [2, [5, 6]] と組み合わせたときの優勝確率 |
[1, [3, 5]], [2, [4, 6]] と組み合わせたときの優勝確率 |
... |
|---|---|---|---|
| 1位 0.aaa 2位 0.bbb 3位 0.ccc ... |
1位 0.ddd 2位 0.eee 3位 0.fff ... |
1位 0.ggg 2位 0.hhh 3位 0.iii ... |
|
| 1位 0.jjj 2位 0.kkk 3位 0.fff ... |
1位 0.lll 2位 0.mmm 3位 0.nnn ... |
1位 0.ooo 2位 0.ppp 3位 0.qqq ... |
|
| ... | ... | ... | |
| 上の100個の平均 1位 0.rrr 2位 0.sss 3位 0.ttt ... |
上の100個の平均 1位 0.uuu 2位 0.vvv 3位 0.www ... |
結果
組み合わせと優勝する確率と差
単位は%
| [1, [3, 4]], [2, [5, 6]] |
[1, [3, 5]], [2, [4, 6]] |
[1, [3, 6]], [2, [4, 5]] |
★採用されている [1, [4, 5]], [2, [3, 6]] |
[1, [4, 6]], [2, [3, 5]] |
[1, [5, 6]], [2, [3, 4]] |
|
|---|---|---|---|---|---|---|
| 1位 | 25.741 | 25.775 | 25.809 | 25.812 | 25.847 | 25.884 |
| 差 | 0.225 | 0.295 | 0.364 | 0.371 | 0.440 | 0.510 |
| 2位 | 25.516 | 25.480 | 25.445 | 25.441 | 25.407 | 25.374 |
| 差 | 13.036 | 12.949 | 12.862 | 12.823 | 12.841 | 12.859 |
| 3位 | 12.480 | 12.531 | 12.583 | 12.618 | 12.566 | 12.515 |
| 差 | 0.286 | 0.148 | 0.251 | 0.321 | 0.218 | 0.287 |
| 4位 | 12.194 | 12.383 | 12.332 | 12.297 | 12.348 | 12.228 |
| 差 | 0.027 | 0.404 | 0.266 | 0.265 | 0.334 | 0.097 |
| 5位 | 12.167 | 11.980 | 12.066 | 12.032 | 12.014 | 12.132 |
| 差 | 0.265 | 0.127 | 0.300 | 0.232 | 0.196 | 0.265 |
| 6位 | 11.901 | 11.852 | 11.766 | 11.800 | 11.818 | 11.867 |
考察
2位と3位の差が大きいのは仕方がない。
[1, [3, 4]][2, [5, 6]]
4位と5位の差が極端に小さい(4位に比べて5位があまり悪くない)。
[1, [3, 5]][2, [4, 6]]
3位と4位の差が小さい(3位に比べて4位があまり悪くない)。
5位と6位の差が小さい(5位に比べて6位があまり悪くない)。
[1, [3, 6]][2, [4, 5]]
採用されていないものの中で一番バランスがいい。
しかし、採用されているものと比べると3位と4位の差が小さい(3位に比べて4位があまり悪くない)。
[1, [4, 5]][2, [3, 6]]
使用されている組み合わせ。差が上から綺麗に少なくなっている(2位と3位の差は省く)。
[1, [4, 6]][2, [3, 5]]
3位と4位の差が小さい(3位に比べて4位があまり悪くない)。
5位と6位の差が小さい(5位に比べて6位があまり悪くない)。
[1, [5, 6]][2, [3, 4]]
4位と5位の差が極端に小さい(4位に比べて5位があまり悪くない)。
対戦した2チームの勝敗
勝率がaのチームAと、勝率がbのチームBとが対戦したとき、勝敗の確率
| 0.1 | 0.3 | 0.5 | 0.7 | 0.9 | |
|---|---|---|---|---|---|
| 0.1 | 0.500 | 0.794 | 0.900 | 0.955 | 0.988 |
| 0.3 | 0.500 | 0.700 | 0.845 | 0.955 | |
| 0.5 | 0.500 | 0.700 | 0.900 | ||
| 0.7 | 0.500 | 0.794 | |||
| 0.9 | 0.500 |
| 0.4 | 0.45 | 0.5 | 0.55 | 0.6 | |
|---|---|---|---|---|---|
| 0.4 | 0.500 | 0.551 | 0.600 | 0.647 | 0.692 |
| 0.45 | 0.500 | 0.550 | 0.599 | 0.647 | |
| 0.5 | 0.500 | 0.550 | 0.600 | ||
| 0.55 | 0.500 | 0.551 | |||
| 0.6 | 0.500 |
Aが勝つ確率は a * (1 - b) / (a * (1 - b) + b * (1 - a))
Bが勝つ確率は b * (1 - a) / (a * (1 - b) + b * (1 - a))
とする。
コード Swift
import Foundation
let orders: [[Int]] = [
[3, 4, 5, 6],
[3, 5, 4, 6],
[3, 6, 4, 5],
[4, 5, 3, 6],
[4, 6, 3, 5],
[5, 6, 3, 4],
]
var probs: [Double] = []
func setProbs() {
probs = [1.0]
for _ in 1...6 {
probs.append(Double.random(in: 0.5...0.52))
}
probs.sort(by: >)
}
func match(a: [Int : Double], b: [Int : Double]) -> [Int : Double] {
var returnVal: [Int : Double] = [:]
for i in a {
for j in b {
let iWin = probs[i.key] * (1.0 - probs[j.key])
let jWin = probs[j.key] * (1.0 - probs[i.key])
let iWinProb = iWin / (iWin + jWin)
let jWinProb = jWin / (iWin + jWin)
returnVal[i.key, default: 0.0] += iWinProb * i.value * j.value
returnVal[j.key, default: 0.0] += jWinProb * i.value * j.value
}
}
return returnVal
}
var results: [[Double]] = [[Double]](repeating: [Double](repeating: 0.0, count: 7), count: 6)
for _ in 1...100 {
setProbs()
for (i, order) in orders.enumerated() {
let result = match(
a: match(
a: [1 : 1.0],
b: match(
a: [order[0] : 1.0],
b: [order[1] : 1.0])),
b: match(
a: [2 : 1.0],
b: match(
a: [order[2] : 1.0],
b: [order[3] : 1.0])))
for r in result {
results[i][r.key] += r.value
}
}
}
for i in 0...5 {
print("[1, [\(orders[i][0]), \(orders[i][1])]][2, [\(orders[i][2]), \(orders[i][3])]]")
for j in 1...6 {
print(String(format: "%.3f", results[i][j]), terminator: " ")
}
print()
for j in 2...6 {
print(String(format: "%.3f", (results[i][j - 1] - results[i][j])), terminator: " ")
}
print()
print()
}
Discussion