ishigenの技術ブログ

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

フォームの可視化と再現性の計算①

今回は前回の記事の動画を用いて、フォームの可視化と再現性の計算を行います。とりあえず動くコードでしかないので、後々もう少しきれいにします。

ishigentech.hatenadiary.jp

環境

ubuntu 16.04.04
python 3.6.6
jupyter notebook

フォームの可視化について

全身が写っているものから始めるのがやりやすかったため、今回はカットです。そのうちドライブとかチキータとでもやりたいとは思います。

openposeについて

前回の記事でも出ましたが、openposeは骨格を推定するものです。下記画像のように、体の各ポイント18点を推定し、数値として座標を出力します。
f:id:ishigentech:20180916140807p:plain
公式より

可視化の結果

出力された数値を用いて、体の動きをグラフにするとこうなります。各ポイント体の動きを折れ線で表現しています。
1を原点にしているので、全て0にしています。f:id:ishigentech:20180916150312p:plain3が右肘なのでカットの動きになっています。f:id:ishigentech:20180916150316p:plain4は右手首で3と同様です。f:id:ishigentech:20180916150319p:plainf:id:ishigentech:20180916150323p:plainf:id:ishigentech:20180916150327p:plainf:id:ishigentech:20180916150331p:plain12は左膝です。右膝と比較しても意外と動いています。高さを調節しているのかもしれません。f:id:ishigentech:20180916150334p:plainf:id:ishigentech:20180916150337p:plain16は欠損値が多いかつ右耳だったので、全て同じ値にしました。f:id:ishigentech:20180916150340p:plain

以上です。体のポイントごとの動きなので、変な癖とかがあるとすぐにわかりそうではあります。

データの前処理

import json
import numpy as np
import matplotlib.pyplot as plt
import os
from pylab import *

データの読み込み
json形式で保存されているファイルを読み込みます。

data = np.array([])
path = 'パス'
files = os.listdir(path)
files = sort(files)
for file in files:
    filename = path +'/'+ file
    f = open(filename)
    val = json.load(f)
    f.close()
    val = np.array(val['people'][0]['pose_keypoints_2d'])
    data = np.append(data, val)
data = data.reshape(len(files), 18, 3)
  1. 欠損値、外れ値の処理
  2. どこからどこまでがカットなのかを判断
  3. 欠損値、外れ値の処理
  4. 横ずれの調整


1. 外れ値がそれなりにあるので、分割する前に中央値で補完します。二回に分かれてしまっているのは、対応方法が異なるからです。

for i, ivalue in enumerate(data):
    if i <= 1: continue
    for k,kvalue in enumerate(ivalue):
        for t, tvalue in enumerate(kvalue):
            if t >= 1:
                medivalue = data[i-2:i+3, k, t]
                data[i, k, t] = np.median(medivalue)
    if i >= 580: break


2. まず厳密な区分は存在しません。ただこの計算には一律の評価基準があればいいので、今回は目で見て判断しました。自分で判断したのでフレーム単位で見るとノイズが含まれているのは間違いありません。20フレームを1スイングとして抽出しました。7回打球しているので(7*20*18*3)の四次元のデータを使います。
※(スイング数*1スイングあたりのフレーム数*推定される体のポイントの数*x座標y座標スコアの3つ)

swingperframe = 20
swing = np.array([])
index = np.array([140,208,277,343,412,485,558])
for i in index:
    swing = np.append(swing, data[i:i+swingperframe,:,:])
swing = swing.reshape(len(index),swingperframe, 18, 3)
print(swing.shape)

3. 3フレーム以上連続で欠損値を取るデータがあり、これらは上記の中央値での補完処理では対応しきれないので、分割してから平均値で修正します。

for i,ivalue in enumerate(swing):
    for k,kvalue in enumerate(ivalue):
        for t, tvalue in enumerate(kvalue):
            for w, wvalue in enumerate(tvalue):
                if wvalue == 0 and w <= 1:
                    swing[i, k, t, w] = np.mean(swing[:, k, t, w])

4.鎖骨の中心あたりのポイントを原点として調整し、横への移動の影響を除きます。ミドルのボールを打球してしまうときなどのためです。

for i in range(7):
    for k in range(20):
        originx = swing[i, k, 1, 0]
        originy = swing[i, k, 1, 1]
        swing[i, k, :, 0] -= originx
        swing[i, k, :, 1] -= originy

これでデータを確認するとポイント16にいくつか欠損値があり、ポイント16は右耳とかで今回は分析対象ではないので、影響を除きます。

