# (必須)モジュールのインポート
import os
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt

# 表示設定
np.set_printoptions(suppress=True, precision=3)
pd.set_option('display.precision', 3)    # 小数点以下の表示桁
pd.set_option('display.max_rows', 10)   # 表示する行数の上限
pd.set_option('display.max_columns', 20)  # 表示する列数の上限
%precision 3
'%.3f'

6. イベントデータの解析#

6.1. イベントデータ#

6.1.1. Pappalardoデータセット#

Pappalardoデータセットはサッカーのイベントデータをまとめた大規模データセットであり,CC BY 4.0ライセンスの下で提供されている. 元のデータはWyscout社によって収集されたもので,それをL. Pappalardoらが編集し2019年に公開された. 2023年時点で一般公開されているサッカーのイベントデータセットの中では最大級である. データセットの詳細については以下の付録を参照のこと:Pappalardoデータセットの詳細

6.1.2. 本講義で用いる加工済みデータ#

Pappalardoデータセットはjson形式で提供されており,このままではデータ分析がしづらい. そこで,予めjson形式のデータを整形・加工し,PandasのDataFrameの形で保存しておくと便利であるが,この過程は本講義で扱うレベルを超える. 本講義ではデータの整形・加工の過程は省略し,加工済みデータ(csvファイル)のデータを提供することにする. 以下では,加工済みデータの一部を使ってデータ解析の例を示すが,他のデータを解析したい場合は付録からダウンロードできる:Pappalardoデータセットの詳細

6.2. リーグ成績と順位表#

以下のデータをダウンロードし,カレントディレクトリに保存せよ:

各リーグの最終的な順位は勝ち点によって決まる. 1試合で獲得する勝ち点は勝利が3,引き分けが1,負けが0である. 得点データを用いれば,チームごとに勝ち点を計算し,順位表を作成することができる.

以下では,イングランド・プレミアリーグの最終成績と順位表を作成してみよう. なお,公式に公開されている2017年度イングランド・プレミアリーグの最終成績と順位表は以下で確認できる:

6.2.1. データの読み込み#

まずは game.csv をダウンロードしてカレントディレクトリに移動し,GMという名前のDataFrameに読み込む.

GM = pd.read_csv('./game.csv', header=0)
GM.head(5)
game_id league section date venue away away_id home home_id away_score home_score
0 2499719 England 1 2017-08-11 Emirates_Stadium Leicester_City 1631 Arsenal 1609 3 4
1 2499723 England 1 2017-08-12 Goodison_Park Stoke_City 1639 Everton 1623 0 1
2 2499724 England 1 2017-08-13 Old_Trafford West_Ham_United 1633 Manchester_United 1611 0 4
3 2499722 England 1 2017-08-12 Selhurst_Park Huddersfield_Town 1673 Crystal_Palace 1628 3 0
4 2499725 England 1 2017-08-13 St_James'_Park Tottenham_Hotspur 1624 Newcastle_United 1613 2 0

このデータの各行には2017年度にヨーロッパリーグで行われた全試合の情報が収められている. 各列の意味は下表の通りである. このうち,away_score列とhome_score列がアウェイチームとホームチームの得点である. 例えば,第0行はアーセナル(ホーム)対レイチェスターシティ(アウェイ)の試合情報を表し,得点は4-3であることが分かる.

各列の変数

内容

game_id

試合の一意なID

league

リーグ名

section

節(全38節)

date

日付

venue

試合地

away

アウェイチーム名

away_id

アウェイチームID

home

ホームチーム名

home_id

ホームチームID

away_score

アウェイチームのスコア

homw_score

ホームチームのスコア

次に team.csv をダウンロードしてカレントディレクトリに移動し,TMという名前のDataFrameに読み込む.

TM = pd.read_csv('./team.csv', header=0)
TM.head()
name team_id city country league
0 Arsenal 1609 London England England
1 Chelsea 1610 London England England
2 Manchester_United 1611 Manchester England England
3 Liverpool 1612 Liverpool England England
4 Newcastle_United 1613 Newcastle_upon_Tyne England England

このデータの各行には2017年度ヨーロッパリーグに出場したクラブチームの情報が収められている. 各列の意味は下表の通りである. 例えば,第0行はイングランド・プレミアリーグに所属するアーセナルのチーム情報を表している.

各列の変数

内容

name

チームの俗称

team_id

チームID

city

チームの所在都市

country

チームの所在国

league

チームの所属リーグ

以下では,イングランド・プレミアリーグのデータを解析対象とする. そこで,条件付き抽出を用いて,TMGMからイングランド・プレミアリーグのデータだけ抽出する.

GM_E = GM.loc[GM['league']=='England']
TM_E = TM.loc[TM['league']=='England']

6.2.2. 1チームのリーグ成績#

チームプロフィールTM_Eの先頭行のチーム(アーセナル)に対し,リーグ成績を求めてみよう. まずはiloc属性を用いてTM_Eの先頭行を抽出し,このチームのチームIDとチーム名を取得する.

tm_id = TM_E['team_id'].iloc[0]
tm_name = TM_E['name'].iloc[0]
print(tm_id)
print(tm_name)
1609
Arsenal

得点・失点・得失点差

得点データGMでは,2チームをhome,awayによって区別している. よって,チームごとに得点と失点を集計するには,ホームゲームとアウェイゲームに分けて処理する必要がある. ホームゲームではhome_score列が得点,away_score列が失点に対応し,アウェイゲームではaway_score列が得点,home_score列が失点である. このことに注意し,アーセナルのホームゲームの得点・失点をS_h,アウェイゲームの得点・失点をS_aに保存する. また,得失点差の列diffを追加する.

# 得点と失点(ホームゲーム)
S_h = GM_E.loc[(GM_E['home_id']==tm_id), ['section', 'home_score', 'away_score']] # 対象とするチームのスコアを抽出
S_h = S_h.rename(columns={'home_score': 'goal', 'away_score': 'loss'}) # 列ラベルのリネーム
S_h.head()
section goal loss
0 1 4 3
31 4 3 0
51 6 2 0
61 7 2 0
91 10 2 1
# 得点と失点(アウェイゲーム)
S_a = GM_E.loc[(GM_E['away_id']==tm_id), ['section', 'away_score', 'home_score']] # 対象とするチームのスコアを抽出
S_a = S_a.rename(columns={'away_score': 'goal', 'home_score': 'loss'}) # 列ラベルのリネーム
S_a.head()
section goal loss
11 2 0 1
20 3 0 4
44 5 0 0
78 8 1 2
82 9 5 2
# 得失点差列の追加
S_h['diff'] = S_h['goal'] - S_h['loss']  # ホーム
S_a['diff'] = S_a['goal'] - S_a['loss']  # アウェイ
S_h
section goal loss diff
0 1 4 3 1
31 4 3 0 3
51 6 2 0 2
61 7 2 0 2
91 10 2 1 1
... ... ... ... ...
291 30 3 0 3
310 32 3 0 3
321 33 3 2 1
341 35 4 1 3
361 37 5 0 5

