女子卓球レーティング

囲碁将棋などで有名な実力計算方式である"イロレーティング"を卓球に応用し、ITTFランキングでは測れない「実力」を試算しました。イロレーティングをもとに、トーナメントドローに応じたモンテカルロシミュレーションを加えた勝敗予想を提供します。さらにイロレーティングを超える「AIレーティング」を開発しています。

【テクニカル】Pythonによるモンテカルロ・シミュレーションの概略

Pythonによるモンテカルロ・シミュレーションの概略をメモしておきます。

今回で、現時点で使っている主要なPythonコードの説明は最終回です。そのほかにも、レーティング推移とかランキング推移とか、このブログではいろいろPythonで分析しています。

モンテカルロ・シミュレーションについてはモンテカルロ法 - Wikipediaを参照してください。ざっくり言うと「サイコロを振って当てずっぽうに結果を出すことでも、何度も何度も繰り返すと、あら不思議、かなり確からしい予測になります」という理論です。イロレーティングによって各試合の勝率は出せますので、その勝率をもとに乱数で勝者を予測することを繰り返してトーナメントの優勝者を求める、というのを大量に繰り返すことで、各プレイヤーの確からしい優勝確率を得ることができます。AIで有名なアルファ碁でも利用している、由緒正しい?方法です。

以下に主要なソースを示します(細かいところは省略しています)。

# 説明1
import random

# 説明2
tournament_entries = [
    ('伊藤美誠', '早田ひな'),
    ('石川佳純', '橋本帆乃香'),
]

# 説明3
class PredictMatch:
    # 説明4
    def __init__(self):
        self.playerA = Player()
        self.playerX = Player()
        self.round = 0
        self.result = 0  # 1: Winner is playerA, 0: Winner is playerX

    # 説明5
    def ratingA(self):
        return 1 / (10 ** ((self.playerX.rating - self.playerA.rating) / 400) + 1)

    # 説明6
    def eval(self):
        rating = random.random()
        if rating < self.ratingA():
            self.result = 1

    # 説明7
    def winner(self):
        if self.result == 0:
            return self.playerX
        else:
            return self.playerA

# 説明8
class PredictTournament:
    # 説明9
    def __init__(self):
        # 説明10
        self.size = len(tournament_entries) * 2
        self.matches = []
        # 説明11
        for match_pair in tournament_entries:
            match = PredictMatch()
            # 説明12
            result_list = [x for x in players if x.name == match_pair[0] or x.nameJa == match_pair[0]]
            match.playerA = result_list[0]
            result_list = [x for x in players if x.name == match_pair[1] or x.nameJa == match_pair[1]]
            match.playerX = result_list[0]
            match.round = self.size
            self.matches.append(match)

    # 説明13
    def eval(self, round):
        new_match = PredictMatch()
        new_match_is_setting = False
        # 説明14
        for match in [x for x in self.matches if x.round == round]:
            # 説明15
            match.eval()
            # 説明16
            if not new_match_is_setting:
                new_match.playerA = match.winner()
                new_match.round = match.round // 2
                self.matches.append(new_match)
                new_match_is_setting = True
            else:
                self.matches[-1].playerX = match.winner()
                new_match = PredictMatch()
                new_match_is_setting = False

    # 説明17
    def playout(self):
        round = self.size
        while round > 1:
            self.eval(round)
            round = round // 2

# 説明18
winner_count = {}
# 説明19
trial_count = 100000
for i in range(trial_count):
    tournament = PredictTournament()
    tournament.playout()
    # 説明20
    winner = tournament.matches[-1].playerA  # Get the next of the last match (playerA == winner)
    # 説明21
    if winner.id in winner_count:
        winner_count[winner.id] += 1
    else:
        winner_count[winner.id] = 1

今回は、これまで省略していたClass定義部分について、全体ロジックの説明に重要な部分を含むため、一部紹介しています。また、Python特有のリスト内包表記も多用しています。そのため、今までよりも説明が多いですが、がんばって読んでみてください。

