ishigenの技術ブログ

卓球 機械学習 競ぷろ

datapingpongに変態モードを追加しました

さらに本気(変態?)の人たちに向けて、機能を追加しました。
以下のツイートにあるように、本気の織部さんに応えられるよう全打球を記録できるようにしました。

どんなデータになるのかは以下のリンクから実際のデータをご覧ください。2020年の全日本の男子決勝のデータです。

datapingpong.com

追加した機能

  1. 全打球情報(打球位置・フォアorバック・打法・コース・インorアウト)を記録可能に
  2. 打球情報を基にしたグラフを追加
  3. データの検索ページの追加(以下画像)

f:id:ishigentech:20200119111811p:plain
上記画像の検索ボタンからデータの検索ページへ

f:id:ishigentech:20200119113408p:plain
データの検索ページ

このページではユーザーが独自のパターンを設定すると、そのパターンの一覧と得点率がわかります。

サーブのコースや種類を限定し、得点数を表示するのはもちろん、

  • 中陣vs中陣のラリーになった場合の得点率
  • ミドル前にストップレシーブ→ダブルストップ→チキータからのラリーの得点率

などなど、組み合わせ次第で様々なパターンで集計することができます。

ちなみに全日本男子決勝をご覧になった方は気になるであろう、宇田選手のフォア前サイドへのサーブからの得点数は 11/13 で、得点率85%でした。

このパターンの組み合わせの中から試合を理解するのに有効な指標を見つけられるかどうかが、データを考察する人の腕の見せ所です。

データの入力について

データの入力は分析画面右上のproモードボタンをタップしていただくとおわかりのように、全打球ボタン入力です。(笑)

1打球あたり4回のタップが必要です。

このデータを作成するのに1試合3-4時間はかかります。もしやってみようと思う人がいれば、覚悟して取り組んでください。

自分でやろうと思う人はかなり少ないでしょうが、継続的にデータを作成できればなかなか価値があるデータになると思います。

まとめ

自動化しないとつらい。

datapingpongのapiを作りました

先日apiとかねえの?って聞かれたので調べてみると、Cloud Functionsで簡単に実装できそうだったので作ってみました。 ※エンジニア向けの記事になりますのでご注意ください。

個人のブログで申し訳ありませんが、使用方法の説明をします。

  • アクセス先
https://us-central1-datapingpong-vue.cloudfunctions.net/gameData?id={game_id}

※ game_idはdatapingpongで生成する共有URLの後方20桁の文字列です。
https://datapingpong.com/#/share/9rHCYjMiDulGCB2o3bTH → 9rHCYjMiDulGCB2o3bTH
また、試合データの公開設定が「リンクを知っている人に公開する」となっているデータのみ対象です。

  • HTTPメソッド

GET

JSON

正常時のレスポンス項目 2019/8/21 時点

key value 備考
getPointPlayer ポイントごとにどちらのプレイヤーが得点したかを表す 1:player1の得点、2 : player2の得点
rallyCnt ポイントごとに何本ラリーが続いたかを表す  
player1 player1の選手名  
player2 player2の選手名  
matchName 試合名  
firstGamseServer 1ゲーム目のサーバーを表す 1:player1がサーブ、2:player2がサーブ
memo 試合のメモ  

game_idが不正及び非公開のデータとなっている場合のレスポンス項目

key value
message 公開されているデータがありません

サーバー側でエラーが発生した場合のレスポンス項目

key value
message エラーが発生しました。お問い合わせください。


以上がレスポンス内容です。ここから減ることはないと思いますが、レスポンス項目が増えることはあると思います。増えればその都度、更新していきます。

最後にpythonでデータを取得するコードを例として記載します。

import requests
import json
game_id = "9rHCYjMiDulGCB2o3bTH"
data = requests.get("https://us-central1-datapingpong-vue.cloudfunctions.net/gameData?id={}".format(game_id))
data = json.loads(data.text)
print(data)