19 rows × 4 columns

試合結果

次に,試合結果の列resultを追加する. 勝ちを1,引き分けを0,負けを-1で表すことにすると,各試合の結果は得失点差を変換することで求められる. 以下ではnp.sign関数を使って正の数を1,負の数を-1に変換することでresult列を求める.

S_h['result'] = np.sign(S_h['diff']) # ホーム
S_a['result'] = np.sign(S_a['diff']) # アウェイ
S_h
section goal loss diff result
0 1 4 3 1 1
31 4 3 0 3 1
51 6 2 0 2 1
61 7 2 0 2 1
91 10 2 1 1 1
... ... ... ... ... ...
291 30 3 0 3 1
310 32 3 0 3 1
321 33 3 2 1 1
341 35 4 1 3 1
361 37 5 0 5 1

19 rows × 5 columns

ホームゲームとアウェイゲームのデータを結合する

次に,pd.concat関数を使ってホームゲームのデータの下にアウェイゲームのデータを結合する.

S = pd.concat([S_h, S_a])
S
section goal loss diff result
0 1 4 3 1 1
31 4 3 0 3 1
51 6 2 0 2 1
61 7 2 0 2 1
91 10 2 1 1 1
... ... ... ... ... ...
286 29 1 2 -1 -1
303 31 1 3 -2 -1
335 34 1 2 -1 -1
353 36 1 2 -1 -1
377 38 1 0 1 1

38 rows × 5 columns

勝ち点

勝ち点は勝ちの場合に3,引き分けの場合に1として計算する.

S['point'] = 0  # 勝ち点列を0で初期化する
S.loc[S['result']==1, 'point'] = 3  # 勝ちの場合
S.loc[S['result']==0, 'point'] = 1  # 引き分けの場合
S.sort_values('section') # 節でソート
section goal loss diff result point
0 1 4 3 1 1 3
11 2 0 1 -1 -1 0
20 3 0 4 -4 -1 0
31 4 3 0 3 1 3
44 5 0 0 0 0 1
... ... ... ... ... ... ...
335 34 1 2 -1 -1 0
341 35 4 1 3 1 3
353 36 1 2 -1 -1 0
361 37 5 0 5 1 3
377 38 1 0 1 1 3

38 rows × 6 columns

最終成績

最後に各試合のデータを集計し,総得点,総失点,総得失点差,勝ち点を計算すれば,アーセナルのリーグ成績が求められる. 他のチームの成績を統合することを考えて,以下のようにDataFrameの形に整形しておく.

pd.DataFrame([[tm_name, tm_id, S['goal'].sum(), S['loss'].sum(), S['diff'].sum(), S['point'].sum()]],
              columns=['チーム', 'ID', '得点', '失点', '得失点', '勝点'])
チーム ID 得点 失点 得失点 勝点
0 Arsenal 1609 74 51 23 63

6.2.3. 全チームのリーグ成績と順位表#

全チームのリーグ成績を求めるには上の手続きを繰り返せば良い. 以下では,Rankという名前のDataFrameに全チームのリーグ成績を保存する.

Rank = pd.DataFrame(columns=['チーム', 'ID', '得点', '失点', '得失点', '勝点'])
for i in range(len(TM_E)):
    tm_id = TM_E['team_id'].iloc[i]
    tm_name = TM_E['name'].iloc[i]
    
    '''ホームゲーム'''
    # 得点と失点
    S_h = GM_E.loc[(GM_E['home_id']==tm_id), ['section', 'home_score', 'away_score']]
    S_h = S_h.rename(columns={'home_score': 'goal', 'away_score': 'loss'})

    # 得失点差
    S_h['diff'] = S_h['goal'] - S_h['loss']

    # 勝敗(勝:1,分:0,負:-1)
    S_h['result'] = np.sign(S_h['diff']) # 符号に応じて1,0,-1を返す
    
    '''アウェイゲーム'''
    # 得点と失点
    S_a = GM_E.loc[(GM_E['away_id']==tm_id), ['section', 'home_score', 'away_score']]
    S_a = S_a.rename(columns={'away_score': 'goal', 'home_score': 'loss'})

    # 得失点差
    S_a['diff'] = S_a['goal'] - S_a['loss']

    # 勝敗(勝:1,分:0,負:-1)
    S_a['result'] = np.sign(S_a['diff'])  # 符号に応じて1,0,-1を返す
    
    # 統合
    S = pd.concat([S_h, S_a])
    
    # 勝ち点
    S['point'] = 0
    S.loc[S['result']==1, 'point'] = 3
    S.loc[S['result']==0, 'point'] = 1
    
    # 節でソート
    S = S.sort_values('section')
    
    # 順位表への統合
    gf = S['goal'].sum()  # 総得点
    ga = S['loss'].sum()  # 総失点
    gd = S['diff'].sum()  # 総得失点差
    pt = S['point'].sum()  # 勝ち点
    
    # チーム成績の結合
    df = pd.DataFrame([[tm_name, tm_id, gf, ga, gd, pt]], columns=Rank.columns)
    Rank = pd.concat([Rank, df])

最後に,データフレームを勝ち点の順にソートしてインデックスを付け直し,csvファイルとして保存する.

# ソートと再インデックス
Rank = Rank.sort_values(['勝点'], ascending=False)
Rank = Rank.reset_index(drop=1)

# csvファイルへの出力
Rank.to_csv('./Rank_England.csv', index=True)

以上により,イングランド・プレミアリーグの順位表が作成できた.

Wikipediaの情報とは一部合わないが,Premier League Table, Form Guide & Season Archivesとは一致している.

Rank
チーム ID 得点 失点 得失点 勝点
0 Manchester_City 1625 106 27 79 100
1 Manchester_United 1611 68 28 40 81
2 Tottenham_Hotspur 1624 74 36 38 77
3 Liverpool 1612 84 38 46 75
4 Chelsea 1610 62 38 24 70
... ... ... ... ... ... ...
15 Huddersfield_Town 1673 28 58 -30 37
16 Southampton 1619 37 56 -19 36
17 Stoke_City 1639 35 68 -33 33
18 Swansea_City 10531 28 56 -28 33
19 West_Bromwich_Albion 1627 31 56 -25 31

