【Python】サッカーのトラッキングデータを可視化する

今回は、選手やボールの動きを記録したサッカーのトラッキングデータをpythonで可視化する方法を紹介します。

出来上がりのgif動画がこちらです。

完成版のコードはこちらです。

#ライブラリをインストールする
import numpy as np
import pandas as pd
from matplotlib import animation
from matplotlib import pyplot as plt
from mplsoccer import Pitch
from IPython import display

# Homeチームのデータをロードする
LINK1 = ('https://raw.githubusercontent.com/metrica-sports/sample-data/master/'
         'data/Sample_Game_1/Sample_Game_1_RawTrackingData_Home_Team.csv')
df_home = pd.read_csv(LINK1, skiprows=2)
df_home.sort_values('Time [s]', inplace=True)

# Awayチームのデータをロードする
LINK2 = ('https://raw.githubusercontent.com/metrica-sports/sample-data/master/'
         'data/Sample_Game_1/Sample_Game_1_RawTrackingData_Away_Team.csv')
df_away = pd.read_csv(LINK2, skiprows=2)
df_away.sort_values('Time [s]', inplace=True)

# カラムを整形する関数を作成する
def set_col_names(df):
    cols = list(np.repeat(df.columns[3::2], 2))
    cols = [col+'_x' if i % 2 == 0 else col+'_y' for i, col in enumerate(cols)]
    cols = np.concatenate([df.columns[:3], cols])
    df.columns = cols

# 作成した関数でHome、Awayデータのカラムを整形する
set_col_names(df_home)
set_col_names(df_away)

# 秒数を指定したデータフレームを複製する
df_home = df_home[(df_home['Time [s]'] >= 0) & (df_home['Time [s]'] < 10)].copy()
df_away = df_away[(df_away['Time [s]'] >= 0) & (df_away['Time [s]'] < 10)].copy()

# Homeのデータセットからボールの座標カラムを抽出したデータフレームを複製する
df_ball = df_home[['Period', 'Frame', 'Time [s]', 'Ball_x', 'Ball_y']].copy()

# ボールのデータフレームのカラム名を変更する
df_ball.rename({'Ball_x': 'x', 'Ball_y': 'y'}, axis=1, inplace=True)

# Home、Awayのデータフレームからボールの座標カラムを削除する
df_home.drop(['Ball_x', 'Ball_y'], axis=1, inplace=True)
df_away.drop(['Ball_x', 'Ball_y'], axis=1, inplace=True)

# 横持ちから縦持ちへデータを整形する関数を作成する
def to_long_form(df):
    df = pd.melt(df, id_vars=df.columns[:3], value_vars=df.columns[3:], var_name='player')
    df.loc[df.player.str.contains('_x'), 'coordinate'] = 'x'
    df.loc[df.player.str.contains('_y'), 'coordinate'] = 'y'
    df = df.dropna(axis=0, how='any')
    df['player'] = df.player.str[6:-2]
    df = (df.set_index(['Period', 'Frame', 'Time [s]', 'player', 'coordinate'])['value']
          .unstack()
          .reset_index()
          .rename_axis(None, axis=1))
    return df

# 作成した関数でHome、Awayのデータを整形する
df_home = to_long_form(df_home)
df_away = to_long_form(df_away)

# 描画する領域全体(figure)と個別のプロットを描画するする領域(ax)を定義する
fig, ax = plt.subplots(figsize=(16, 10.4))

# mplsoccerライブラリのPitchメソッドを使って、ピッチを定義する
pitch = Pitch(pitch_type='metricasports', pitch_width=68, pitch_length=105, goal_type='box', goal_alpha=1)

# axにピッチを描画する
pitch.draw(ax)

# ax.plotでLine2Dオブジェクトを定義する
ball, = ax.plot([], [], 'o', ms=6, mfc='w', mec='black', mew=3)
home, = ax.plot([], [], 'o', ms=10, mfc='#7f63b8', mec='black')
away, = ax.plot([], [], 'o', ms=10, mfc='#b94b75', mec='black')

# グラフ更新用関数を作成する
def update(i):
    # ボールのデータセットからi行目のフレームidを取得する
    frame = df_ball.iloc[i, 1]
    # 選手、ボールのx座標、y座標をセットする
    ball.set_data(df_ball.loc[df_ball.Frame == frame, 'x'],
                  df_ball.loc[df_ball.Frame == frame, 'y'])
    away.set_data(df_away.loc[df_away.Frame == frame, 'x'],
                  df_away.loc[df_away.Frame == frame, 'y'])
    home.set_data(df_home.loc[df_home.Frame == frame, 'x'],
                  df_home.loc[df_home.Frame == frame, 'y'])
    return ball, away, home

# アニメーションを定義する
anim = animation.FuncAnimation(fig, update, frames=len(df_ball), interval=50, blit=True)

# アニメーションを表示する
plt.show()

# アニメーションをgif形式で保存する
w = animation.PillowWriter(fps=20)
anim.save('animation.gif', writer=w)