追加して欲しい返却項目があれば、お知らせください。
12.5万/月を超えるアクセスが発生すると、利用停止となります。あんまりいじめないでください。

半年前に作ったwebサイトを壊した話

半年前に作ったwebサイトを1から作りなおしてリニューアルしました。

www.datapingpong.com


デザインほぼ一緒やんけという突っ込みは一旦引っ込めてください。。。

見た目はほぼ同じですが、できることは増えています。

  • ユーザーが追加した得点パターンの集計
  • スコアの変化と同時にグラフを表示
  • スコアと集計データのリアルタイムでのシェア

以上のようなことができるようになっています。

以前のバージョンを公開してから感じたのは、自分で集計するのはめんどくさいけど、集計されたデータなら興味がある人はそれなりにいるということです。

当初の目的は自分の試合で使ってもらい、強くなってもらうことでした。しかし、トップ選手の戦術を分析することのほうが需要があることを知りました。

今後はトップ選手の試合のデータを通して、戦術解釈って面白くね?ってところをもう少し自分でも発信していきたいです。

さらに、せっかくリアルタイムでのシェア機能があるので、YouTubeなどで配信されている試合の集計データをリアルタイムでシェアすることもやっていきたいです。

スコアをライブ配信するサービスはすでにありますが、その卓球特化番みたいなポジションになれればいいかなと思っています。誰でも配信できる仕組みになっているので、独自の戦術解釈を配信してくれる人が出てくると盛り上がるかなあとも思います。

他にも細かな機能は使いやすいものになっているはずなので、触ってみてください。

技術選定

旧バージョンは以下のような作りでした。

新バージョンで使ったフレームワーク、サービスは以下の通りです。

フロント

Vue.js
ミーハーなので流行りのVue.jsで作りました。Vue.jsはおろかJavascriptすらほぼ初心者だったのでいろいろと勉強しました。以前のバージョンはほとんどの処理をサーバーサイドで行っていたので、全く異なる作りになりました。

Vue.jsなり最近のjavascriptフレームワークを使わないと、スコアの変化と同時にグラフを表示する処理は実現が困難だったと思います。ユーザーがラベルを追加する処理も同様です。

サーバーサイド

Firebase
Firebaseじゃないとリアルタイムでシェアするという機能も実現するのが大変だったと思います。

認証周りも任せっきりになったので、セキュリティも自分で管理するより安全になりました。また、デプロイも「firebase deploy」で終わってしまうので、sshで接続してみたいなことをしなくていいので非常に簡単です。

開発時間・費用

開発時間

130時間くらい

正確に計ったわけではありませんが、5月の下旬からはじめて最低でも週20時間はやっていたので、このくらいの時間はかかってると思います。

1から作り直しましたが、Firebaseを使用、デザインはほとんど同じということもあり100時間ちょいでできたと思っています。

費用

0円

Firebaseの無料枠で開発環境、本番環境を構築してるので、費用はかかっていません。

とはいえ独自ドメインで公開し、独自ドメインのメールアドレスを取得しているので、数百円/月はかかります。


今後も実現したい機能はすでにいくつかあるので、ぼちぼち開発していきたいと思います。

サーブ・レシーブ得点率から見るTリーグの勢力図(個人編)

今回は前回の記事で取得したデータを用いて、サーブ・レシーブ得点率を可視化していきます。

ishigentech.hatenadiary.jp

サーブ時の得点率についてはランキング形式で公式ページに掲載されています。

tleague.jp

ただこのランキングはシーズンを通して13試合以上の試合に出場しないと、集計対象外になってしまいます。(21試合 ☓ 0.6以上が条件のため)より集計対象を広げたランキングを見てみたいと思います。


また、サーブ得点率女子一位の石川選手と男子一位の張本選手には5%ほどの差があります。女子が全体的にサーブ得点率が高くなりやすいのか、それともただ石川選手が鬼強いだけなのかがよくわかりません。この辺りも検証したいと思います。