20 rows × 6 columns

6.2.4. 演習問題#

  • イングランド以外のリーグについて,同様の順位表を作成せよ

  • 好きなチームに対して,横軸に節,縦軸に累積勝ち点を取ったグラフを作成し,1シーズンの勝ち点の変動を可視化せよ.

  • 特定のリーグの全チームについて,勝ち点の変動を可視化せよ.

6.3. 得点分布#

サッカーは非常に得点頻度が低い競技であるが,得点がランダムに入るため常に試合から目が離せない. 得点のランダム性はサッカーが人々を熱狂させる理由と考えられるが,実はこのようなランダム性の裏にはきれいな法則が隠れている.

6.3.1. ポアソン分布#

二項分布からポアソン分布へ

成功確率が \( p \) の試行を独立に \( n \) 回繰り返すことを考える. 例えば,サイコロを振って特定の目が出ることを成功とすると,\( p=1/6 \) である. いま,\( n \) 回中 \( x \) 回成功する確率を \( f(x) \) とすると,\( f(x) \) は二項分布に従う:

\[ f(x) = \binom{n}{x}p^{x}(1-p)^{n-x} \]

この式において,\( p^{x}(1-p)^{n-x} \) は成功が \( x \)回,失敗が \( n-x \) 回生じる確率を意味する. また,\( \binom{n}{x} \)\( n \) 個から \( x \) 個を取り出す組み合わせの数 \( _{n}C_{x} \) を表し,\( n \) 回の中で何回目に成功するかの場合の数に対応する.

いま,成功確率 \( p \) が小さく,かつ試行回数 \( n \) が大きい極限を考える. ただし,極限を取る際に発散しないように平均値が一定値 \( np=\mu \) になるようにする. このような条件で \(n\) 回中 \(x\) 回成功する確率 \(f(x)\) は,二項分布の式に \( np=\mu \) を代入し,極限 \( p\to 0,\ n\to \infty \) を取ることで

\[ f(x) = \frac{\mu^{x}}{x!} \mathrm{e}^{-\mu} \]

と求まる. これをポアソン分布と呼ぶ. ポアソン分布は1つのパラメータ \( \mu \) だけで特徴づけられ,期待値と分散はともに \( \mu \) となる. ポアソン分布はその導出過程より,一定の期間内に発生確率の小さい稀な現象を何度も試行した場合に,その発生回数が従う分布である. 例えば,以下の現象は全てポアソン分布に従うことが知られている:

  • 1日のコンビニの来客数

  • 1日の交通事故件数

  • 1分間の放射性元素の崩壊数

  • 1ヶ月の有感地震の回数

  • プロシア陸軍で馬に蹴られて死亡した兵士の数

サッカーの得点分布

チームの強さや試合展開など細かいことはひとまず無視し,サッカーにおける得点がランダムに発生すると仮定する. 例えば,1プレーが数秒に1回行われるとし,どのプレーでも一定の得点確率 \( p \) で得点が入ると見なせば,サッカーは得点確率 \( p \) の小さい試行を何度も繰り返す現象(\( n\to \infty \))と見なすことができ,1試合の得点数はポアソン分布に従うことが期待される.

6.3.2. 得点データの要約#

まずはgame.csvをダウンロードしてカレントディレクトリに保存し,GMという名前のDataFrameに読み込む.

GM = pd.read_csv('./game.csv', header=0)
GM.head(2)
game_id league section date venue away away_id home home_id away_score home_score
0 2499719 England 1 2017-08-11 Emirates_Stadium Leicester_City 1631 Arsenal 1609 3 4
1 2499723 England 1 2017-08-12 Goodison_Park Stoke_City 1639 Everton 1623 0 1

この得点データを用いて,リーグごとにアウェイチームとホームチームの得点傾向を調べてみよう. 以下はアウェイチームとホームチームの得点の平均値および分散である. この結果からおおよそ以下のようなことが読み取れる

  • 1試合の得点の平均値はおおよそ1.2点くらいとなっており,サッカーが得点頻度の少ない競技であることが分かる.

  • ホームとアウェイで比べると,ホームの方がやや平均得点が高い傾向にある.

  • 得点の平均値と分散はほぼ同じ値となっており,ポアソン分布の性質をおおよそ満たしている.

# England
print(GM.loc[GM['league']=='England', ['away_score', 'home_score']].mean())
print(GM.loc[GM['league']=='England', ['away_score', 'home_score']].var())
away_score    1.147
home_score    1.532
dtype: float64
away_score    1.387
home_score    1.796
dtype: float64
# France
print(GM.loc[GM['league']=='France', ['away_score', 'home_score']].mean())
print(GM.loc[GM['league']=='France', ['away_score', 'home_score']].var())
away_score    1.189
home_score    1.529
dtype: float64
away_score    1.267
home_score    1.817
dtype: float64
# Germany
print(GM.loc[GM['league']=='Germany', ['away_score', 'home_score']].mean())
print(GM.loc[GM['league']=='Germany', ['away_score', 'home_score']].var())
away_score    1.193
home_score    1.601
dtype: float64
away_score    1.291
home_score    1.644
dtype: float64
# Italy
print(GM.loc[GM['league']=='Italy', ['away_score', 'home_score']].mean())
print(GM.loc[GM['league']=='Italy', ['away_score', 'home_score']].var())
away_score    1.221
home_score    1.455
dtype: float64
away_score    1.413
home_score    1.721
dtype: float64
# Spain
print(GM.loc[GM['league']=='Spain', ['away_score', 'home_score']].mean())
print(GM.loc[GM['league']=='Spain', ['away_score', 'home_score']].var())
away_score    1.147
home_score    1.547
dtype: float64
away_score    1.408
home_score    1.900
dtype: float64

6.3.3. 得点分布#

平均値と分散の一致だけではポアソン分布に従う根拠として乏しい. そこで,リーグ別にホームチームの得点のヒストグラムを求めてみよう. 以下はイングランド・プレミアリーグのホームチームの得点分布である.

data = GM.loc[GM['league']=='England', 'home_score']

fig, ax = plt.subplots(figsize=(4,3))
x = np.arange(data.max()+2)
ax.hist(data, 
        bins=x, # 階級の左端の値を指定する
        align='left',    # バーの中央を階級の左端に合わせる
        histtype='bar',  # ヒストグラムのスタイル
        color='gray',    # バーの色
        edgecolor='k',   # バーの枠線の色
        rwidth=0.2       # バーの幅
        )

