Edited at

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


モンテカルロシミュレーション

乱数を用いて事象を確率的にシミュレーションする手法のことです。乱数で繰り返し計算を行い、統計的に答えを出すことができます。

このモンテカルロシミュレーションを活用して、テニスの試合の勝率を算出することをやってみました。


なんのためにつくったの?(目的)

テニスの試合で、ゲームスコア、統計スタッツ以外で試合の内容を伝えられるデータ可視化方法はないかなあ、と前から考えてました。

というのも、テニスは数時間戦うスポーツで、どうしても試合の中でアップダウンがあります。

その試合の流れというか、選手の優勢・劣勢の推移を表現できれば、より試合の全体像が伝えられるんじゃないか。

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

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


勝率推移チャート

2018年錦織選手とグルビス選手が対戦した試合のデータを用いて説明していきます。

↓は勝率推移をプロットした結果となっています。

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


勝率はサーブポイント率をもとにモンテカルロシミュレーションを行い算出

勝率の計算方法について、簡単ですが説明します。

計算のイメージ図は↓のようになっています。

まずはサーブポイント率を計算し、それをもとにモンテカルロシミュレーションを行います。

乱数シミュレーションではどちらの選手が勝つかを判定し、それを1000回行い、1000回中何回勝つかをみて勝率を計算します。

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

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


各選手のサーブポイント率を計算する

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

↓の表は錦織とグルビスのどちらがポイントを取得したかを示しています。

錦織が○、グルビスが×になっていれば錦織がポイントをとったことになります。

例えば、34ポイント目のポイント率を計算する場合、その前10ポイントが計算対象となります。

(実際は、直近30ポイントで計算してますが、表が長くなるので説明では10ポイントとします)

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

それぞれのサーブポイント率を計算すると、

錦織は○が4つあるので、4/10で40%のサーブポイント率

グルビスは○が9つあるので、9/10で90%のサーブポイント率

となります。

(実際は、1stサーブと2ndサーブそれぞれのポイント率を算出してます。また1stサーブのIn率も計算してます。)

一番最初のポイントは両選手50%のサーブポイント率とします。

両選手の1(〇)の数、0(×)をカウントするため、↓のように設定します。

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の数から、両選手のサーブポイント率を計算します。

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を上回るかを判定することで、ポイントを進行させます。

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アニメーション

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


Pythonコード

github.com/taikoma/TennisWinProbSim

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

全コードは↓です。

#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")