目次

  1. サーブ得点率ランキング
  2. レシーブ得点率ランキング
  3. 基本統計量
  4. グラフにしてみる

サーブ得点率ランキング

男子

対象選手はサーブ機会が200以上、つまりTリーグの出場前試合を通して200回以上サーブを出した選手としています。また、ダブルスは除外していますが、ヴィクトリーマッチは含まれています。

それぞれの列の説明は以下の通りです。
serve_point : サーブ得点数
serve : サーブ機会数
serve_winrate : サーブ時の得点率

f:id:ishigentech:20190310150432p:plain

公式ページのランキングと比較しても、サーブ得点数、サーブ機会数がほとんど一致しているので、データは正しいものとして進めていきます。

6試合にしか出場しておらず、公式ランキングでは集計対象外だった林選手が張本選手に次いで二位となっています。

女子

f:id:ishigentech:20190310151218p:plain

こちらも公式ページでは集計対象外だった早田選手が二位となっています。11試合に出場しているのでたまたま運が良かったわけではなく、実力通りの数値になっていると思います。

レシーブ得点率ランキング

公式ページには掲載されていませんが、レシーブ得点率も集計しました。

男子

サーブ同様、対象選手はレシーブ機会数が200以上の選手にします。

それぞれの列の説明は以下の通りです。
receive_point : レシーブ得点数
receive : レシーブ機会数
receive_winrate : レシーブ時の得点率

f:id:ishigentech:20190310151635p:plain

張本選手が一位なのはもうさすがとしか言いようがありませんが、サーブ時得点率ではそこまで上位ではなかった森園選手が二位となっているのが意外でした。ダブルスのレシーブだけでなく、シングルのレシーブ力もかなり高いことが窺えます。

女子

f:id:ishigentech:20190310152121p:plain

ツートップが離れているのはサーブ時の得点率と変わらずという結果になりました。トップと最下位の差がけっこう大きいなとも思います。

基本統計量

女子は差が大きかったような気がしたので、それぞれの基本統計量だけ見ておきます。

f:id:ishigentech:20190310152842p:plain

平均値にはほとんど差がありませんが、女子のほうが少しだけサーブが有利なのかもしれません。標準偏差は女子のほうが大きくなっており、ばらつきが大きい、つまり個々の選手の実力差が男子と比較すると大きいとは言えそうです。

グラフにしてみる

男女まとめ

f:id:ishigentech:20190310153420p:plain

女子のほうがばらばらになっていて、サーブ得点率とレシーブ得点率の相関関係が高そうに見えます。

男子

f:id:ishigentech:20190310153435p:plain

男子はサーブ得点率とレシーブ得点率は相関関係がほとんどありません。

森園選手のようにレシーブの得点率が高く、サーブの得点率はさほど高くない選手がいたり、陳選手のようにサーブの得点率が高く、レシーブの得点率はさほど高くない選手もいます。

女子

f:id:ishigentech:20190310153445p:plain

男子に比べラリー勝負になる展開が多く、ラリーに強い選手の両方の得点率が高いという構図になっていそうです。

わかったこと

  1. 石川選手が鬼強く、サーブ得点率が高くなっていた
  2. 早田選手も鬼強かった
  3. 公式ランキングでは集計対象外の選手も見てみると面白かった
  4. レシーブ得点率も公式ページに掲載してほしい

こんなところです。今回は以上です。今後はチームごとにやるとか、得点推移に着目するとか、気が向けばやってみます。

Tリーグの試合結果をスクレイピングで収集する

Tリーグのプレーオフもいよいよ来週に迫ってきました。今回はTリーグのこれまでの試合のデータをスクレイピングで収集してみました。pythonスクレイピングしてmysqlで作成したDBに突っ込むまでやってみます。