ax.set_xlabel('Home Score', fontsize=12)
ax.set_ylabel('Frequency', fontsize=12)
ax.set_xticks(x);
../_images/70b14cf96dd0d12ce81392a15b371d1d5365fb21fdf150cb7e12011568ad3529.png

次に,上のヒストグラムがポアソン分布に従っているか調べるため,試合データから求めた平均値をパラメータとするポアソン分布を描いてみる. イングランド・プレミアリーグのホームチームの平均得点は1.53であったので,

\[ f(x) = \frac{1.53^{x}}{x!} \mathrm{e}^{-1.53} \]

のグラフを描けば良い.

from scipy.stats import poisson

fig, ax = plt.subplots(figsize=(4, 3))
x = np.arange(data.max()+2)
fx = poisson.pmf(x, data.mean())
ax.plot(x, fx, '-ok')
[<matplotlib.lines.Line2D at 0x16ff3f0a0>]
../_images/9a405435e8cc93db2a6cd45976494f2f08b7fec365895496353e1812950ce349.png

上のグラフを見比べると,確かに似た分布になっていることが分かる. そこで,最後に2つのグラフを合わせよう.

from scipy.stats import poisson
data = GM.loc[GM['league']=='England', 'home_score']

fig, ax = plt.subplots(figsize=(4,3))
x = np.arange(data.max()+2)
ax.hist(data, 
        bins=x, # 階級の左端の値を指定する
        align='left',    # バーの中央を階級の左端に合わせる
        histtype='bar',  # ヒストグラムのスタイル
        color='gray',    # バーの色
        edgecolor='k',   # バーの枠線の色
        rwidth=0.2
        )

fx = data.size * poisson.pmf(x, data.mean())
ax.plot(x, fx, '-ok')

ax.set_xlabel('Home Score', fontsize=12)
ax.set_ylabel('Frequency', fontsize=12)
ax.set_xticks(x);
../_images/c3787aa9194b8b62ab211e5d0a8bf6c8c832be6ca9eb8b0d31534a33ad4a20ae.png

実データ(棒グラフ)とポアソン分布(折れ線)の概形はおおよそ一致していることが分かる. これは,得点の平均値と分散が近い値になったことと共に,サッカーの得点分布がポアソン分布に従うことを裏付ける材料となる. もちろん,サッカーの得点分布が普遍的にポアソン分布に従うかどうかは,他のリーグのデータを調べなければわからない(確かめてみよ). また,\(\chi^2\)検定などを用いてより定量的な検証を行うことも必要である(試してみよ).

6.3.4. 演習問題#

  • 他のリーグについて,ホームチームまたはアウェイチームの得点分布を描画し,さらに実データから求めた平均値をパラメータとするポアソン分布を同じグラフに描画せよ.

  • \( \chi^2 \) 検定を用いて,サッカーの得点分布がポアソン分布に従うかどうか検証せよ.

6.4. イベントデータの解析#

Pappalardoデータセットのイベントデータはイベントログとイベントタグの2種類から成る:

  • イベントログ

    • パスやシュートなどのボールに関わるイベントに対して,起きた時刻,場所,関わった選手などの基本情報が紐付けられたデータ

    • 1試合あたり1500~2000イベント

  • イベントタグ

    • イベントログの各イベントに対して,より詳細な付加情報が紐付けられたデータ

イベントデータにはボールに関わるほぼ全てのプレー情報が含まれているため,これを分析すれば詳細な試合展開を把握することができる. イベントデータは選手プロフィールや得点データに比べて格段に情報量が多いため,その扱いの難易度も高い. 基本的にExcelで解析するのは困難であり,Pandasの本領が最も発揮されるデータといえる.

6.4.1. 加工済みデータの内容#

加工済みデータの詳細およびダウンロード用リンクを以下にまとめる.
※ W杯とCLのデータはヨーロッパリーグと試合数が異なるので除外する.

内容

ファイル

ファイルサイズ

イベントIDとイベント名の対応

event_list.csv

0.9KB

イベントに付与されるタグの説明

tag_list.csv

2KB

各試合のイベントデータ(イングランド)

イベントログ:event_England.csv
イベントタグ:event_tag_England.csv

58MB
76.2MB

//(フランス)

イベントログ:event_France.csv
イベントタグ:event_tag_France.csv

57.6MB
74.8MB

//(ドイツ)

イベントログ:event_Germany.csv
イベントタグ:event_tag_Germany.csv

47.2MB
61.5MB

//(イタリア)

イベントログ:event_Italy.csv
イベントタグ:event_tag_Italy.csv

58.9MB
76.6MB

//(スペイン)

イベントログ:event_Spain.csv
イベントタグ:event_tag_Spain.csv

56.1MB
74.5MB

以下では,イングランド・プレミアリーグのデータを解析対象とする. 準備として,次のファイルをダウンロードしてカレントディレクトリに移動する:

これらを以下のように読み込んでおく.

# イベントデータと選手プロフィールの読み込み
EV = pd.read_csv('./event_England.csv')
EV_tag = pd.read_csv('./event_tag_England.csv')
PL = pd.read_csv('./player.csv', header=0)

6.4.2. イベントデータの詳細#

イベントログ

EV.head()
id game_id half t team_id player_id event event_id subevent subevent_id x1 y1 x2 y2
0 177959171 2499719 1 2.759 1609 25413 pass 8 simple_pass 85.0 49.0 49.0 31.0 78.0
1 177959172 2499719 1 4.947 1609 370224 pass 8 high_pass 83.0 31.0 78.0 51.0 75.0
2 177959173 2499719 1 6.542 1609 3319 pass 8 head_pass 82.0 51.0 75.0 35.0 71.0
3 177959174 2499719 1 8.143 1609 120339 pass 8 head_pass 82.0 35.0 71.0 41.0 95.0
4 177959175 2499719 1 10.302 1609 167145 pass 8 simple_pass 85.0 41.0 95.0 72.0 88.0

EVには380試合分のイベントログが含まれており,その行数は64万行にのぼる. EVの各行は試合中の1イベントに対応し,各列にそのイベントに関する基本情報が収められている. 各列の内容は下表の通りである.

変数名

内容

id

イベント識別ID

game_id

試合ID

half

1(前半),2(後半)

t

ハーフ開始からの経過時間(単位は秒)

team_id

チームID

player_id

選手ID

event

イベント名

event_id

イベントID

subevent

サブイベント名

subevent_id

サブイベントID

x1

イベント開始 \(x\) 座標(単位は%)

y1

