ishigenの技術ブログ

卓球×テクノロジー、機械学習

卓球の試合における流れの可視化

テニスの試合の流れを可視化したく、確率シミュレーションを用いて勝率の推移をプロットしてみました【2018ウィンブルドン錦織】 | DataTennis.NET
このブログを参考にさせて頂き、卓球バージョンを自分なりにやってみました。
それっぽいやつはできたかなと思います。実際にやってみると、下のようなグラフになります。

 

2018年 全日本男子決勝 張本 9,5,-8,2,-6,5 水谷
オレンジ:張本選手 青:水谷選手
f:id:ishigentech:20180829210408p:plain

終始張本選手のペースなのがわかるかと。

 

2017年 世界卓球男子決勝 馬龍 -7,6,3,8,-5,-7,10 樊 振東
オレンジ:樊 振東選手 青:馬龍選手
f:id:ishigentech:20180829210737p:plain

接戦だっただけに、最終セットまでわからない状況なのが見て取れる。

 

グラフの見方としては、

  • 横軸 ポイント
  • 縦軸 それぞれの選手の勝率
  • 青線 セットの区切

 となっていて、ポイントが進むにつれて、試合の後半になっていきます。

 

実際の試合をご覧いただくとわかると思いますが、それとなくは流れを可視化することができているんじゃないかと思います。セットを跨ぐ、タイムアウトが入ると流れも変わりやすくなっているのが感覚的にもしっくりきます。

 

今回のシミュレーションは直近の得点率から求めた確率に試合終了まで従うという非常に強引な得点計算方法であり、シミュレーターとしてはあまり優秀ではありません。実際の試合はお互いの心理状況、戦術などが複雑に絡み合い、その先の得点率に影響を与えているからです。

 

このあたりも考慮して後からどこでどんな流れだったのかを確認するのには使えるんじゃないかと思います。

 

コード(python)は少し長いですがこんなんです。

import numpy as np
import matplotlib.pyplot as plt


両者の得点からシミュレーションに使う得点率を計算。シグモイド関数しか思い浮かばなかったので、シグモイド関数を二回使って数字を圧縮するという荒業。。。

def sigmoid(x):
    return 1.0 / (1.0 + np.exp(-x))
#player1の直近の得点率を計算
def ratecalc(pla1Point,pla2Point):
    x = pla1Point / (pla1Point+pla2Point)
    x = sigmoid(x - 0.5)
    return sigmoid(x - 0.5)


入力した状況(得点、セット数)から終わるまで試合を行う関数を定義。

def gameStart(pla1Point,pla2Point,pla1Set,pla2Set,rate,gamematch):
    #両者の得点
    player1_point = pla1Point
    player2_point = pla2Point
    #セット数
    player1_setcount = pla1Set
    player2_setcount = pla2Set
    #試合終了(どちらかが3セット先取)までループ処理
    for i in range(1000):
        random = np.random.rand()
        player1_point = player1_point + 1 if random < rate else player1_point
        player2_point = player2_point + 1 if random > rate else player2_point
        #得点に応じて、セット終了かどうかを判定する
        if player1_point >= 11 or player2_point >= 11:
            #デュース判定、どっちがセットを取ったか判定
            if player1_point - player2_point >= 2 or player2_point - player1_point >= 2:
                player1_setcount = player1_setcount + 1 if player1_point > player2_point else player1_setcount
                player2_setcount = player2_setcount + 1 if player2_point > player1_point else player2_setcount
                player1_point,player2_point = 0, 0
        if player1_setcount >= gamematch:
            #試合終了,player1の勝利
            return 1
        if player2_setcount >= gamematch:
            #試合終了,player2の勝利
            return 0
    return


gameStart関数を1000回ループして、勝率を計算する処理。

def gameroop(x1,x2,y1,y2,r,gamematch):
    result = np.array([])
    for i in range(999):
        result = np.append(result,gameStart(x1,x2,y1,y2,r,gamematch))
    return result.sum() / 1000


実際の試合の得点を入力し、gameroop関数をぶん回しながら試合終了まで行う処理。