なぜこんなことをやるのかというと、もっと興味深いデータが眠っていると思ったからです。今でもランキング形式で多少のデータが載っていますが、もっと細かくできるんじゃないかと。API用意しろとか贅沢は言わないので、このくらいのアクセスは許して欲しいところです。

追記 2019-03-24

選手のデータを収集するプログラムもgithubに追加しました。

環境

言語 Python

説明するまでもなく。BeautifulSoup,requestsライブラリとかも同様に。

データベース mysql

dockerですぐに使えそうだったのでmysqlにしました。INSERT文でデータを作成するだけなのでどのデータベースでも同じように動くはずです。

実装

github.com

ここに置いています。収集したデータを配布する行為は会員規約での禁止行為*1なので、ソースコードだけ置いておきます。一回動かしても100回弱くらいのアクセスなので、サーバーに高い負荷をかけることはないと考えています。とはいえ頻繁に動かすのもどうかとは思うので、自己責任で動かしてください。

解説

最終的には以下のような3つのテーブルにデータを突っ込むイメージです。

team_match

チームでの対戦成績です。
ホームチーム、アウェイチーム、ホームチームの団体でのポイント、アウェーチームの団体でのポイント、来場者数、性別が入ります。

game

各試合での対戦成績です。
ホームチームの選手、アウェーチームの選手、ホームチームの選手の獲得セット数、アウェーチームの選手の獲得セット数が入ります。

point

各ラリーでのポイントです。
セット数、ポイント数、ポイントの得失、サーブ権、ホームチームタイムアウト直後か、アウェーチームがタイムアウト直後かどうかが入ります。


まずは以下のSQLを実行して、データベースとテーブルを作成します。

CREATE DATABASE TLeagu;

CREATE TABLE team_match (
match_id VARCHAR(20) ,
date DATE ,
home VARCHAR(100) ,
away VARCHAR(100) ,
home_point INT ,
away_point INT ,
visitors INT ,
sex INT ,
PRIMARY KEY(match_id)
);

CREATE TABLE game (
match_id VARCHAR(20) ,
game_id INT ,
home_player1 VARCHAR(100) ,
home_player2 VARCHAR(100) ,
away_player1 VARCHAR(100) ,
away_player2 VARCHAR(100) ,
home_point INT ,
away_point INT ,
PRIMARY KEY(match_id,game_id)
);

CREATE TABLE point (
match_id VARCHAR(20) ,
game_id INT ,
set_id INT ,
point_id INT ,
point INT ,
serve INT ,
home_timeout_fag INT ,
away_timeout_fag INT ,
PRIMARY KEY(match_id,game_id,set_id,point_id)
);


次に必要な関数を定義していきます。

get_link

各試合へのリンクを配列にして取得します。

get_match_recorde

team_matchテーブルに対応したデータを取得します。

get_game_recorde

gameテーブルに対応したデータを取得します。

get_point_record

pointテーブルに対応したデータを取得します。

judge_end_set

ポイントの得失点からセットが終了したかどうかを判定します。

# func.py
import requests
from bs4 import BeautifulSoup
import re

basicurl = "https://tleague.jp/match/"

def get_link():
  link_list = []
  for month in ["201810","201811","201812","201901","201902"]:
    url = basicurl + "?season=2018&month=" + month + "&mw="
    response = requests.get(url)
    soup = BeautifulSoup(response.text, "html.parser")
    matchlist = soup.find(class_="ui-match-table")
    for i in matchlist.find_all(class_="match-men"):
      for inner in i.find_all(class_="inner"):
        link = inner.find("a").get("href")
        link_list.append(link)
    for i in matchlist.find_all(class_="match-women"):
      for inner in i.find_all(class_="inner"):
        link = inner.find("a").get("href")
        link_list.append(link)

  return link_list


def get_match_recorde(soup):
  match = soup.find(class_="match-info")
  home = match.find(class_="home").get_text()
  away = match.find(class_="away").get_text()
  point = match.find_all(class_="cell-score")
  home_point = point[0].get_text()
  away_point = point[1].get_text()
  itemclass = match.find(class_="item-spec").find_all("li")
  visitors = itemclass[2].get_text()
  visitors = re.sub(r"[^0-9]", "", visitors)

  return [home, away,home_point, away_point, visitors]