イベント開始 \(y\) 座標(単位は%)

x2

イベント終了 \(x\) 座標(単位は%)

y2

イベント終了 \(y\) 座標(単位は%)

座標系

イベント開始座標 \((x_{1}, y_{1})\) と終了座標 \((x_{2}, y_{2})\) は以下の座標系に従う:

  • 原点は左下

  • \(x,\ y\) 座標の値はフィールドの横幅と縦幅の最大値に対する割合(単位は%)

    • \(0\le x \le 100\)

    • \(0\le y \le 100\)

  • 両チームの攻撃方向は右方向となるように統一されている

    • チームや前後半に関係なく,\( x > 50 \) が相手陣,\( x < 50 \) が自陣

    • ※ 解析内容に応じて,攻撃方向が逆になるように変換する必要がある

サッカーコートの公式規格は \(105\mathrm{m}\times 68\mathrm{m}\) なので,コートを描く際にはアスペクト比を以下のように設定する:

ax.set_aspect(68/105)
'''サッカーコートの描画'''
fig, ax = plt.subplots(figsize=(4, 4))
ax.set_aspect(68/105)  # アスペクト比を設定する

# ハーフウェイライン
ax.plot([50, 50], [0, 100], 'k--') 

# 描画範囲と軸ラベル
ax.set_xlim(0, 100); ax.set_ylim(0, 100)
ax.set_xlabel('$X$'); ax.set_ylabel('$Y$');
../_images/04d2238d44d2a487dbe294362cd9f6f0b3184e01f31113077d50872f8c6a067c.png

イベントタグ

EV_tag.head()
id goal own_goal opportunity assist keypass left right head/body free_space_r ... interception sliding_tackle red_card yellow_card second_yellow_card accurate not_accurate counter_attack dangerous_ball_lost blocked
0 177959171 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 ... 0.0 0.0 0.0 0.0 0.0 1.0 0.0 0.0 0.0 0.0
1 177959172 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 ... 0.0 0.0 0.0 0.0 0.0 1.0 0.0 0.0 0.0 0.0
2 177959173 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 ... 0.0 0.0 0.0 0.0 0.0 1.0 0.0 0.0 0.0 0.0
3 177959174 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 ... 0.0 0.0 0.0 0.0 0.0 1.0 0.0 0.0 0.0 0.0
4 177959175 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 ... 0.0 0.0 0.0 0.0 0.0 1.0 0.0 0.0 0.0 0.0

5 rows × 58 columns

EV_tagはイベントログEVと同じ行数のDataFrameであり,各行が試合中の1イベントを表している. 各列にはイベントに付与されたタグ('goal','assist'など)が並んでおり,真ならば1,偽ならば0となっている. (このようなデータをOne-Hotエンコーディングと呼ぶ.) 例えば,'goal'列が1である行では,そのイベントにおいて得点が入ったことを意味する.

タグの詳細情報はtag_list.csvにまとめられている. 主要なタグを下表にまとめる.

タグ名

内容

goal

得点

own_goal

オウンゴール

assist

アシスト

key_pass

パス

accurate

イベントの成功

not accurate

イベントの失敗

6.4.3. イベントデータ解析の基本#

イベントログEVとイベントタグEV_tagには,ボールに関わるイベントに関するほぼ全ての情報が含まれている. イベントデータ解析の目的はこれらのデータから意味のある情報を抽出することである. イベントデータを解析する際の手順は以下のようにまとめられる:

  1. イベントログEV,イベントタグEV_tagから必要な行を条件付き抽出する

  2. 条件付き抽出したデータを集計する

  3. 集計したデータを可視化する

以下では,条件付き抽出の例をいくつか示す.

特定の試合・時間帯の抽出

# 特定の試合を抽出する
ev = EV.loc[EV['game_id']==2499719].copy()
ev_tag = EV_tag.loc[EV['game_id']==2499719].copy()
# 前半のみ抽出する
ev.loc[ev['half']==1].tail()
id game_id half t team_id player_id event event_id subevent subevent_id x1 y1 x2 y2
896 177960132 2499719 1 2814.015 1631 8653 others_on_the_ball 7 touch 72.0 14.0 49.0 11.0 61.0
897 177960129 2499719 1 2814.485 1609 14869 pass 8 simple_pass 85.0 89.0 39.0 92.0 50.0
898 177960130 2499719 1 2815.901 1609 7945 shot 10 shot 100.0 92.0 50.0 0.0 0.0
899 177960121 2499719 1 2817.605 1631 8480 save_attempt 9 reflexes 90.0 100.0 100.0 8.0 50.0
900 177960127 2499719 1 2852.557 1631 14853 pass 8 simple_pass 85.0 31.0 24.0 100.0 100.0
# 前半開始20秒までを抽出する
ev.loc[(ev['half']==1) & (ev['t']<20)].tail()
id game_id half t team_id player_id event event_id subevent subevent_id x1 y1 x2 y2
6 177959186 2499719 1 13.961 1631 8653 pass 8 head_pass 82.0 23.0 25.0 39.0 15.0
7 177959189 2499719 1 14.765 1631 8013 duel 1 air_duel 10.0 39.0 15.0 33.0 20.0
8 177961218 2499719 1 14.765 1609 0 duel 1 air_duel 10.0 61.0 85.0 67.0 80.0
9 177959178 2499719 1 15.320 1609 167145 pass 8 head_pass 82.0 67.0 80.0 59.0 61.0
10 177959179 2499719 1 18.052 1609 49876 pass 8 head_pass 82.0 59.0 61.0 45.0 45.0

特定のイベントの抽出

イベントログEVには'event'列と'subevent'列が存在する. 'event'列は'pass''foul'などの大分類,'subevent'列は'simple_pass''high_pass'などの小分類となっている. 'event'および'subevent'のリストはevent_list.csv にまとめられている.

