大リーグポストシーズン組み合わせの謎をプログラムを走らせて分析する

に公開

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

これは片方のリーグの頂点を決める組み合わせです。
なぜ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の右の範囲が選ばれるとき、どちらの勝ちが選ばれるかというもの。
下の方に具体的な値にしたときの表を追加している。

で、検証のやり方は

  1. 0.5から0.6のあたりでランダムに6つの値を取得して、それを6チームの勝率とする。
  2. 6チームの組み合わせ方を決める
  3. それぞれのチームの優勝する確率を求める。
  4. すべての組み合わせで2と3を行う。
  5. 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