swing[:, :, 16, 0] = swing[:,:,16,0].mean()
swing[:, :, 16, 1] = swing[:, :, 16, 1].mean()

以上で前処理は終了です。

グラフへの描画方法

以下を実行すると、冒頭のグラフが描画されます。

%matplotlib inline

for k in range(9):
    plt.figure(figsize=(15,15))
    for i in range(2):
        plt.subplot(1,2,i+1)
        for t in range(7):
            x = swing[t,:,i+k*2,0]
            y = swing[t,:,i+k*2,1]*-1
            plt.plot(x,y)
            plt.ylim(np.min(swing[:,:,:,1] * -1),np.max(swing[:,:,:,1] * -1))
            plt.xlim(np.min(swing[:,:,:,0]),np.max(swing[:,:,:,0]))
            plt.title(str(i + k * 2), fontsize=20)
            plt.grid(True)
        plt.grid(True)
    #filename = 'plot'+ str(k)
    #plt.savefig(filename)
    plt.show()

再現性の計算

再現性の計算とか難しそうな書き方してますが、結局はどれだけフォームが固まっているかってことです。どうやって求めるかというと、上記のグラフの線がどれだけばらついているかを計算します。

計算方法

  1. 同じフレーム、同じ体のポイントでスイング間のばらつきを比較する
  2. 集計と指標の計算

1. バラつきの計算は同じフレーム、同じ体のポイントの7つの点の分散を求めています。resultに(20*18*2)のデータが入ります。

result = np.array([])
for i in range(20):
    for k in range(18):
        x = swing[:, i, k, 0]
        y = swing[:, i, k, 1]
        meanx = x.mean()
        meany = y.mean()
        result = np.append(result,sum((x - meanx) ** 2))
        result = np.append(result,sum((y - meany) ** 2))
result = result.reshape(20,18,2)

2. 計算の精度はopenposeから出力されるスコアの平均を取れば求められます。ついでに結果ももう少し見やすくします。各点の標準偏差の平均値を集計し、さらにその平均値を全体のずれとして出してみました。現段階ではこの数値が高いのか低いのかは判断がつかない状態であり、意味をなさないものになっています。これから対応したいところです。

print('精度')
print(swing[:,:,:,2].mean())
print('各ポイントのずれ')
aggregate = np.array([])
for i in range(18):
    print(i,sqrt(result[:, i, 0]).mean(),sqrt(result[:, i, 1]).mean())
    aggregate = np.append(aggregate,sqrt(result[:, i, 0]).mean())
    aggregate = np.append(aggregate,sqrt(result[:, i, 1]).mean())
aggregate = aggregate.reshape(18, 2)
print('全体のずれ')
print(aggregate.mean())

出力結果

精度
0.839300687936508
各ポイントのずれ
0 9.50189204164633 10.564456302273527
1 0.0 0.0
2 9.282807860849129 7.985441214121016
3 14.702756879996823 10.611583195300145
4 23.20150323797283 22.482897128714875
5 8.508900989408707 7.900002342359047
6 11.781042222499767 13.336132157249125
7 15.929800657273452 14.729540493196364
8 16.141903539345996 11.318877266007219
9 27.38270955353574 15.6975989054097
10 59.53738866599265 21.545035691419343
11 15.903026407091605 15.729575436582426
12 37.45575582965355 27.12303847364089
13 55.11463401664059 28.103541255376577
14 9.363773603068832 11.410209261304832
15 8.897818944743424 11.028936257261567
16 9.399596873523532e-15 0.0
17 8.009300705809611 8.791248233536667
全体のずれ
15.807586910257843

課題

  • 評価尺度が曖昧

もっとたくさんのデータがないと今回計算した数値がどのくらいのものなのかが全くわかりません。求めた標準偏差ピクセル単位なので、身長との比率とかを取れば、cm単位では出せるかもしれませんが、、、

  • データが少ない

毎回課題になりますが、やはり7スイングだとデータが少なく、100スイングくらいは揃えたほうがいいと思います。ただ、フレーム単位でどこからがスイングの始まりなのかを判断するのに手間がかかります。

  • まだまだノイズが含まれている

横ずれだけは対応しましたが、他にも様々なノイズが含まれていると思います。30fpsだとスイングスピードが微妙に速かったりすると、おそらく誤差になってしまいます。

  • 卓球台が邪魔

冗談ではなく、この分析の際には卓球台が本当に邪魔で、正面から全身が映る映像を取れるプレーが限られてしまいます。この辺も考えていかないといけません。