# event列が'pass'の行を抽出
ev.loc[ev['event']=='pass'].head()
id game_id half t team_id player_id event event_id subevent subevent_id x1 y1 x2 y2
0 177959171 2499719 1 2.759 1609 25413 pass 8 simple_pass 85.0 49.0 49.0 31.0 78.0
1 177959172 2499719 1 4.947 1609 370224 pass 8 high_pass 83.0 31.0 78.0 51.0 75.0
2 177959173 2499719 1 6.542 1609 3319 pass 8 head_pass 82.0 51.0 75.0 35.0 71.0
3 177959174 2499719 1 8.143 1609 120339 pass 8 head_pass 82.0 35.0 71.0 41.0 95.0
4 177959175 2499719 1 10.302 1609 167145 pass 8 simple_pass 85.0 41.0 95.0 72.0 88.0
# subevent列が'simple_pass'の行を抽出
ev.loc[ev['subevent']=='simple_pass'].head()
id game_id half t team_id player_id event event_id subevent subevent_id x1 y1 x2 y2
0 177959171 2499719 1 2.759 1609 25413 pass 8 simple_pass 85.0 49.0 49.0 31.0 78.0
4 177959175 2499719 1 10.302 1609 167145 pass 8 simple_pass 85.0 41.0 95.0 72.0 88.0
5 177959177 2499719 1 12.549 1609 3319 pass 8 simple_pass 85.0 72.0 88.0 77.0 75.0
17 177959196 2499719 1 29.981 1631 265366 pass 8 simple_pass 85.0 29.0 26.0 37.0 8.0
18 177959197 2499719 1 31.164 1631 8013 pass 8 simple_pass 85.0 37.0 8.0 23.0 5.0
# event列が'shot'の行を抽出
ev.loc[(ev_tag['goal']==1)].head()
id game_id half t team_id player_id event event_id subevent subevent_id x1 y1 x2 y2
46 177959212 2499719 1 94.596 1609 25413 shot 10 shot 100.0 88.0 41.0 0.0 0.0
47 177959226 2499719 1 96.971 1631 8480 save_attempt 9 reflexes 90.0 100.0 100.0 12.0 59.0
91 177959280 2499719 1 254.745 1631 14763 shot 10 shot 100.0 96.0 52.0 100.0 100.0
92 177959249 2499719 1 256.548 1609 7882 save_attempt 9 reflexes 90.0 0.0 0.0 4.0 48.0
554 177959759 2499719 1 1710.855 1631 12829 shot 10 shot 100.0 94.0 54.0 100.0 100.0

イベントタグを用いた抽出

イベントタグEV_tagはイベントログEVと同じ行数で共通の行ラベル(インデックス)を持つ. よって,EV_tagで取得したブールインデックスを用いてEVから条件付き抽出することができる.

# イベント名が'pass'で,'accurate'タグが1である行(成功パス)を抽出
ev.loc[(ev['event']=='pass') & (ev_tag['accurate']==1)]
id game_id half t team_id player_id event event_id subevent subevent_id x1 y1 x2 y2
0 177959171 2499719 1 2.759 1609 25413 pass 8 simple_pass 85.0 49.0 49.0 31.0 78.0
1 177959172 2499719 1 4.947 1609 370224 pass 8 high_pass 83.0 31.0 78.0 51.0 75.0
2 177959173 2499719 1 6.542 1609 3319 pass 8 head_pass 82.0 51.0 75.0 35.0 71.0
3 177959174 2499719 1 8.143 1609 120339 pass 8 head_pass 82.0 35.0 71.0 41.0 95.0
4 177959175 2499719 1 10.302 1609 167145 pass 8 simple_pass 85.0 41.0 95.0 72.0 88.0
... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
1739 177961023 2499719 2 2865.715 1631 26150 pass 8 head_pass 82.0 65.0 94.0 72.0 83.0
1740 177961024 2499719 2 2866.966 1631 12829 pass 8 simple_pass 85.0 72.0 83.0 69.0 74.0
1759 177961037 2499719 2 2985.152 1631 8488 pass 8 head_pass 82.0 20.0 77.0 25.0 72.0
1762 177961039 2499719 2 2990.768 1631 8653 pass 8 simple_pass 85.0 11.0 68.0 7.0 53.0
1764 177961035 2499719 2 2994.901 1609 49876 pass 8 head_pass 82.0 54.0 51.0 73.0 58.0

679 rows × 14 columns

# イベント名が'shot'で,'goal'タグが1である行(成功シュート)
ev.loc[(ev['event']=='shot') & (ev_tag['goal']==1)]
id game_id half t team_id player_id event event_id subevent subevent_id x1 y1 x2 y2
46 177959212 2499719 1 94.596 1609 25413 shot 10 shot 100.0 88.0 41.0 0.0 0.0
91 177959280 2499719 1 254.745 1631 14763 shot 10 shot 100.0 96.0 52.0 100.0 100.0
554 177959759 2499719 1 1710.855 1631 12829 shot 10 shot 100.0 94.0 54.0 100.0 100.0
898 177960130 2499719 1 2815.901 1609 7945 shot 10 shot 100.0 92.0 50.0 0.0 0.0
1107 177960379 2499719 2 634.312 1631 12829 shot 10 shot 100.0 92.0 54.0 100.0 100.0
1570 177960849 2499719 2 2231.120 1609 7870 shot 10 shot 100.0 94.0 63.0 0.0 0.0
1613 177960902 2499719 2 2374.621 1609 26010 shot 10 shot 100.0 91.0 44.0 0.0 0.0

6.4.4. イベント別のヒートマップ#

条件付き抽出の応用として,イベント別にヒートマップを描いてみよう. まず,以下のようにヒートマップを描くevent_hmap関数を作成する. この関数は,\(x,\ y\)座標のデータを引数として受け取り,matplotlibのhist2d関数を用いてヒートマップを描く.

def event_hmap(x, y, cm='Greens'):
    '''
    イベントデータからヒートマップを描画する
    '''
    
    fig, ax = plt.subplots(figsize=(4, 4))
    
    # アスペクト比の変更
    ax.set_aspect(68/105)
    
    # ヒートマップの描画
    ret = ax.hist2d(x, y,\
                    bins=[50, 25], range=[[0, 100], [0, 100]], cmap=cm, cmin=0)

    # カラーバーを追加
    fig.colorbar(ret[3], orientation='vertical', 
                 shrink=0.4, aspect=10, pad=0.05)
    
    # ハーフウェイラインを追加
    ax.plot([50, 50], [0, 100], 'k--') 

    # 描画範囲とラベル
    ax.set_xlim(0, 100); ax.set_ylim(0, 100)
    ax.set_xlabel('$X$'); ax.set_ylabel('$Y$')

特定のイベントだけを条件付き抽出してその\(x,\ y\)座標をevent_hmap関数に渡せば,そのイベントが行われたフィールド上の位置をヒートマップで可視化することができる. 以下にいくつかの例を示す.