なお、今回作成したコードは、サッカーのプレーデータを可視化するためのライブラリmplsoccerのサイトに掲載されているサンプルコードをベースに、日本語の解説を加えつつ、初心者向けに分かりやすくしたものになります。元のサンプルコードは以下を参照ください。
https://mplsoccer.readthedocs.io/en/latest/gallery/pitch_plots/plot_animation.html

それでは、順番にコードを解説していきます。

Pythonライブラリをインストールする

# ライブラリをインストールする
import numpy as np
import pandas as pd
from matplotlib import animation
from matplotlib import pyplot as plt
from mplsoccer import Pitch

今回紹介する方法では、以下のライブラリを使用します。

  • numpy・・・効率的に数値計算を行うために使用するライブラリ。
  • pandas・・・データの読み込み、抽出などの操作に使用するライブラリ。
  • matplotlib・・・グラフの描画やデータの可視化に使用するライブラリ。
  • mplsoccer・・・サッカーのプレーデータを可視化するのに使用されるライブラリ。

今回、mplsoccerはピッチを描くために使用しますが、他にも様々な可視化機能を持った便利なライブラリです。
MITライセンスのため、github上にも公開されています。
https://github.com/andrewRowlinson/mplsoccer

ゲームのトラッキングデータを取得する

# Homeチームのcsvデータの保存先URLを変数に格納する
LINK1 = ('https://raw.githubusercontent.com/metrica-sports/sample-data/master/'
         'data/Sample_Game_1/Sample_Game_1_RawTrackingData_Home_Team.csv')

# Homeチームのcsvデータをpandasデータフレームとして読み込む
df_home = pd.read_csv(LINK1, skiprows=2)

# Homeチームのデータフレームをカラム「Time [s]」の昇順に並べ替える
df_home.sort_values('Time [s]', inplace=True)

# 上記と同じ手順でAwayチームのデータフレームも作成する
LINK2 = ('https://raw.githubusercontent.com/metrica-sports/sample-data/master/'
         'data/Sample_Game_1/Sample_Game_1_RawTrackingData_Away_Team.csv')
df_away = pd.read_csv(LINK2, skiprows=2)
df_away.sort_values('Time [s]', inplace=True)

今回使用するデータは、Metrica Sports B.V社から提供されているサッカーのトラッキングデータのサンプルです。
Metrica Sports B.V社は、サッカー、アメフト、テニスなど様々なスポーツのトラッキングデータ解析ソフトを手掛ける会社です。
サンプルデータは、github上で公開されています。
https://github.com/metrica-sports/sample-data/tree/master/data/Sample_Game_1

データの中身を確認してみます。

df_home.head(3)

出力結果を見てみると、それぞれの選手がピッチ上のどこにいたのかを座標形式で、0.04秒刻みに記録したデータになっています。
例えば、開始0.04秒時点のPlayer11は(0.00082, 0.48238)の座標にいたという見方になります。

座標の定義として、ピッチの左上を(0, 0)、ピッチの右下を(1, 1)として、X座標、Y座標は0~1の範囲で値を取る、
と記載があるので、Player11はピッチ上で左端の真ん中あたりにいるということが分かります。

データを整形する

完成版のコードでは、カラムやデータを整形する関数を作成してdf_home、df_awayに適用する方法を記述していますが、
以下では、df_homeを直接整形するコードを用いて、データ整形のプロセスを丁寧に解説します。

カラム内の選手名を取得する

# 左から4番目のカラムから2つおきにカラム名を取得する
cols = df_home.columns[3::2]
print(cols)

配列の各値を2つずつ並べる

# 配列の値を2つずつ繰り返す配列を生成する
cols = np.repeat(cols, 2)
print(cols)

x座標とy座標を区別できるようにする

# list型に変換する
cols = list(cols)

# 配列を順番に取り出して偶数番目に_x、奇数番に_yをくっつける
cols = [col+'_x' if i % 2 == 0 else col+'_y' for i, col in enumerate(cols)]
print(cols)

元のデータフレームのカラムを置き換えられるように配列を作成する

# 選手以外のカラムを取得して作成した配列にくっつける
cols = np.concatenate([df_home.columns[:3], cols])
print(cols)

データフレームのカラムを置き換える

# データフレームのカラムを置き換える
df_home.columns = cols
df_home.head(3)

秒数を指定したデータフレームを複製する

# Time [s]のカラムが0秒以上、10秒未満のデータセットを抽出し複製する
df_home = df_home[(df_home['Time [s]'] >= 0) & (df_home['Time [s]'] < 10)].copy()
df_away = df_away[(df_away['Time [s]'] >= 0) & (df_away['Time [s]'] < 10)].copy()

ボールの座標データを切り出す

# Homeのデータセットからボールの座標カラムを抽出したデータフレームを複製する
df_ball = df_home[['Period', 'Frame', 'Time [s]', 'Ball_x', 'Ball_y']].copy()