まとめ

今回は以上です。
再現性の計算は課題がたくさんありますが、可視化するほうは使えたりするんじゃないかと思っています。体の部位ごとに分割してフォームを見たりすることもないと思うので、やってみると面白いかもしれません。

卓球の動画でoponposeを試す

今回は卓球の映像をopenposeで試してみます。windowsでのデモ環境で動かしています。本当はubuntuで動かしたいんですが、今入ってるのがCUDA9.2とかでバージョンを合わせるのがめんどくさそうでやめました。

環境

Windows10
CUDA8
cuDNN5.1

openpose

github.com
openposeってのは、人間の写真や映像から骨格を推定してくれるやつです。

とりあえず試してみる

クラウドをあさったら発見した三年前のサーブ練習の動画から。


上半身はそれなりの精度で推定されています。ただ、台が邪魔で上半身しか映らないので、下半身の推定はうまくできていません。


次は全身が移るような動画で試してみます。


この動画だと全身が映っているので、うまく推定できているかと思います。

これから何ができるか

今回試したように、もとの動画の上に推定した骨格を上乗せして表示させることもできますが、骨格の推定値を二次元の数値として出力することも可能です。
例(二つ目の動画、1フレーム目、1人目の推定された骨格の画像内での位置)
[495.121,348.579,0.879666,492.466,382.422,0.893065,471.505,384.969,0.919974,468.875,411.349,0.84414,474.084,442.729,0.886111,513.368,377.308,0.87303,536.901,408.693,0.759093,536.907,437.42,0.658357,489.803,445.328,0.889805,495.011,487.14,0.854795,505.463,531.502,0.618641,513.193,442.737,0.871228,505.451,487.17,0.790325,505.443,531.604,0.614839,492.402,345.924,0.988282,502.919,345.905,0.921015,487.076,351.115,0.812774,508.121,351.143,0.432742]


このデータを使えば以下のことはできそうな気がしています。

  • どれだけフォームが固まっているかを計測
  • 各打球ごとの技術ラベル判定(ドライブ、カット、サーブetc)


個人的には基本練習でどれだけ同じフォームで打球できるかが卓球の上達において非常に重要な指標だと思っているので、求めてみる価値はあると思っています。具体的な計算方法はまだわかりません。どうやって計算するか考えてみます。


ラリーの中でどの技術を使ってどっちが得点したかを判定できるようになれば、試合をしっかり振り返りたいやる気のある人の集計作業が非常に楽になります。分析の手法としては、今後の分析も考えるとCNNを使うのがベターかと思います。まだデータ数が足りませんが、取り組んでみたいと思います。


以上です。いい結果が得られればまた書きます。

ツイートからテキスト生成①

今回はおーばさんのツイートからディープラーニングでテキストを生成します。なぜおーばさんなのかというと、自分と異なるテイストのツイートをする人でやってみたくて。おーばさんのツイートは良くも悪くも独特のテイストがあるなあと思い選びました。


結果

5件分のツイートを生成するとこんな感じでした。

してないないから!!!!!!たし笑
わ笑結局結局試合したけど、でだろうけど
あるところってって笑大学のバイト
ちゃったんでんで誰か?だったけど、女子も面白い笑
回もええ、そなねぇ。、、、、、、そうな人がいた。


ちょっと何言ってるかわかりませんよね。(AIにしても女子が出てくるんかい!って思うけど。)ってことで、このテキストがどのくらいましなのかを検討するために、完全にランダムに単語を選んで連結したテキストと比較してみます。

ツイッターで
しまった11マネキンこらフルボッコほど
・∇・)やっぱりhalationノーヒットかわし桐一点
すぎるw用紙段差撮りプラス待った福留しもた人文句くれる
開学場合西武さや軽減かけて川島


完全にランダムに単語を連結した場合はさらに意味がわからないテキストになっていて、ディープラーニングで生成したほうが多少ましかなというレベルです。


データ

ツイートデータはtwitterAPIを叩いて取得した直近3200件を使います。

手法

まず、どうやってディープラーニングのモデルがテキストを生成するかというと、

  1. 最初の 文字列をランダムに選ぶ
  2. 予測:ランダムに選ばれた文字列から、次に当てはまる確率が高い文字列を推定
  3. 連結:予測した文字列を連結する
  4. 以下2と3をループ処理