説明内容
1 乱数を使うためのライブラリを宣言します。
2 トーナメントの組み合わせを定義します。この例では2020年全日本選手権のSF以降を定義しています。個々の試合を()を使う「タプル」という構造で定義し、その全体をリストで表現しています。タプルは、1つの試合を構成する2人のプレイヤーのような、優先関係が無い値の組を表現するのに都合が良いです。
3 クラス定義です。ここでは予測結果や予測用関数(クラスに所属する関数はメソッドと呼びます)を含む試合クラスを定義しています。
4 クラスを実際に使う(オブジェクトとかインスタンスと呼ぶ状態にする)時に、自動的に初期化するコードの定義です。コンストラクタと呼びます。一般的にクラスで使う変数を宣言して初期化します。
5 クラスメソッドの定義です。ここでは試合でPlayerAが勝利する確率を、イロレーティングによって求めています。
6 クラスメソッドの定義です。ここでは、どちらのプレイヤーが勝利するかを、イロレーティングと乱数で決定しています。モンテカルロ・シミュレーションの根幹の部分です。ここを説明したいためにクラスから紹介しました。なお、evalとはevaluation(評価)の略で、予測系のコンピュータサイエンスでは頻出する言い方です。
7 クラスメソッドの定義です。ここではevalの結果としてどちらが勝利をしたのかを求めています。
8 クラス定義です。ここでは先ほどの試合クラスを、トーナメント全体としてまとめて管理しています。
9 コンストラクタです。ここでは説明2のトーナメント組み合わせ定義を読み込んで、試合クラスなど評価可能なオブジェクトに変換しています。
10 トーナメント組み合わせ定義をもとに、トーナメントサイズを求めています。初期の組み合わせ定義である1回戦がRoundいくつになっているかは、トーナメントサイズから自動判定しています。
11 トーナメント組み合わせ定義を読み込んで、試合クラスのオブジェクトに変換するループです。
12 試合クラスのオブジェクトへの変換の一部として、組み合わせ定義に記載されたプレイヤー名をもとにPlayerクラス(ソースでは省略)を検索しています。組み合わせ定義には名前しかありませんが、所属国とかイロレーティング値とか分析に必要な情報はPlayerクラスにまとまっているため、Playerクラスを求めておくと便利です。Python特有のリスト内包表記を使っており、if以降の条件(プレイヤー名がPlayerクラスのnameかnameJaに一致すること)でPlayerオブジェクトをplayersから検索しています。なんとなくわかるでしょう。
13 トーナメントの評価です。ここでは1つのroundの全試合をシミュレーション実行し、その結果の勝利者をもとに次のroundを生成しています。
14 リスト内包表記を応用したループです。特定roundのみの全試合だけループ対象にしています。
15 個々の試合の評価をします。
16 試合結果をもとに次のroundを生成しています。個々の試合の勝者は次のroundの片方のプレイヤーに過ぎませんので、2試合分を組み合わせて次のroundを生成する必要があり、やや複雑なロジックになっています。
17 トーナメントの全てのroundを最後までやり切る、すなわち優勝者を決めるメソッドです。playoutというのはモンテカルロ・シミュレーション特有の言い方で、個々の打ち手を乱数で試行することを全体の勝敗が確定するまで繰り返しやり切る、という概念です。なお、playoutすると、決勝戦の次のroundとして、優勝者のみがplayerAに配置される架空のround(== 1)が生成されます。
18 ここまで長々と準備したら、あとはplayoutをたくさん繰り返して、優勝した人を数え上げて確率を求めればよいです。それが確からしい優勝確率である、というのがモンテカルロ・シミュレーションの原理です。優勝した人とその回数を記録する辞書型変数を準備します。
19 playoutの繰り返し回数です。10万回としています。
20 1回のplayoutでの優勝者を求めています。tournament.matches[-1].playerAの-1は「リストの一番最後」という意味です。最後の試合は架空の優勝者だけ残るround == 1でした。そのため、その架空の試合のplayerAが優勝者となります。
21 辞書型変数の優勝回数をカウントアップします。初優勝の人は新規に優勝回数1をセットします。10万回試行後に、優勝回数÷10万がその人の優勝確率であり、その逆数が優勝オッズとなります。

いかがでしょうか。ご参考になりましたら幸いです。