Pythonでモンテカルロ法によるテニスの勝率推移シミュレーションをつくってみた


#モンテカルロシミュレーション
乱数を用いて事象を確率的にシミュレーションする手法のことです。乱数で繰り返し計算を行い、統計的に答えを出すことができます。
このモンテカルロシミュレーションを活用して、テニスの試合の勝率を算出することをやってみました。

#なんのためにつくったの？（目的）
テニスの試合で、ゲームスコア、統計スタッツ以外で試合の内容を伝えられるデータ可視化方法はないかなあ、と前から考えてました。

というのも、テニスは数時間戦うスポーツで、どうしても試合の中でアップダウンがあります。
その試合の流れというか、選手の優勢・劣勢の推移を表現できれば、より試合の全体像が伝えられるんじゃないか。

そんな思いもあって、試合の流れを可視化・表現することにトライしてみました。

モンテカルロシミュレーションで勝率を算出してその勝率推移をチャート化します。

#勝率推移チャート
2018年錦織選手とグルビス選手が対戦した試合のデータを用いて説明していきます。
↓は勝率推移をプロットした結果となっています。

![Image1 (1).png](https://qiita-image-store.s3.amazonaws.com/0/45007/48f811a5-ae72-0150-c7b8-c5c75a93b82e.png)


直近30ポイントのサーブポイント率を算出し、そのポイント率で残りの試合を進行させたときの勝率をモンテカルロシミュレーションにて計算し、その推移をプロットしてます。

#勝率はサーブポイント率をもとにモンテカルロシミュレーションを行い算出
勝率の計算方法について、簡単ですが説明します。
計算のイメージ図は↓のようになっています。
![Image4.png](https://qiita-image-store.s3.amazonaws.com/0/45007/c98dad04-e24b-fb70-8a92-8998b3ab7891.png)

まずはサーブポイント率を計算し、それをもとにモンテカルロシミュレーションを行います。
乱数シミュレーションではどちらの選手が勝つかを判定し、それを1000回行い、1000回中何回勝つかをみて勝率を計算します。

勝率を計算したら、次のポイントに移行し、サーブポイント率の計算・モンテカルロシミュレーションと前のポイントと同じようにして勝率を計算します。

こうして全ポイントの勝率を計算していきます。

#各選手のサーブポイント率を計算する
どうやってサーブポイント率を計算しているか説明しましょう。

↓の表は錦織とグルビスのどちらがポイントを取得したかを示しています。
錦織が○、グルビスが×になっていれば錦織がポイントをとったことになります。

![Image2.png](https://qiita-image-store.s3.amazonaws.com/0/45007/1b89031f-fdc9-0d34-72ed-ab3dd07c6028.png)

例えば、34ポイント目のポイント率を計算する場合、その前10ポイントが計算対象となります。
（実際は、直近30ポイントで計算してますが、表が長くなるので説明では10ポイントとします）

サーブのポイント率を計算するので、錦織であれば錦織のサーブ10ポイント（黄色）、グルビスであればグルビスのサーブ10ポイント（オレンジ色）となります。

それぞれのサーブポイント率を計算すると、
錦織は○が４つあるので、4/10で40%のサーブポイント率
グルビスは○が9つあるので、9/10で90%のサーブポイント率
となります。
（実際は、1stサーブと2ndサーブそれぞれのポイント率を算出してます。また1stサーブのIn率も計算してます。）

一番最初のポイントは両選手50%のサーブポイント率とします。
両選手の1（〇）の数、0（×）をカウントするため、↓のように設定します。

```python:
point_1st_A = np.array((0, 1) * num, dtype='uint8')#start as 50percentage
point_1st_B = np.array((0, 1) * num, dtype='uint8')#start as 50percentage
```

1の数から、両選手のサーブポイント率を計算します。

```python:
def calcPer(np_pointList):
    per = np.count_nonzero(np_pointList) * 100.0 / len(np_pointList)
    return per

per_point1st_A = round(calcPer(point_1st_A), 1)
per_point2nd_A = round(calcPer(point_2nd_A), 1)

per_point1st_B = round(calcPer(point_1st_B), 1)
per_point2nd_B = round(calcPer(point_2nd_B), 1)


```

#モンテカルロシミュレーションにて残りのポイントを進行させ、勝つか負けるかを判定させる
計算したサーブポイント率で残りのポイントを進行させた場合、試合に勝つか負けるかを判定させます。

34ポイント目の次の35ポイント目は、錦織のサーブなので、40%の確率で錦織がポイント勝利し、60%の確率でグルビスがポイントロスとなるように、
乱数シミレーションを実施します。

コードは↓のようにかいています。
np.random.randint(0, 100)で、0～99の乱数を生成し、サーブポイント率perServeを上回るかを判定することで、ポイントを進行させます。

```python:
def addGamePoint(perServe, serve, gamePoint, numServe, totalServe):
    totalServe[serve] += 1
    winlose = np.random.randint(0, 100) < perServe[serve]  # 0～99
    if(winlose):
        gamePoint[serve] += 1
        numServe[serve] += 1
    else:#
        gamePoint[(serve + 1) % 2] += 1
    return gamePoint
```

36ポイント目以降～試合が終わるまでこのやり方で計算して試合を進行させ、最終的に錦織とグルビスのどちらが勝つかを判定します。

#モンテカルロシミュレーションを1000回繰り返して、勝った回数から勝率を計算する
↑の乱数計算による勝ち負け判定を1000回繰り返し、錦織が200回勝ち、グルビスが800回勝つという結果になれば、このときの錦織の勝率は20%となります。

これで34ポイント目の勝率計算は終了し、次の35ポイント目の勝率の計算に進みます。
前のポイントと同じように、サーブポイント率とモンテカルロシミュレーションを実施し、勝率を計算します。

これを最後までやることで前述した全ポイントの勝率チャートが得られます。

以上、こんな感じで勝率の計算をしています。

#gifアニメーション
少しでも臨場感が伝わればと思い、グラフをアニメーションにして動きをもたせてみました。

<blockquote class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr">テニスの勝率推移シミュレーションのグラフアニメーション見た目を少し修正しました。錦織ロッテルダム4試合（エルベール→グルビス→フチョビッチ→ワウリンカ）データの取得から計算、グラフ化まで一気通貫で出力できるよう自動化してます <a href="https://twitter.com/hashtag/%E3%83%86%E3%83%8B%E3%82%B9%E3%83%87%E3%83%BC%E3%82%BF%E3%81%AE%E8%A6%96%E8%A6%9A%E5%8C%96?src=hash&amp;ref_src=twsrc%5Etfw">#テニスデータの視覚化</a> <a href="https://twitter.com/hashtag/%E3%83%86%E3%83%8B%E3%82%B9%E3%81%AE%E5%8B%9D%E7%8E%87%E6%8E%A8%E7%A7%BB%E3%82%B7%E3%83%9F%E3%83%A5%E3%83%AC%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3?src=hash&amp;ref_src=twsrc%5Etfw">#テニスの勝率推移シミュレーション</a> <a href="https://t.co/PANZgDJjka">pic.twitter.com/PANZgDJjka</a></p>&mdash; おたこ (@otakoma) <a href="https://twitter.com/otakoma/status/1097142723776299008?ref_src=twsrc%5Etfw">2019年2月17日</a></blockquote>
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

#Pythonコード
[github.com/taikoma/TennisWinProbSim](https://github.com/taikoma/TennisWinProbSim)

Pythonで、JupyterNotebookで実行できるipynbとなってます。

全コードは↓です。

```python:
#This Script is for Win Probability Simulation
#Before simulating,You need to edit the init.json

import pandas as pd
import numpy as np
import codecs
import sys
import json


def isServeIn(perServeIn, serve):
    return np.random.randint(0, 100) < perServeIn[serve]


def addGamePoint(perServe, serve, gamePoint, numServe, totalServe):
    totalServe[serve] += 1
    winlose = np.random.randint(0, 100) < perServe[serve]  # 0～99
    if(winlose):
        gamePoint[serve] += 1
        numServe[serve] += 1
    else:
        gamePoint[(serve + 1) % 2] += 1
    return gamePoint


def returnGameNext(gamePoint, serve, game):
    for i in range(2):
        if(gamePoint[i] > 3 and (gamePoint[i] - gamePoint[(i + 1) % 2]) > 1):
            game[i] += 1
            serve = (serve + 1) % 2
            gamePoint[0] = 0
            gamePoint[1] = 0
    return gamePoint, serve, game


def returnTiebreakNext(gameNum, gamePoint, serve, game, status, gameLog, sets):
    for i in range(2):
        if(gamePoint[i] > 6 and (gamePoint[i] - gamePoint[(i + 1) % 2]) > 1):
            status = 0
            game[i] += 1
            serve = (serve + 1) % 2
            gamePoint[0] = 0
            gamePoint[1] = 0

            gameLog.append([gameNum, game[0], game[1]])
            sets[i] += 1
            game[0] = 0
            game[1] = 0

    return gamePoint, serve, game, status, gameLog, sets


def returnSetNext(gameNum, game, serve, sets, status, gameLog):
    if((sets[0] + sets[1]) < 4 and game[0] == 6 and game[1] == 6):
        status = 1
    else:
        for i in range(2):
            if(game[i] > 5 and (game[i] - game[(i + 1) % 2]) > 1):
                gameLog.append([gameNum, game[0], game[1]])
                sets[i] += 1
                serve = (serve + 1) % 2
                game[0] = 0
                game[1] = 0
    return game, serve, sets, status, gameLog


def returnFinishNext(serve, sets, status, gameWon_array, setLog, setMatch):
    for i in range(2):
        if(sets[i] > setMatch - 1):
            setLog.append([sets[0], sets[1]])
            gameWon_array[i] += 1
            status = 2
    return status, gameWon_array, setLog


def createList(dftemp):
    list = [
        dftemp["1st Serve"],
        dftemp["1st Serve Points Won"],
        dftemp["2nd Serve Points Won"],
        dftemp["1st Serve Return Points Won"],
        dftemp["2nd Serve Return Points Won"]]
    return list


def calcWonPer(
        per1stServeIn,
        per2ndServeIn,
        p1_1,
        s1_1,
        p1_2,
        s1_2,
        p2_1,
        s2_1,
        p2_2,
        s2_2,
        numGames,
        gamePoint_start,
        game_start,
        sets_start):

    s = 0
    perGame_array = []
    stats_array = []

    num1stServe = [0, 0]
    num2ndServe = [0, 0]
    total1stServe = [0, 0]
    total2ndServe = [0, 0]
    gameWon_array = [0, 0]
    gameLog = []
    setLog = []

    for i in range(numGames):
        num1stServe = [0, 0]
        num2ndServe = [0, 0]
        total1stServe = [0, 0]
        total2ndServe = [0, 0]
        status = 0
        gamePoint = gamePoint_start[:]
        game = game_start[:]
        sets = sets_start[:]

        serve = np.random.randint(0, 2)
        dist1_1 = np.random.normal(p1_1, s1_1)
        dist2_1 = np.random.normal(p2_1, s2_1)
        dist1_2 = np.random.normal(p1_2, s1_2)
        dist2_2 = np.random.normal(p2_2, s2_2)

        per1stServe = [dist1_1, dist2_1]
        per2ndServe = [dist1_2, dist2_2]

        while status < 2:
            if(isServeIn(per1stServeIn, serve)):
                gamePoint = addGamePoint(
                    per1stServe, serve, gamePoint, num1stServe, total1stServe)
            else:
                if(isServeIn(per2ndServeIn, serve)):
                    gamePoint = addGamePoint(
                        per2ndServe, serve, gamePoint, num2ndServe, total2ndServe)
                else:
                    gamePoint[(serve + 1) % 2] += 1

            if(status == 1):
                gamePoint, serve, game, status, gameLog, sets = returnTiebreakNext(
                    i, gamePoint, serve, game, status, gameLog, sets)
            elif(status == 0):
                gamePoint, serve, game = returnGameNext(gamePoint, serve, game)

            game, serve, sets, status, gameLog = returnSetNext(
                i, game, serve, sets, status, gameLog)
            status, gameWon_array, setLog = returnFinishNext(
                serve, sets, status, gameWon_array, setLog, 2)

    wonPerA, wonPerB = round(
        gameWon_array[0] / numGames * 100, 1), round(gameWon_array[1] / numGames * 100, 1)
    return wonPerA, wonPerB


def calcPer(np_pointList):
    per = np.count_nonzero(np_pointList) * 100.0 / len(np_pointList)
    return per


def inOut(np_pointList, a):
    # pointList.pop(0)######todo numpy方式に変換
    np_pointList = np.delete(np_pointList, 0)
    np_pointList = np.append(np_pointList, a)  # pointList.append(a)
    return np_pointList


def calcPoint(ab1, ab2, df, serveIn, point_1st, point_2nd, gamePoint):  # ab1=1 Aのポイント ab2=1 Bのポイント
    gamePoint[ab2] += 1
    if(int(df['FirstSecond'][i]) == 1):
        serveIn = inOut(serveIn, 1)
        point_1st = inOut(point_1st, ab1)
    elif(int(df['FirstSecond'][i]) == 2):
        point_2nd = inOut(point_2nd, ab1)
    return point_1st, point_2nd


if __name__ == '__main__':
    f = open("init.json", 'r')
    json_data = json.load(f)

    playerNameA = json_data['playera']
    playerNameB = json_data['playerb']
    num = int(json_data['plength'] / 2)
    fileName = "data/"+json_data['file']
    nGames = json_data['ngamges']

    point_serveIn_A = np.array((1, 1) * num, dtype='uint8')#start as 100percentage
    point_serveIn_B = np.array((1, 1) * num, dtype='uint8')#start as 100percentage
    point_1st_A = np.array((0, 1) * num, dtype='uint8')#start as 50percentage
    point_1st_B = np.array((0, 1) * num, dtype='uint8')#start as 50percentage
    point_2nd_A = np.array((0, 1) * num, dtype='uint8')#start as 50percentage
    point_2nd_B = np.array((0, 1) * num, dtype='uint8')#start as 50percentage

    with codecs.open(fileName, "r", "SJIS", "ignore") as file:
        df = pd.read_table(file, delimiter=",")
    df['FirstSecond'] = df['FirstSecond'].convert_objects(
        convert_numeric=True).fillna(-1).astype(np.int)
    df = df.reset_index()

    gameLog = []
    setLog = []
    gamePoint = [0, 0]
    game = [0, 0]
    sets = [0, 0]

    pointA_array = []
    pointB_array = []

    gameA_array = []
    setA_array = []
    gameB_array = []
    setB_array = []

    flow_array = []
    point1st_array = []
    point2nd_array = []
    status = 0
    serve = 0

    for i in range(len(df)):
        print("Point",i)
        if(df['Server'][i] == playerNameA):
            point_1st_A, point_2nd_A = calcPoint(int(df['WonA'][i]), (int(
                df['WonA'][i]) + 1) % 2, df, point_serveIn_A, point_1st_A, point_2nd_A, gamePoint)
            serve = 0
        elif(df['Server'][i] == playerNameB):
            point_1st_B, point_2nd_B = calcPoint(
                (int(
                    df['WonA'][i]) + 1) %
                2, (int(
                    df['WonA'][i]) + 1) %
                2, df, point_serveIn_B, point_1st_B, point_2nd_B, gamePoint)
            serve = 1

        if(status == 1):
            gamePoint, serve, game, status, gameLog, sets = returnTiebreakNext(
                i, gamePoint, serve, game, status, gameLog, sets)
        elif(status == 0):
            gamePoint, serve, game = returnGameNext(gamePoint, serve, game)

        game, serve, sets, status, gameLog = returnSetNext(
            i, game, serve, sets, status, gameLog)
        # status,gameWon_array,setLog=returnFinishNext(serve,sets,status,gameWon_array,setLog,3)
        
        #print(i+2,sets,game)

        per_serveIn_A = round(calcPer(point_serveIn_A), 1)
        per_serveIn_B = round(calcPer(point_serveIn_B), 1)

        per_point1st_A = round(calcPer(point_1st_A), 1)
        per_point2nd_A = round(calcPer(point_2nd_A), 1)

        per_point1st_B = round(calcPer(point_1st_B), 1)
        per_point2nd_B = round(calcPer(point_2nd_B), 1)

        pointA_array.append(gamePoint[0])
        pointB_array.append(gamePoint[1])

        gameA_array.append(game[0])
        setA_array.append(sets[0])
        gameB_array.append(game[1])
        setB_array.append(sets[1])

        flow_array.append(calcWonPer([per_serveIn_A,
                                      per_serveIn_B],
                                     [100,
                                      100],
                                     per_point1st_A,
                                     8.2,
                                     per_point2nd_A,
                                     10.8,
                                     per_point1st_B,
                                     8.2,
                                     per_point2nd_B,
                                     10.8,
                                     nGames,
                                     gamePoint,
                                     game,
                                     sets))

        point1st_array.append([per_point1st_A, per_point1st_B])
        point2nd_array.append([per_point2nd_A, per_point2nd_B])
        #print(flow_array)
    #df_add = pd.DataFrame({'PointA':pointA_array,'PointB':pointB_array,'GameA':gameA_array,'GameB':gameB_array,'SetA':setA_array,'SetB':setB_array})
    fileName=fileName.replace(".csv","")
    np.savetxt(fileName + '_output.csv', np.array(flow_array), delimiter=',')
    print("Complete")
```