def get_game_recorde(soup):
  table = []
  
  match = soup.find(class_="cell-game")
  home_point = 0
  away_point = 0
  game = []
  for i, col in enumerate(match.find_all(class_="col")):
    if i % 3 == 1:
        # point col
        home_point = col.get_text()[0]
        away_point = col.get_text()[2]
        continue
    a_list = col.find_all("a")
    if len(a_list) >= 2:
        for a in a_list:
            game.append(a.get_text().replace("\n",""))
    else:
        for a in a_list:
            game.append(a.get_text().replace("\n",""))
            game.append(None)
    if i % 3 == 2:
      # reset
      game.append(home_point)
      game.append(away_point)
      table.append(game)
      game = []

  return table

def judge_end_set(point, is_final):
  home = 0
  away = 0
  if is_final:
    home = 6
    away = 6
  for i in point:
    if i == "1":
      home += 1
    else:
      away += 1
      
    if home >= 11 or away >= 11:
      if (home - away) ** 2 >= 4:
        return True
      
  return False

def get_point_record(soup):
  match = []
  match_serve = []
  match_timeout = []
  home_timeout, away_timeout = 0, 0
  is_final = False
  for i in soup.find_all("div",class_="match-game"):
    game = []
    game_serve = []
    game_timeout = []
    for w in i.find_all(class_="wrap-table"):
      point = []
      serve = []
      timeout = []
      tmp = ""
      for k_ind,k in enumerate(w.find_all("td")):
        if k_ind == 0:
          if k.get_text() == "1" or k.get_text() == "0":
            point.append(k.get_text())
          else:
            point.append(str(int(k.get_text()) - 6))
            is_final = True
        else:
          if k.get_text() == "T":
              home_timeout = 1
              continue
          elif re.match("[0-9]", k.get_text()) == None:
              away_timeout = 1
              continue
          elif tmp == k.get_text():
            point.append("0")
          else:
            point.append("1")
        timeout.append([home_timeout, away_timeout])
        home_timeout, away_timeout = 0, 0
        tmp = k.get_text()
        if k.get("class") != None:
          serve.append(1)
        else:
          serve.append(2)

        if judge_end_set(point, is_final):
          is_final = False
          game.append(point)
          game_serve.append(serve)
          game_timeout.append(timeout)
          break
    match.append(game)
    match_timeout.append(game_timeout)
    match_serve.append(game_serve)

  return match, match_timeout, match_serve


そして実際にwebページにアクセスして実行します。処理は以下のとおりです。

  1. mysqlへのコネクションを作成
  2. func.pyで定義した関数を順番に実行
  3. 結果からINSERT文を作成
  4. DBに登録
# Tleagu.py
import requests
from bs4 import BeautifulSoup
import re
import mysql.connector
from func import get_game_recorde,get_link,get_match_recorde,get_point_record

conn = mysql.connector.connect(
    host = '172.17.0.3',#localhost
    port = 3306,
    user = '',
    password = '',
    database = 'TLeagu',
)

cur = conn.cursor()