# パス
cond = (EV['event']=='pass')
x, y = EV.loc[cond, 'x1'], EV.loc[cond, 'y1']
event_hmap(x, y)
../_images/b3dceddaa152a37701421cb048d690ba7d77cf9ad7a9d8ead8b2b33e4fac8b45.png
# 特定の選手のパス
cond = (EV['event']=='pass') & (EV['player_id']==167145)
x, y = EV.loc[cond, 'x1'], EV.loc[cond, 'y1']
event_hmap(x, y, 'Blues')
../_images/565bf0ed21e3799c22844f2927d1ef1697016defdd4ec5fcf9304d83a9b268d6.png
# クロス
cond = (EV['subevent']=='cross')
x, y = EV.loc[cond, 'x1'], EV.loc[cond, 'y1']
event_hmap(x, y, 'Reds')
../_images/edd665902c6c8b34f27f1a219ab2a5d209200166a238d4596bbeb6402f615368.png
# デュエル
cond = (EV['event']=='duel')
x, y = EV.loc[cond, 'x1'], EV.loc[cond, 'y1']
event_hmap(x, y, 'Greys')
../_images/32e8c7bb2ffe35821535ea79bccb41918c008a587b55857981679fbe5b481f8e.png
# デュエル(攻撃時)
cond = (EV['subevent']=='ground_attacking_duel')
x, y = EV.loc[cond, 'x1'], EV.loc[cond, 'y1']
event_hmap(x, y, 'jet')
../_images/ec69c11503040dc3c6bea8e00f492213b80d2f63d1c05760dfe26b81d8d87b29.png
# シュート
cond = (EV['event']=='shot')
x, y = EV.loc[cond, 'x1'], EV.loc[cond, 'y1']
event_hmap(x, y)
../_images/4f253a756d7596ddf2cce66b02ec2e43b137444493dff3527f03dd329e1d08e8.png
# シュート(成功)
cond = (EV['event']=='shot') & (EV_tag['goal']==1)
x, y = EV.loc[cond, 'x1'], EV.loc[cond, 'y1']
event_hmap(x, y)
../_images/101c15aa168d6589f2a93af2c14af51c981303c3fa619463ee82960fa01fdb35.png

6.4.5. 選手のランキング#

シーズンが終了すると,チームのリーグ成績と共に選手の個人成績が発表される. 個人成績は,シュート数やゴール数などの部門別ランキングとなっている. ここでは,イベントデータを用いてこれらのランキングを求めてみよう. なお,どのようなプレーをシュートやパスと見なすかは用いるデータセットによって異なっており, 以下で求めるランキングが公式発表されたものと完全に一致するわけではない. 2017年度プレミアリーグの個人成績は例えば,

にて確認できるが,細かい数値は本データセットから求めたものと一致しない.

ランキングの作成方法は以下の通りである.

  • ランキング項目に応じて条件付き抽出する.

    • 例えば,パス数の場合は'event'列が'pass'である行を抽出する

  • 条件付き抽出後のDataFrameに対し,'player_id'ごとの出現回数を求める

    • DataFrameのvalue_countsメソッドを用いる

  • 選手プロフィールPLを用いて'player_id'を選手名に変換する

    • 'player_id'と'name'が対応した辞書を作成し,renameメソッドを用いる

# 'player_id'と'name'が対応した辞書
dict_id_name = dict(PL[['player_id', 'name']].values)

シュート数

cond = (EV['subevent']=='shot') | (EV['subevent']=='free_kick_shot') | (EV['subevent']=='penalty')
Rank_shot = EV.loc[cond, 'player_id'].value_counts()
Rank_shot = Rank_shot.rename(index=dict_id_name)  # 選手IDを選手名に変換する
Rank_shot.iloc[:10]
H_Kane            175
Mohame_Salah      142
C_Eriksen          97
Richarlison        92
S_Agüero           91
K_D_Bruyne         91
A_Sánchez          85
R_Sterling         80
R_Lukaku           80
Robert_Firmino     80
Name: player_id, dtype: int64

パス数

Rank_pass = EV.loc[(EV['event']=='pass'), 'player_id'].value_counts()
Rank_pass = Rank_pass.rename(index=dict_id_name)  # 選手IDを選手名に変換する
Rank_pass.iloc[:10]
G_Xhaka         2974
N_Otamendi      2964
Fernandinho     2842
Azpilicueta     2713
K_D_Bruyne      2672
N_Matić         2456
Davi_Silva      2382
J_Vertonghen    2370
K_Walker        2316
C_Eriksen       2196
Name: player_id, dtype: int64

アシスト数

Rank_assist = EV.loc[(EV_tag['assist']==1), 'player_id'].value_counts()
Rank_assist = Rank_assist.rename(index=dict_id_name)  # 選手IDを選手名に変換する
Rank_assist.iloc[:10]
K_D_Bruyne      16
L_Sané          13
R_Mahrez        10
R_Sterling      10
Davi_Silva      10
H_Mkhitaryan     9
D_Alli           9
C_Eriksen        9
Mohame_Salah     8
P_Groß           8
Name: player_id, dtype: int64

ゴール数

cond = ((EV['event']=='shot') | (EV['event']=='free_kick')) & (EV_tag['goal']==1)
Rank_goal = EV.loc[cond, 'player_id'].value_counts()
Rank_goal = Rank_goal.rename(index=dict_id_name)  # 選手IDを選手名に変換する
Rank_goal.iloc[:10]
Mohame_Salah      32
H_Kane            29
S_Agüero          21
J_Vardy           20
R_Sterling        18
R_Lukaku          16
Robert_Firmino    15
A_Lacazette       14
Gabrie_Jesus      13
G_Murray          12
Name: player_id, dtype: int64

6.4.6. 選手間のパス数#

最後に,特定の試合における選手間のパス数を可視化してみよう. 本来,このような解析にはnetworkxという専用のライブラリを使うべきだが,以下ではpandasとseabornという可視化ライブラリを用いて実装する.

試合の抽出

# 特定の試合を抽出
ev = EV.loc[EV['game_id']==2499719].copy()
ev_tag = EV_tag.loc[EV['game_id']==2499719].copy()

パスリストの作成

選手間のパス数を求めるには,パスの出し手と受け手の情報が必要である. しかし,イベントログEVにはパスの出し手の情報しかないので,受け手の情報を加える必要がある. イベント名が'pass'の行については,次の行の選手IDがパスの受け手に対応するので,以下のようにパスリストpass_listを作成できる. なお,イベントログには選手ID('player_id')の情報しかないので,選手プロフィールPLのデータを用いて選手名を追加する. 以下のように,replaceメソッドを用いて,選手ID('player_id')を選手名('name')に置換すれば良い.

# イベント名が'pass'の行を抽出
ev_pass = ev.loc[ev['event']=='pass', ['player_id', 'team_id']]

