- 作者:鳥越 規央
- 出版社/メーカー: 化学同人
- 発売日: 2015/05/20
- メディア: 単行本(ソフトカバー)
目次
背景・目的
最近では深層学習などを駆使したスポーツのデータ分析が非常に
盛り上がっていますが、自分も趣味である卓球をテーマに何か
データ分析をしてみたくなりました。
その第一歩として、今回は限られたデータから追加でデータを
自動計算させる事にトライしてみたので紹介します。
分析の目標
卓球のデータ分析をするに当たり、自分が今一番興味があるのは
「試合の流れの可視化」です。今どういう状況で試合が進んでいる
のか、ここから先はどういう流れになりそうかなどを定量的に
示せないかを第一の目標にしていきます。
制約条件
スポーツのデータ分析でよく行われているのは、例えばこちらの
ように、テニスコート全体を捉える角度から試合動画を撮り、
そこから選手やボールを検出してトラッキングするといったもの
があります。
しかしながら、一般的な卓球の試合ではこのような位置にカメラを
設置する事は難しく、大抵の場合はこちらのようにプレイヤーの
後ろ側から捉えるような位置になる場合が多いです。
このような場合、卓球台上でのボールの位置を高精度に追う事は
難しそうです。なので今回は、ボールの位置や軌道、球種といった
ようなデータは取れないという制約を設け、それ以外のデータで
どこまで良い分析が出来そうかを探ってみる事にします。
サンプルデータの入手
データ分析をするには、肝心のデータが無ければ始まりません。
何かサンプルになるデータはないかと思っていたところ、
@goes425さんのご厚意で下記サイトから試合データを
取得できるようにして頂きました。
こちらの記事にて紹介されているAPIを使うことで、上記サイトで
分析された試合データをJSON形式で保存する事が出来ます。
この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を入力するだけです。
試合IDについては、先に紹介したAPIについての記事を参照下さい。
追加データの自動計算
上記のスクリプトで取得できるJSONファイルは例えばこちらのように
なります。
そして、中に記録されるデータは下記の通りとなっています。
- ポイント毎のラリー数
- 選手の名前
- 試合名
- 試合についてのメモ
- 第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()
まずは、こちらのコマンドで実行します。
その後、読み込みたいJSONファイルを選択するGUIが開くので、
対象のファイルを選択し、「開く」を押します。
すると、このようなCSVファイルとして出力され、その中には
前述した各データが書きこまれている事が確認できます。
ここまでに紹介したコードやサンプルデータは下記のGitHubリポジトリで
公開しています。
次の取り組み
とりあえず、今の時点でぱっと思い浮かぶ追加データを自動作成できるように
なりました。今後はいろいろ思考錯誤しながら、データを更に追加したり、
削除したりしていく事になるのでしょう。
次はこれらを実際に可視化して、分析に有効そうか検証していきたいので、
そのための可視化ツールみたいなものを作ってみたいと思います。