するのが基本的な流れになります。予測する際に次の単語を予測する方法と次の一文字を予測する方法があります。日本語だと漢字もあり、一文字ずつの予測だと候補数がかなり多くなってしまうので、難しくなります。英語だとアルファベットと記号のみの候補から選ぶことになるので、比較的簡単です。


今回は次に当てはまりそうな単語を予測する方法を採用しています。単語の分割にはmecabを利用し、mecab-ipadic-NEologdを辞書にしています。

モデル

今回はkerasで公開されているテキスト生成モデルを単語ごとに処理するように手を加えて使います。LSTM層と全結合層のみのシンプルな構成になっています。


単語ごとのone-hotエンコーディングを適用させる前処理。

f = open('ohba.txt')
textall = f.readlines()
tagger = MeCab.Tagger("-Owakati")
text = []
for sentence in textall:
    str_output = tagger.parse(sentence)
    list_output = str_output.split(' ')
    list_output = list_output[0:-1]
    if len(list_output) == 0:
        x = 1
    else:
        text.extend(list_output)
f.close()

print('corpus length:', len(text))

chars = sorted(list(set(text)))
print('total words:', len(chars))
word_indices = dict((c, i) for i, c in enumerate(chars))
indices_word = dict((i, c) for i, c in enumerate(chars))

maxlen = 2
step = 3
sentences = []
next_word = []
for i in range(0, len(text) - maxlen, step):
    sentences.append(text[i: i + maxlen])
    next_word.append(text[i + maxlen])

print('Vectorization...')
x = np.zeros((len(sentences), maxlen, len(chars)), dtype=np.bool)
y = np.zeros((len(sentences), len(chars)), dtype=np.bool)
for i, sentence in enumerate(sentences):
    for t, char in enumerate(sentence):
        x[i, t, word_indices[char]] = 1
    y[i, word_indices[next_word[i]]] = 1


モデルの構築。

model = Sequential()
model.add(LSTM(128, input_shape=(maxlen, len(chars))))
model.add(Dense(len(chars), activation='softmax'))

optimizer = RMSprop(lr=0.01)
model.compile(loss='categorical_crossentropy', optimizer=optimizer)
model.summary()

f:id:ishigentech:20180906205632p:plain


このモデルをサンプルにあるコールバック処理を削除して、60エポック学習させてみました。

model.fit(x, y,
          batch_size=128,
          epochs=60)


モデルの予測結果はただの数字の配列であり、その配列を入力してランダム性も考慮して文字を選ぶ関数を定義。

def sample(preds, temperature=1.0):
    preds = np.asarray(preds).astype('float64')
    preds = np.log(preds) / temperature
    exp_preds = np.exp(preds)
    preds = exp_preds / np.sum(exp_preds)
    probas = np.random.multinomial(1, preds, 1)
    return np.argmax(probas)


テキストを生成するコードは以下の通り。

for i in range(5):
    start_index = random.randint(0, len(text) - maxlen - 1)
    generated_text = text[start_index: start_index + maxlen]
    alltext = generated_text
    wrodlength = random.randrange(20)
    for i in range(wrodlength):
        sampled = np.zeros((1, maxlen, len(word_indices)))
        for t, char in enumerate(generated_text):
            sampled[0, t, word_indices[char]] = 1.
        preds = model.predict(sampled, verbose=0)[0]
        next_index = sample(preds, 0.5)
        next_word = indices_word[next_index]
        generated_text.append(next_word)
        generated_text = generated_text[1:]
        alltext.append(next_word)
    print(''.join(alltext))

課題

  1. モデルが単純すぎる
  2. ツイートのような細切れのテキストへの対応
  3. データ数


1.論文やら他の方法を参考にしつつ、もう少し複雑なモデルが必要じゃないかと。

2.ツイート特有の問題でもありますが、3200ツイートを一つの長いテキストを分析するためのモデルに突っ込んでいるので、ツイートの終わりと始まりが考慮できていません。

3.一人のツイートにこだわるのであれば、3200が今のところ限界っぽい。類似単語に置き換えて、似たような文章を作成する方法もありますが、雰囲気が変わってしまいそうで悩みどころ。


本当はこのモデルでツイート生成して、自分の代わりにツイートしてもらおうと企んでいましたが、結果がよろしくなかったので今はやりません。
これらの課題をもう少し改善しつつ、またリベンジします。

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

テニスの試合の流れを可視化したく、確率シミュレーションを用いて勝率の推移をプロットしてみました【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セットマッチ、タイムアウトなしだと以下のように配列の入力だけで動きます。

simlator(np.array)

 

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

 

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