basicurl = "https://tleague.jp"
link = get_link()
# link = ["/match/20181024m01"]
try:
    cur.execute("DELETE FROM team_match;")
    cur.execute("DELETE FROM game;")
    cur.execute("DELETE FROM point;")
    for i in link:
        url = basicurl + i
        print(url)
        response = requests.get(url)
        soup = BeautifulSoup(response.text, "html.parser")
        sex = 0 if i[-3] == "m" else 1
        date = i[-11:-3]
        match_id = i[7:]
        all_match = get_match_recorde(soup)
        all_match_game = get_game_recorde(soup)
        all_match_point, all_match_timeout, all_match_serve = get_point_record(soup)
        
        # inesrt match table
        execute_str = "INSERT INTO team_match VALUES ('{}','{}','{}','{}',{},{},{},{});".format(
            match_id, date, all_match[0], all_match[1], int(all_match[2]), int(all_match[3]), int(all_match[4]), int(sex)
        )
        cur.execute(execute_str)

        # insert game table
        for a_game_index, a_game in enumerate(all_match_game):
            if a_game[1] != None:
                execute_str = "INSERT INTO game VALUES ('{}',{},'{}','{}','{}','{}',{},{});".format(
                    match_id, a_game_index, a_game[0], a_game[1], a_game[2], a_game[3], int(a_game[4]), int(a_game[5])
                )
            else:
                execute_str = "INSERT INTO game VALUES ('{}',{},'{}',NULL,'{}',NULL,{},{});".format(
                    match_id, a_game_index, a_game[0], a_game[2], int(a_game[4]), int(a_game[5])
                )
            cur.execute(execute_str)

        # insert point table
        for a_match_point_index, a_match_point in enumerate(all_match_point):
            for a_set_point_index, a_set_point in enumerate(a_match_point):
                for a_point_index, a_point in enumerate(a_set_point):
                    execute_str = "INSERT INTO point VALUES ('{}',{},{},{},{},{},{},{});".format(
                        match_id, a_match_point_index, a_set_point_index, a_point_index,
                        int(a_point), int(all_match_serve[a_match_point_index][a_set_point_index][a_point_index]),
                        all_match_timeout[a_match_point_index][a_set_point_index][a_point_index][0],
                        all_match_timeout[a_match_point_index][a_set_point_index][a_point_index][1]
                    )
                    cur.execute(execute_str)
except:
    print(all_match)
    print(all_match_game)
    print(all_match_point)
    conn.rollback()
    raise

conn.commit()


ここまで実行すると、DBにデータが作成されていることが確認できると思います。

次回はこの取得したデータを使って分析してみたいと思います。

*1:https://fan.tleague.jp/terms/ 第4章 第10条 1.に該当

とあるwebサイトを作った話

ただの宣伝記事です。

こんなサイトを作ってみました。

www.datapingpong.com

使い方は説明するまでもなく、得失点の内容に応じてボタンをぽちぽちするだけです。

まだまだできることは限られていますが、昔の自分なら使うかどうかを考えた結果、作ってみることにしました。

当時は似たようなことを紙で集計していたので、それに比べればかなり使いやすくなったと自己満足しています。


特に説明することもないので、ありがちな質問に回答しておきます。

・こんなの意味あるの?
ただ集計しているだけなので、使うだけでは意味はありません。意味を持たせるのは使う人次第だと思います。何かとデータを見るのが好きで、卓球にそれなりに時間をかけている人なら、意味はあるんじゃないかと。

・誰も使わないのでは?
わざわざ自分で試合の反省をし、かつそれを数値で表したい人なんてほとんどいないでしょう。現段階では日本で10人の役に立てばいいかなと思っています。

・何を目指しているの?
『さいきょうのじんこうちのう』ってやつが欲しいです。人間が獲得できる経験値を越えたその先に何があるのか見てみたいわけです。


ちなみに現在は人工知能だなんてものとはほど遠く簡単な処理しかしていません。実現できそうなものから順にやってけばいいかなと。いずれはもっと多くの人に役立つものにしていきます。飽きない限りもっともっと面白くなるので、たまには見てみてください。

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

今回は前回の続きという形です。

ishigentech.hatenadiary.jp


前回は可視化する部分でグラフにプロットしていましたが、わかりにくいと思うので、GIF動画を作ってみます。

下記ページを参考にさせていただきました。
stmind.hatenablog.com

作成した動画

7つのスイングを重ねてGIF動画にしています。

コードについて


importと後々使う変数の定義

