EurekaMoments

ロボットや自動車の自律移動に関する知識や技術、プログラミング、ソフトウェア開発について勉強したことをメモするブログ

限られた卓球の試合データから追加データを自動計算してみた

スポーツを10倍楽しむ統計学 (DOJIN選書)

スポーツを10倍楽しむ統計学 (DOJIN選書)

  • 作者:鳥越 規央
  • 出版社/メーカー: 化学同人
  • 発売日: 2015/05/20
  • メディア: 単行本(ソフトカバー)

目次

背景・目的

最近では深層学習などを駆使したスポーツのデータ分析が非常に
盛り上がっていますが、自分も趣味である卓球をテーマに何か
データ分析をしてみたくなりました。
その第一歩として、今回は限られたデータから追加でデータを
自動計算させる事にトライしてみたので紹介します。

分析の目標

卓球のデータ分析をするに当たり、自分が今一番興味があるのは
「試合の流れの可視化」です。今どういう状況で試合が進んでいる
のか、ここから先はどういう流れになりそうかなどを定量的に
示せないかを第一の目標にしていきます。

制約条件

スポーツのデータ分析でよく行われているのは、例えばこちらの
ように、テニスコート全体を捉える角度から試合動画を撮り、
そこから選手やボールを検出してトラッキングするといったもの
があります。

datatennis.net

しかしながら、一般的な卓球の試合ではこのような位置にカメラを
設置する事は難しく、大抵の場合はこちらのようにプレイヤーの
後ろ側から捉えるような位置になる場合が多いです。

www.youtube.com

このような場合、卓球台上でのボールの位置を高精度に追う事は
難しそうです。なので今回は、ボールの位置や軌道、球種といった
ようなデータは取れないという制約を設け、それ以外のデータで
どこまで良い分析が出来そうかを探ってみる事にします。

サンプルデータの入手

データ分析をするには、肝心のデータが無ければ始まりません。
何かサンプルになるデータはないかと思っていたところ、
@goes425さんのご厚意で下記サイトから試合データを
取得できるようにして頂きました。

datapingpong.com

こちらの記事にて紹介されているAPIを使うことで、上記サイトで
分析された試合データをJSON形式で保存する事が出来ます。

ishigentech.hatenadiary.jp

このAPIを使い、今回はこちらのようなPythonスクリプトを作成して
データを取得、保存できるようにしました。

# -*- coding: utf-8 -*-

import requests
import json
from pykakasi import kakasi

game_id = input('Please input game id string from datapingpong.com: ')

game_data = requests.get("https://us-central1-datapingpong-vue.cloudfunctions.net/gameData?id={}".format(game_id))

game_data.encoding = game_data.apparent_encoding

game_data_json = game_data.json()

player1_jpn = game_data_json['player1']
player2_jpn = game_data_json['player2']

kakasi = kakasi()
kakasi.setMode("H", "a")
kakasi.setMode("K", "a")
kakasi.setMode("J", "a")
kakasi.setMode("r", "Hepburn")
conv = kakasi.getConverter()
player1_ascii = conv.do(player1_jpn)
player2_ascii = conv.do(player2_jpn)

save_file_name = player1_ascii + '_vs_' + player2_ascii + '.json'

with open(save_file_name, 'w') as f:
    json.dump(game_data_json, f, ensure_ascii=False, indent=4)

使い方としては、このようにコマンド実行して、データが欲しい試合の
試合IDを入力するだけです。
f:id:sy4310:20190902192632p:plain

試合IDについては、先に紹介したAPIについての記事を参照下さい。

追加データの自動計算

上記のスクリプトで取得できるJSONファイルは例えばこちらのように
なります。

github.com

そして、中に記録されるデータは下記の通りとなっています。

  • ポイント毎のラリー数
  • 選手の名前
  • 試合名
  • 試合についてのメモ
  • 第1ゲームがどちらの選手のサービスから始まったか
  • 各ポイントをどちらの選手が取ったか