# ボールのデータフレームのカラム名を変更する
df_ball.rename({'Ball_x': 'x', 'Ball_y': 'y'}, axis=1, inplace=True)

# Home、Awayのデータフレームからボールの座標カラムを削除する
df_home.drop(['Ball_x', 'Ball_y'], axis=1, inplace=True)
df_away.drop(['Ball_x', 'Ball_y'], axis=1, inplace=True)

df_ball.head(3)

横持ちデータを縦持ちデータに変換する

# 4列目以降を変換対象として指定し、player、valueの2カラムの縦持ちデータに変換する
df_home = pd.melt(df_home, id_vars=df_home.columns[:3], value_vars=df_home.columns[3:], var_name='player', value_name='value')
df_home.head(3)

pandasのmelt関数を使用することで、横持ちデータを縦持ちデータに変換することができます。
melt関数は、以下の5つのパラメータを指定して使用します。

  • 対象のデータセット
  • id_vars:変換しないカラム
  • value_vars:変換する対象のカラム
  • var_name:変換前のカラム名を格納する列のカラム名
  • value_name:変換前の値を格納する列のカラム名

player列のxとyを切り出す

# player列の値に'_x'が含まれている行を抽出して、coordinate列に値'x'を代入する
df_home.loc[df_home.player.str.contains('_x'), 'coordinate'] = 'x'

# player列の値に'_y'が含まれている行を抽出して、coordinate列に値'y'を代入する
df_home.loc[df_home.player.str.contains('_y'), 'coordinate'] = 'y'

# player列の値を、先頭7文字目から末尾3文字目までの値に、上書きする(=先頭の'Player'と末尾の'_x''_y'をカットする)
df_home['player'] = df_home.player.str[6:-2]
df_home.head(3)

x座標とy座標の値を別の列に分ける

df_home = (df_home.set_index(['Period', 'Frame', 'Time [s]', 'player', 'coordinate'])['value'] # value以外の列をインデックスに設定する
           .unstack(level=4) # coordinateの値を列に展開する
           .reset_index() # インデックスをリセットする
           .rename_axis(None, axis=1)) # インデックス名をリセットする
df_home.head(3)

matplotlibを使用して、アニメーションを作成する

ピッチを描画する

# 描画する領域全体(figure)と個別のプロットを描画するする領域(ax)を定義する
fig, ax = plt.subplots(figsize=(16, 10.4))

# mplsoccerライブラリのPitchメソッドを使って、ピッチを定義する
pitch = Pitch(pitch_type='metricasports', pitch_width=68, pitch_length=105, goal_type='box', goal_alpha=1)

# axにピッチを描画する
pitch.draw(ax)

Line2Dオブジェクトを定義する

選手とボールの動きをプロットするために、座標の値が空の描画フレームを変数に格納します。
今回は、以下のマーカーの変数を設定しています。

  • marker:マーカーの形
  • ms:マーカーのサイズ
  • mfc:マーカー内部の色
  • mec:マーカー枠線の色
  • mew:マーカー枠線の幅
# ax.plotでLine2Dオブジェクトを定義する
ball, = ax.plot([], [], 'o', ms=6, mfc='w', mec='black', mew=3)
home, = ax.plot([], [], 'o', ms=10, mfc='#7f63b8', mec='black')
away, = ax.plot([], [], 'o', ms=10, mfc='#b94b75', mec='black')

グラフ更新用関数を作成する

# グラフ更新用関数を作成する
def update(i):
    # ボールのデータセットからi行目のフレームidを取得する
    frame = df_ball.iloc[i, 1]
    # 選手、ボールのx座標、y座標をセットする
    ball.set_data(df_ball.loc[df_ball.Frame == frame, 'x'],
                  df_ball.loc[df_ball.Frame == frame, 'y'])
    away.set_data(df_away.loc[df_away.Frame == frame, 'x'],
                  df_away.loc[df_away.Frame == frame, 'y'])
    home.set_data(df_home.loc[df_home.Frame == frame, 'x'],
                  df_home.loc[df_home.Frame == frame, 'y'])
    return ball, away, home

アニメーションを作成する

# アニメーションを定義する
anim = animation.FuncAnimation(fig, update, frames=len(df_ball), interval=50, blit=True)

# アニメーションを表示する
plt.show()

# アニメーションをgif形式で保存する
w = animation.PillowWriter(fps=20)
anim.save('animation.gif', writer=w)

animationライブラリのFuncanimationメソッドを使用して、作成するアニメーションを定義します。
今回は、以下の引数を設定しています。

  • 第1引数 fig:figureオブジェクト
  • 第2引数 update:アニメーションを構成する図1枚1枚を作成する関数
  • オプション frames:update関数を呼び出す回数を指定する
  • オプション interval:図が切り替わる時間間隔を指定する(単位はミリ秒)。
  • オプション blit:Trueを指定することで描画処理が高速化される

以上です。