import json
import numpy as np
import matplotlib.pyplot as plt
import os
from pylab import *
from PIL import Image, ImageDraw
import numpy as np
import matplotlib.pyplot as plt

#全表示
#pairList = np.array([0,1,1,2,2,3,3,4,1,5,5,6,6,7,1,8,8,9,9,10,1,11,11,12,12,13,0,14,14,16,0,15,15,17])
#目と耳削除
pairList = np.array([0,1,1,2,2,3,3,4,1,5,5,6,6,7,1,8,8,9,9,10,1,11,11,12,12,13])
pairList = pairList.reshape(int(len(pairList) / 2), 2)
index = np.array([139,208,277,343,415,487,558])
colors = [(255.,     0.,    85.),
          (255.,     0.,     0.),
          (255.,    85.,     0.), 
          (255.,   170.,     0.), 
          (255.,   255.,     0.), 
          (170.,   255.,     0.), 
          (85.,   255.,     0.), 
          (0.,   255.,     0.), 
          (0.,   255.,    85.), 
          (0.,   255.,   170.), 
          (0.,   255.,   255.), 
          (0.,   170.,   255.), 
          (0.,    85.,   255.), 
          (0.,     0.,   255.), 
          (255.,     0.,   170.), 
          (170.,     0.,   255.), 
          (255.,     0.,   255.), 
          (85.,     0.,   255.)]

ファイルを開いて変数に格納

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)

GIF動画を作成する関数の定義

def generateGIF(swing, path):
    images = []
    xlim = int(np.max(swing[:, :, :, 0]))
    ylim = int(np.max(swing[:, :, :, 1]))
    for i in range(len(swing[0, :, :, :])):
        im = Image.new('RGB', (xlim, ylim))#, (128, 128, 128))
        draw = ImageDraw.Draw(im)
        for d in swing:
            for pair in pairList:
                pt1 = (int(d[i, pair[0], 0]), int(d[i, pair[0], 1]))
                c1 = d[i, pair[0], 2]
                pt2 = (int(d[i, pair[1], 0]), int(d[i, pair[1], 1]))
                c2 = d[i, pair[1], 2]
                # 信頼度0.0の関節は無視
                if c1 == 0.0 or c2 == 0.0:
                    continue
                # 関節の描画
                color = tuple(list(map(int, colors[pair[0]])))        
                draw.line((pt1, pt2), fill=color, width=2)
                for line in range(1, 3):
                    draw.line((0,int(ylim/3)*line, xlim, int(ylim/3)*line), fill=(50,50,50), width=1)
                for line in range(1, 3):
                    draw.line((int(xlim/3)*line, 0, int(xlim/3)*line, ylim), fill=(50,50,50), width=1)
        images.append(im)
    images[0].save(path,
                   save_all=True, append_images=images[1:], optimize=False, duration=500, loop=0)

全体のデータからスイング部分を切り出し、両骨盤の平均位置を原点に調整して、GIF動画を作成

swingperframe = 20
swing = np.array([])
for i in index:
    swing = np.append(swing, data[i:i+swingperframe,:,:])
swing = swing.reshape(len(index),swingperframe, 18, 3)
    
for i, ivalue in enumerate(swing):
    for k, kvalue in enumerate(ivalue):
        originx = np.mean([kvalue[8, 0], kvalue[11, 0]])
        originy = np.mean([kvalue[8, 1],kvalue[11, 1]])
        swing[i, k, :, 0] -= originx
        swing[i, k, :, 1] -= originy
        for t, tvalue in enumerate(kvalue):
            if tvalue[2] == 0:
                swing[i, k, t, 0] = 0
                swing[i, k, t, 1] = 0
        
swing[:, :, :, 0] -= np.min(swing[:, :, :, 0])
swing[:, :, :, 1] -= np.min(swing[:, :, :, 1])

generateGIF(swing, 'ファイル名.gif')


以上です。やってることは簡単なことしかやっていませんが、グラフなんかよりは見やすくなったと思います。