データらしいのは、「ポイント毎のラリー数」、「第1ゲームがどちらの
選手のサービスから始まったか」、「各ポイントをどちらの選手が取ったか」
くらいですが、これだけでは流石に寂しい。そこで、追加で下記のような
データがあれば少しは分析しやすくなるかと考えたので、これらを自動計算
するツールを作ってみました。

  • 両選手の合計得点
  • 各選手の得点数
  • 各選手の獲得ゲーム数
  • 各得点時のサーバ/レシーバは誰か
  • サーブミスによる失点か
  • レシーブミスによる失点か
  • サービスエースによる得点か
  • レシーブエースによる得点か
  • 3球目攻撃による得点か
  • 4球目攻撃による得点か
  • 5球目攻撃による得点か
  • 6球目攻撃による得点か
  • 長いラリー(7回以上)を制しての得点か
  • 連続得点回数

保存されたJSONファイルからこれらのデータを自動計算する
下記のようなPythonスクリプトを作りました。

# -*- coding: utf-8 -*-

import tkinter as tk
import tkinter.filedialog as tkfd
import pandas as pd
import numpy as np
import json

class EngineeringFeatureAsCsv:
    def __init__(self):
        self.json_path_list     = None
        self.json_data          = None
        self.player_1_name      = None
        self.player_2_name      = None
        self.server_num         = None
        self.first_server_num   = None
        self.receiver_num       = None
        self.first_receiver_num = None
        self.sum_score_1        = 0
        self.sum_score_2        = 0
        self.sum_game_count_1   = 0
        self.sum_game_count_2   = 0
        self.consec_count_1     = 0
        self.consec_count_2     = 0
        self.df_data            = None
    
    def load_json_data(self):
        fType = [('JSON', '*.json')]
        self.json_path_list = tkfd.askopenfilenames(title='Select json files',
                                                    filetypes=fType)
        if not self.json_path_list:
            print('Select json files')
        else:
            for path in self.json_path_list:
                with open(path, 'r') as f:
                    self.json_data = json.load(f)
                    self.get_first_server_num()
                    self.get_player_name()
                    self.convert_json_to_df(path)
                    self.create_features()
                    self.save_as_csv(path)
    
    def get_player_name(self):
        if self.json_data:
            self.player_1_name = self.json_data['player1']
            self.player_2_name = self.json_data['player2']
    
    def get_first_server_num(self):
        if self.json_data:
            self.server_num = self.json_data['firstGameServer']
            self.first_server_num = self.server_num
            if self.server_num == 1:
                self.receiver_num = 2
                self.first_receiver_num = self.receiver_num
            else:
                self.receiver_num = 1
                self.first_receiver_num = self.receiver_num
    
    def convert_json_to_df(self, path):
        df_org = pd.read_json(path)
        df_drop_memo  = df_org.drop('memo', axis=1)
        df_drop_match = df_drop_memo.drop('matchName', axis=1)
        self.df_data  = df_drop_match
    
    def save_as_csv(self, path):
        data_name = (path.split('/')[-1]).split('.')[0]
        save_name = data_name + '.csv'
        self.df_data.to_csv(save_name, index=False, encoding='shift-jis')
    
    def set_server_receiver(self, index):
        self.server_array[index]   = self.server_num
        self.receiver_array[index] = self.receiver_num
        if (self.sum_score_1 + self.sum_score_2) % 2 == 0:
            if self.server_num == 1:
                self.server_num   = 2
                self.receiver_num = 1
            else:
                self.server_num   = 1
                self.receiver_num = 2
        if self.sum_score_1 == 0 and self.sum_score_2 == 0:
            if self.first_server_num == 1:
                self.server_num = 2
                self.first_server_num = self.server_num
                self.receiver_num = 1
                self.first_receiver_num = self.receiver_num
            else:
                self.server_num = 1
                self.first_server_num = self.server_num
                self.receiver_num = 2
                self.first_receiver_num = self.receiver_num
    
    def count_game(self, index):
        if self.sum_score_1 > self.sum_score_2:
            self.sum_game_count_1 += 1
        else:
            self.sum_game_count_2 += 1
        self.game_count_1_array[index] = self.sum_game_count_1
        self.game_count_2_array[index] = self.sum_game_count_2
    
    def set_serve_error(self, index, gpp, rc):
        if rc == 0:
            if gpp == 1:
                self.serve_error_2_array[index] = True
            else:
                self.serve_error_1_array[index] = True
    
    def set_serve_point_receive_error(self, index, gpp, rc):
        if rc == 1:
            if gpp == 1:
                self.serve_point_1_array[index]   = True
                self.receive_error_2_array[index] = True
            else:
                self.serve_point_2_array[index]   = True
                self.receive_error_1_array[index] = True
    
    def set_receive_point(self, index, gpp, rc):
        if rc == 2:
            if gpp == 1:
                self.receive_point_1_array[index]   = True
            else:
                self.receive_point_2_array[index]   = True
    
    def set_third_point(self, index, gpp, rc):
        if rc == 3:
            if gpp == 1:
                self.third_point_1_array[index]   = True
            else:
                self.third_point_2_array[index]   = True
    
    def set_fourth_point(self, index, gpp, rc):
        if rc == 4:
            if gpp == 1:
                self.fourth_point_1_array[index]   = True
            else:
                self.fourth_point_2_array[index]   = True
    
    def set_fifth_point(self, index, gpp, rc):
        if rc == 5:
            if gpp == 1:
                self.fifth_point_1_array[index]   = True
            else:
                self.fifth_point_2_array[index]   = True
    
    def set_sixth_point(self, index, gpp, rc):
        if rc == 6:
            if gpp == 1:
                self.sixth_point_1_array[index]   = True
            else:
                self.sixth_point_2_array[index]   = True
    
    def set_long_rally_point(self, index, gpp, rc):
        if rc >= 7:
            if gpp == 1:
                self.long_point_1_array[index]   = True
            else:
                self.long_point_2_array[index]   = True
    
    def count_score(self, index, gpp, rc):
        if gpp == 1:
            self.sum_score_1 += 1
            self.consec_count_1 += 1
            self.consec_count_2 = 0
        else:
            self.sum_score_2 += 1
            self.consec_count_2 += 1
            self.consec_count_1 = 0
        self.prev_point_player = gpp
        self.set_serve_error(index, gpp, rc)
        self.set_serve_point_receive_error(index, gpp, rc)
        self.set_receive_point(index, gpp, rc)
        self.set_third_point(index, gpp, rc)
        self.set_fourth_point(index, gpp, rc)
        self.set_fifth_point(index, gpp, rc)
        self.set_sixth_point(index, gpp, rc)
        self.set_long_rally_point(index, gpp, rc)
        self.score_1_array[index] = self.sum_score_1
        self.score_2_array[index] = self.sum_score_2
        self.game_count_1_array[index] = self.sum_game_count_1
        self.game_count_2_array[index] = self.sum_game_count_2
        self.consec_point_1_array[index] = self.consec_count_1
        self.consec_point_2_array[index] = self.consec_count_2
        # detect next game start
        sum_score_12 = self.sum_score_1 + self.sum_score_2
        if sum_score_12 >= 20:
            if abs(self.sum_score_1 - self.sum_score_2) == 2:
                self.count_game(index)
                self.sum_score_1 = 0
                self.sum_score_2 = 0
        else:
            if self.sum_score_1 >= 11 or self.sum_score_2 >= 11:
                self.count_game(index)
                self.sum_score_1 = 0
                self.sum_score_2 = 0
    
    def add_features_to_df(self):
        self.df_data['pointNum']  = self.point_num_array
        self.df_data['player1Score'] = self.score_1_array
        self.df_data['player2Score'] = self.score_2_array
        self.df_data['player1Game']  = self.game_count_1_array
        self.df_data['player2Game']  = self.game_count_2_array
        self.df_data['Server']       = self.server_array
        self.df_data['Receiver']     = self.receiver_array
        self.df_data['serveError1']  = self.serve_error_1_array
        self.df_data['serveError2']  = self.serve_error_2_array
        self.df_data['receiveError1']  = self.receive_error_1_array
        self.df_data['receiveError2']  = self.receive_error_2_array
        self.df_data['servePoint1']  = self.serve_point_1_array
        self.df_data['servePoint2']  = self.serve_point_2_array
        self.df_data['receivePoint1']  = self.receive_point_1_array
        self.df_data['receivePoint2']  = self.receive_point_2_array
        self.df_data['thirdPoint1'] = self.third_point_1_array
        self.df_data['thirdPoint2'] = self.third_point_2_array
        self.df_data['fourthPoint1'] = self.fourth_point_1_array
        self.df_data['fourthPoint2'] = self.fourth_point_2_array
        self.df_data['fifthPoint1'] = self.fifth_point_1_array
        self.df_data['fifthPoint2'] = self.fifth_point_2_array
        self.df_data['sixthPoint1'] = self.sixth_point_1_array
        self.df_data['sixthPoint2'] = self.sixth_point_2_array
        self.df_data['longPoint1'] = self.long_point_1_array
        self.df_data['longPoint2'] = self.long_point_2_array
        self.df_data['concecPoint1'] = self.consec_point_1_array
        self.df_data['concecPoint2'] = self.consec_point_2_array
    
    def create_features(self):
        self.get_point_player  = self.df_data['getPointPlayer'].values
        self.rally_count       = self.df_data['rallyCnt'].values
        # additional feature array
        self.point_num_array       = range(1, len(self.get_point_player)+1)
        self.score_1_array         = np.zeros(len(self.get_point_player))
        self.score_2_array         = np.zeros(len(self.get_point_player))
        self.game_count_1_array    = np.zeros(len(self.get_point_player))
        self.game_count_2_array    = np.zeros(len(self.get_point_player))
        self.server_array          = np.zeros(len(self.get_point_player))
        self.receiver_array        = np.zeros(len(self.get_point_player))
        self.serve_error_1_array   = np.zeros(len(self.get_point_player))
        self.serve_error_2_array   = np.zeros(len(self.get_point_player))
        self.receive_error_1_array = np.zeros(len(self.get_point_player))
        self.receive_error_2_array = np.zeros(len(self.get_point_player))
        self.serve_point_1_array   = np.zeros(len(self.get_point_player))
        self.serve_point_2_array   = np.zeros(len(self.get_point_player))
        self.receive_point_1_array = np.zeros(len(self.get_point_player))
        self.receive_point_2_array = np.zeros(len(self.get_point_player))
        self.third_point_1_array   = np.zeros(len(self.get_point_player))
        self.third_point_2_array   = np.zeros(len(self.get_point_player))
        self.fourth_point_1_array  = np.zeros(len(self.get_point_player))
        self.fourth_point_2_array  = np.zeros(len(self.get_point_player))
        self.fifth_point_1_array   = np.zeros(len(self.get_point_player))
        self.fifth_point_2_array   = np.zeros(len(self.get_point_player))
        self.sixth_point_1_array   = np.zeros(len(self.get_point_player))
        self.sixth_point_2_array   = np.zeros(len(self.get_point_player))
        self.long_point_1_array    = np.zeros(len(self.get_point_player))
        self.long_point_2_array    = np.zeros(len(self.get_point_player))
        self.consec_point_1_array  = np.zeros(len(self.get_point_player))
        self.consec_point_2_array  = np.zeros(len(self.get_point_player))
        for i, (gpp, rc) in enumerate(zip(self.get_point_player, self.rally_count)):
            self.count_score(i, gpp, rc)
            self.set_server_receiver(i)
        self.add_features_to_df()

if __name__ == "__main__":
    engi = EngineeringFeatureAsCsv()

    root = tk.Tk()
    root.withdraw()

    engi.load_json_data()

まずは、こちらのコマンドで実行します。
f:id:sy4310:20190901162906p:plain
その後、読み込みたいJSONファイルを選択するGUIが開くので、
対象のファイルを選択し、「開く」を押します。 f:id:sy4310:20190901164548p:plain
すると、このようなCSVファイルとして出力され、その中には
前述した各データが書きこまれている事が確認できます。
f:id:sy4310:20190901164715p:plain

ここまでに紹介したコードやサンプルデータは下記のGitHubリポジトリで
公開しています。

github.com

次の取り組み

とりあえず、今の時点でぱっと思い浮かぶ追加データを自動作成できるように
なりました。今後はいろいろ思考錯誤しながら、データを更に追加したり、
削除したりしていく事になるのでしょう。
次はこれらを実際に可視化して、分析に有効そうか検証していきたいので、
そのための可視化ツールみたいなものを作ってみたいと思います。