def simulator(gameall,gamematch=3,pla1timeout = 0,pla2timeout = 0,getPointCalc=8):
    player1_point = 0
    player2_point = 0
    player1_setcount = 0
    player2_setcount = 0
    #セットごとの本数をカウント
    seq = np.array([])
    #得点率計算に使う本数-1
    getpointcalc = getPointCalc - 1
    pla1winrate = np.array([])
    pla2winrate = np.array([])
    #配列から現在の得点とセット数を計算
    for i,point in enumerate(gameall):
        if point == 1:
            player1_point += 1
        else:
            player2_point += 1
        if player1_point >= 11 or player2_point >= 11:
            #デュースかどうか
            if player1_point - player2_point >= 2 or player2_point - player1_point >= 2:
                #どっちがセットを取ったか判定
                player1_setcount = player1_setcount + 1 if player1_point > player2_point else player1_setcount
                player2_setcount = player2_setcount + 1 if player2_point > player1_point else player2_setcount
                player1_point,player2_point = 0, 0
                seq = np.hstack((seq,[i]))
        if i >= getpointcalc:
            #getpointcalcで指定した本数で直近の得点本数を計算
            pla1rate = gameall[i-getpointcalc:i+1].sum()
            pla2rate = getpointcalc - pla1rate
            #gameroop関数でシミュレーションする
            pla1winrate = np.hstack((pla1winrate,[gameroop(player1_point,player2_point,
                                                player1_setcount,player2_setcount,ratecalc(pla1rate, pla2rate),gamematch)]))
    #グラフ描画用のコードを長々と記述
    pla2winrate = 1 - pla1winrate
    p = plt.plot(range(getpointcalc,len(gameall)),pla1winrate)
    p = plt.plot(range(getpointcalc,len(gameall)),pla2winrate)
    p = plt.vlines(seq, 0, 1, linestyle='dashed', linewidth=0.5,colors='blue')
    p = plt.vlines(pla1timeout, 0, 1, linewidth=0.5,colors='red')
    p = plt.vlines(pla2timeout, 0, 1, linewidth=0.5,colors='red')
    p = plt.xlim(0, len(gameall))
    p = plt.ylim(0, 1)
    p = plt.xlabel('point')
    p = plt.ylabel('winrate')
    plt.show(p)


こんな感じで変数名の適当さなど突っ込みどころはあるでしょうけど、一応それっぽいやつはできました。
関数の定義が一通りできたので、動かしてみます。 

 

今回は水谷選手の得点時に1にする配列を用意して、これをsimulator関数に放り込むとグラフが出力されます。

#2018全日本男子決勝
mizutani_harimoto = np.array([0,0,1,0,0,1,0,0,0,1,1,1,0,1,0,1,1,0,1,0,
                0,1,1,0,0,1,1,0,0,0,0,0,0,1,0,0,
                1,0,1,1,0,0,1,0,0,0,0,1,0,1,1,1,1,1,1,
                0,0,0,0,0,1,0,0,0,0,0,1,0,
                1,1,1,0,1,1,1,1,0,0,1,1,0,1,0,0,1,
                0,1,0,0,1,1,0,0,0,0,0,0,1,0,1,0])
simulator(mizutani_harimoto, 4, 46)


こっちも同様に。馬龍選手得点時に1となる配列。

#2017世界卓球男子決勝
maron_fan = np.array([0,0,0,0,1,0,0,0,1,1,1,0,1,0,0,1,1,0,
                     1,1,1,1,0,0,1,1,0,0,0,1,1,0,1,1,1,
                     1,1,0,1,0,1,1,0,1,1,1,1,1,1,
                     1,1,0,0,1,1,1,1,0,0,0,0,1,1,1,0,1,0,1,
                     0,0,1,0,0,0,1,0,1,1,0,0,1,0,0,0,
                     0,1,0,0,0,1,0,0,1,0,0,0,1,1,1,0,1,0,
                     0,0,1,1,0,1,1,1,1,0,0,0,1,0,0,0,1,1,1,0,1,1])
simulator(maron_fan, 4, 59,99)


ちなみに、5セットマッチ、タイムアウトなしだと以下のように配列の入力だけで動きます。

simulator(np.array)

 

参考にさせて頂いたブログではサーブ時のポイント率から計算していますが、卓球の場合はテニスと異なりサーブは2本交代なので、サーブレシーブ関係なく、8ポイントで直近ポイントの得点率を求めています。4か8ポイントで求めるのがいいかなとは思いますが、7セットマッチということもあり、8ポイントで計算しています。

 

以上です。やってみたい人がいれば、動かしてみてください。