# パスリストの作成
pass_list = pd.DataFrame({'player_id': ev_pass['player_id'].values[:-1],
                   'player_id2': ev_pass['player_id'].values[1:],
                   'team_id': ev_pass['team_id'].values[:-1],
                   'team_id2': ev_pass['team_id'].values[1:]})

# パスリストに選手名を追加
pass_list['name'] = pass_list['player_id'].replace(PL['player_id'].values, PL['name'].values)
pass_list['name2'] = pass_list['player_id2'].replace(PL['player_id'].values, PL['name'].values)
pass_list
player_id player_id2 team_id team_id2 name name2
0 25413 370224 1609 1609 A_Lacazette R_Holding
1 370224 3319 1609 1609 R_Holding M_Özil
2 3319 120339 1609 1609 M_Özil Mohame_Elneny
3 120339 167145 1609 1609 Mohame_Elneny Bellerín
4 167145 3319 1609 1609 Bellerín M_Özil
... ... ... ... ... ... ...
830 217078 8488 1631 1631 D_Amartey W_Morgan
831 8488 265366 1631 1631 W_Morgan W_Ndidi
832 265366 8653 1631 1631 W_Ndidi H_Maguire
833 8653 8480 1631 1631 H_Maguire K_Schmeichel
834 8480 49876 1631 1609 K_Schmeichel G_Xhaka

835 rows × 6 columns

パス数行列の作成

チーム内の選手 \(i\)\(j\) 間のパス数を要素とする行列をパス数行列と呼ぶことにする. パス数行列の \((i, j)\) 成分は選手 \(i\) から \(j\) へのパスを表す. 以下はfor文によってパス数行列を作成する例である.

# チームIDの取得
tm_id = ev['team_id'].unique() # 2チームのチームID
pl_id0 = pass_list.loc[pass_list['team_id']==tm_id[0], 'name'].unique() # チーム0の選手ID
pl_id1 = pass_list.loc[pass_list['team_id']==tm_id[1], 'name'].unique() # チーム1の選手ID

# チーム0のパス数行列を作成
A0 = pd.DataFrame(index=pl_id0, columns=pl_id0, dtype=int)
for i in pl_id0:
    for j in pl_id0:
        A0.loc[i, j] = len(pass_list.loc[(pass_list['name']==i) & (pass_list['name2']==j)])

# チーム1のパス数行列を作成
A1 = pd.DataFrame(index=pl_id1, columns=pl_id1, dtype=int)
for i in pl_id1:
    for j in pl_id1:
        A1.loc[i, j] = len(pass_list.loc[(pass_list['name']==i) & (pass_list['name2']==j)])
A0
A_Lacazette R_Holding M_Özil Mohame_Elneny Bellerín G_Xhaka S_Kolašinac Nach_Monreal P_Čech A_Oxlade-Chamberlain D_Welbeck O_Giroud A_Ramsey T_Walcott
A_Lacazette 0.0 1.0 2.0 2.0 0.0 5.0 1.0 1.0 0.0 0.0 0.0 1.0 0.0 0.0
R_Holding 1.0 0.0 4.0 11.0 8.0 5.0 4.0 12.0 2.0 0.0 1.0 0.0 0.0 0.0
M_Özil 5.0 6.0 3.0 3.0 8.0 19.0 9.0 5.0 1.0 5.0 4.0 0.0 1.0 0.0
Mohame_Elneny 1.0 8.0 7.0 1.0 10.0 16.0 4.0 6.0 1.0 5.0 2.0 0.0 0.0 0.0
Bellerín 3.0 10.0 11.0 11.0 0.0 6.0 3.0 0.0 2.0 1.0 0.0 0.0 2.0 0.0
... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
A_Oxlade-Chamberlain 3.0 1.0 6.0 4.0 3.0 9.0 5.0 3.0 2.0 0.0 4.0 2.0 1.0 1.0
D_Welbeck 1.0 1.0 3.0 2.0 0.0 2.0 1.0 0.0 0.0 4.0 0.0 0.0 1.0 0.0
O_Giroud 1.0 0.0 1.0 0.0 0.0 0.0 0.0 0.0 0.0 2.0 0.0 0.0 1.0 0.0
A_Ramsey 0.0 0.0 1.0 0.0 2.0 0.0 1.0 1.0 0.0 1.0 0.0 1.0 0.0 0.0
T_Walcott 0.0 0.0 0.0 0.0 0.0 0.0 1.0 0.0 0.0 1.0 0.0 0.0 0.0 0.0

14 rows × 14 columns

パス数行列の可視化

パス数行列を可視化する方法はいくつか考えられる. 例えば,選手を点,選手間のパス数を線の太さに対応させた図で表す方法がある. このような図はネットワーク呼ばれ,サッカーのデータ分析における標準的な手法となっている. しかし,ネットワークの分析と可視化にはnetworkxなどの専用ライブラリの知識が必要となるので,ここではより直接的にヒートマップを用いた可視化方法を採用する. 以下のplot_corr_mat関数は,seabornという可視化ライブラリを用いてパス数行列をヒートマップで可視化する.

import seaborn
def plot_corr_mat(A):
    fig, ax = plt.subplots(figsize=(5, 5))

    # ヒートマップの作成
    seaborn.heatmap(A, ax=ax, 
                    linewidths=0.1, # セル間の線の太さ
                    linecolor='w',  # セル間の線の色
                    cbar=True,      # カラーバーの表示
                    annot=True,     # セルに値を表示
                    square=True,    # セルを正方形にする
                    cmap='jet',     # カラーマップの色('Reds', 'Greens', 'Blues'など)
                    cbar_kws={"shrink": .5} # カラーバーのサイズ
                    )
    ax.set_xticklabels(A.columns, fontsize=8) # x軸のラベル
    ax.set_yticklabels(A.index, fontsize=8)   # y軸のラベル
# チーム0のパス数行列のヒートマップ
plot_corr_mat(A0)
../_images/bad06c2e64e18e100b3cb4c33fef403376cdaaa023d41366577de1eded924992.png
# チーム1のパス数行列のヒートマップ
plot_corr_mat(A1)
../_images/91a6aca67318e45ca882b38e25fed3116a26001ccf3a7f1a2cf33cbcf0574593.png

6.4.7. 演習問題#

  • イベントログとイベントタグから様々な条件で場面を抽出し,ヒートマップを描画せよ.

  • イングランド以外のリーグについて,選手のランキングを求めよ.

  • 他のチームに対してもパス数行列を作成し,可視化せよ.

  • 相手へのパスに対してパス数行列を作成し,可視化せよ.