# (必須)モジュールのインポート
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 |
チームの所属リーグ |
以下では,イングランド・プレミアリーグのデータを解析対象とする.
そこで,条件付き抽出を用いて,TM
とGM
からイングランド・プレミアリーグのデータだけ抽出する.
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) \) は二項分布に従う:
この式において,\( 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 \) を取ることで
と求まる. これをポアソン分布と呼ぶ. ポアソン分布は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);
次に,上のヒストグラムがポアソン分布に従っているか調べるため,試合データから求めた平均値をパラメータとするポアソン分布を描いてみる. イングランド・プレミアリーグのホームチームの平均得点は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>]
上のグラフを見比べると,確かに似た分布になっていることが分かる. そこで,最後に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);
実データ(棒グラフ)とポアソン分布(折れ線)の概形はおおよそ一致していることが分かる. これは,得点の平均値と分散が近い値になったことと共に,サッカーの得点分布がポアソン分布に従うことを裏付ける材料となる. もちろん,サッカーの得点分布が普遍的にポアソン分布に従うかどうかは,他のリーグのデータを調べなければわからない(確かめてみよ). また,\(\chi^2\)検定などを用いてより定量的な検証を行うことも必要である(試してみよ).
6.3.4. 演習問題#
他のリーグについて,ホームチームまたはアウェイチームの得点分布を描画し,さらに実データから求めた平均値をパラメータとするポアソン分布を同じグラフに描画せよ.
\( \chi^2 \) 検定を用いて,サッカーの得点分布がポアソン分布に従うかどうか検証せよ.
6.4. イベントデータの解析#
Pappalardoデータセットのイベントデータはイベントログとイベントタグの2種類から成る:
イベントログ
パスやシュートなどのボールに関わるイベントに対して,起きた時刻,場所,関わった選手などの基本情報が紐付けられたデータ
1試合あたり1500~2000イベント
イベントタグ
イベントログの各イベントに対して,より詳細な付加情報が紐付けられたデータ
イベントデータにはボールに関わるほぼ全てのプレー情報が含まれているため,これを分析すれば詳細な試合展開を把握することができる. イベントデータは選手プロフィールや得点データに比べて格段に情報量が多いため,その扱いの難易度も高い. 基本的にExcelで解析するのは困難であり,Pandasの本領が最も発揮されるデータといえる.
6.4.1. 加工済みデータの内容#
加工済みデータの詳細およびダウンロード用リンクを以下にまとめる.
※ W杯とCLのデータはヨーロッパリーグと試合数が異なるので除外する.
内容 |
ファイル |
ファイルサイズ |
---|---|---|
イベントIDとイベント名の対応 |
0.9KB |
|
イベントに付与されるタグの説明 |
2KB |
|
各試合のイベントデータ(イングランド) |
イベントログ:event_England.csv |
58MB |
//(フランス) |
イベントログ:event_France.csv |
57.6MB |
//(ドイツ) |
イベントログ:event_Germany.csv |
47.2MB |
//(イタリア) |
イベントログ:event_Italy.csv |
58.9MB |
//(スペイン) |
イベントログ:event_Spain.csv |
56.1MB |
以下では,イングランド・プレミアリーグのデータを解析対象とする. 準備として,次のファイルをダウンロードしてカレントディレクトリに移動する:
イベントログ:event_England.csv
イベントタグ:event_tag_England.csv
選手プロフィール:player.csv
これらを以下のように読み込んでおく.
# イベントデータと選手プロフィールの読み込み
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$');
イベントタグ
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
には,ボールに関わるイベントに関するほぼ全ての情報が含まれている.
イベントデータ解析の目的はこれらのデータから意味のある情報を抽出することである.
イベントデータを解析する際の手順は以下のようにまとめられる:
イベントログ
EV
,イベントタグEV_tag
から必要な行を条件付き抽出する条件付き抽出したデータを集計する
集計したデータを可視化する
以下では,条件付き抽出の例をいくつか示す.
特定の試合・時間帯の抽出
# 特定の試合を抽出する
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)
# 特定の選手のパス
cond = (EV['event']=='pass') & (EV['player_id']==167145)
x, y = EV.loc[cond, 'x1'], EV.loc[cond, 'y1']
event_hmap(x, y, 'Blues')
# クロス
cond = (EV['subevent']=='cross')
x, y = EV.loc[cond, 'x1'], EV.loc[cond, 'y1']
event_hmap(x, y, 'Reds')
# デュエル
cond = (EV['event']=='duel')
x, y = EV.loc[cond, 'x1'], EV.loc[cond, 'y1']
event_hmap(x, y, 'Greys')
# デュエル(攻撃時)
cond = (EV['subevent']=='ground_attacking_duel')
x, y = EV.loc[cond, 'x1'], EV.loc[cond, 'y1']
event_hmap(x, y, 'jet')
# シュート
cond = (EV['event']=='shot')
x, y = EV.loc[cond, 'x1'], EV.loc[cond, 'y1']
event_hmap(x, y)
# シュート(成功)
cond = (EV['event']=='shot') & (EV_tag['goal']==1)
x, y = EV.loc[cond, 'x1'], EV.loc[cond, 'y1']
event_hmap(x, y)
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)
# チーム1のパス数行列のヒートマップ
plot_corr_mat(A1)
6.4.7. 演習問題#
イベントログとイベントタグから様々な条件で場面を抽出し,ヒートマップを描画せよ.
イングランド以外のリーグについて,選手のランキングを求めよ.
他のチームに対してもパス数行列を作成し,可視化せよ.
相手へのパスに対してパス数行列を作成し,可視化せよ.