Proyek Visualisasi Data Shot Map di Sepak Bola dengan Python

Analisis dan Visualisasi Data

PojokStats
8 min readNov 14, 2022
Gol! (Chaos Soccer Gear)

Pengantar

Shot map merupakan sebuah visualisasi yang menampilkan lokasi dan hasil dari peluang yang dilakukan dalam satu pertandingan.

Dataset yang PojokStats gunakan sebagai acuan seperti tabel di bawah ini:

Di dalam tabel di atas menunjukkan nama tim Team, nilai peluang xG, nama pemain Player, lokasi peluang diciptakan kordinat x dan y, terakhir waktu terjadinya sebuah peluang Minutes.

Jika tertarik untuk membuat shot map seperti ini, Anda hanya perlu mengganti beberapa variabel saja sesuai kebutuhan yang diinginkan:

  • home_team
  • away_team
  • home_color
  • away_color

Menyiapkan Perangkat

  1. Google Colab
    https://colab.research.google.com
  2. Mengambil lokasi peluang tercipta x,y, dan Event: https://fcpythonvideocoder.netlify.app/
  3. Mengetahui nilai xG berdasarkan lokasi x dan y: https://torvaney.github.io/projects/xG.html

Menyiapkan Library (Perpustakaan)

Data visualisasi ini membutuhkan beberapa library penting untuk mengolah dataset, library disiapkan dalam proyek ini adalah:

  • mplsoccer
  • matplotlib
  • numpy
  • pandas
  • highlight_text

Menyiapkan library dapat dilakukan dengan cara berikut:

pip install <nama_library>

mplsoccer

# Menginstal mplsoccer
pip install mplsoccer
Jika installasi maplsoccer pada notebook berhasil akan terlihat seperti ini. (PojokStats)

matplotlib

# Menginstal matplotlib
!pip install matplotlib
Jika installasi matplotlib pada notebook berhasil akan terlihat seperti ini. (PojokStats)

Setelah instalasi mplsoccer dan matplotlib selesai, langkah berikutnya adalah mengimpor library tersebut bersama dengan numpy dan pandas.

numpy dan pandas

# Mengimpor library yang dibutuhkan
import numpy as np
import pandas as pd
from matplotlib import cm
import matplotlib.pyplot as plt
from matplotlib.lines import Line2D
from matplotlib.colors import ListedColormap
import matplotlib.patheffects as path_effects
import matplotlib.patches as mpatches
import matplotlib.image as mpimg
from mplsoccer import VerticalPitch, Pitch
from mplsoccer.cm import create_transparent_cmap
from mplsoccer.utils import FontManager

Menyiapkan Data

Satuan menit menggunakan tipe data float, sehingga berbentuk dalam format desimal. Misalnya suatu peluang terjadi pada menit 5 detik 40, maka menit yang diinput adalah 5.4.

Kemudian simpan dengan format .csv dan upload ke Google Drive, jangan lupa untuk membagikan file dengan format share link to anyone agar data dapat dibaca oleh siapa saja. Jika ingin memuat data dari Google Drive bisa menggunakan koding seperti di bawah ini:

# Memuat dataframe dari Google Drive
file = '1byA5HdomR6GFU0oQQcNsK6B_8DC5yGNv'
url = f'https://drive.google.com/uc?id={file}'
df = pd.read_csv(url)
df.head()

Cara untuk mendapatkan nama file adalah dengan menyalin tautan yang sudah dibuat Salin link.

Menyalin tautan file yang sudah diunggah ke Google Drive. (PojokStats)

Kemudian tempel ke bar pencarian pada browser, lalu salin pada bagian ini 1bya5HdomR6GFU0oQQcNsK6B_8DC5yGNv.

Menyalin tautan file yang sudah diunggah ke Google Drive. (PojokStats)
Data frame Persikabo 1973 vs Persebaya. (PojokStats)

Cara lainnya jika ingin mengolah data secara lokal di komputer sendiri, cukup masukan data di dalam folder yang sama dengan file python. Jika ingin memuat datanya bisa menggunakan koding seperti ini di bawah ini:

# Memuat dataframe dari Komputer
df = pd.read_csv('namafile_csv')

Menyiapkan Jenis Huruf (Font)

# Memuat Jenis Huruf (Font)
font_1 = FontManager(('https://github.com/dharmatype/Bebas-Neue/blob/master/fonts/BebasNeue(2014)ByFontFabric/'
'BebasNeue-Bold.ttf?raw=true'))
font_2 = FontManager(('https://github.com/EverRest/EvSe/blob/master/fonts/AlternateGothic2/'
'alternate-gothic-no2-bt.ttf?raw=true'))

Menyeleksi Event Berdasarkan Tim

Langkah berikunya adalah membagi Event peluang yang terjadi berdasarkan masing-masing tim menggunakan variabel home_team dan away_team.

# Menyiapkan variabel
home_team = "Persikabo 1973"
away_team = "Persebaya"
# Menyiapkan dataframe tim kandang
df_home = df[(df["Team"] == home_team)]
df_home.head()
Data frame Persikabo 1973 vs Persebaya. (PojokStats)
Data frame tim kandang Persikabo 1973. (PojokStats)

Kemudian jika ingin mencek nilai peluang xG tiap Event yang berhasil dicatatkan oleh Persikabo 1973.

# Mengecek nilai peluang (xG) setiap event
xG_home_all_event = np.round(np.sum(df_home['xG']),2)
xG_home_all_event
# Menyiapkan dataframe tim tandang
df_home = df[(df["Team"] == away_team)]
df_home.head()
Data frame tim tandang Persebaya. (PojokStats)
# Mengecek nilai peluang (xG) semua event
xG_away_all_event = np.round(np.sum(df_away['xG']),2)
xG_away_all_event
Nilai peluang (xG) Persebaya. (PojokStats)

Menyeleksi Event Berdasarkan Jenis Outcome

Dalam Event dalam dataframe terdapat beberapa outcome yang telah diklasifikasikan, seperti: goal, saved, blocked, dan miss. Sehingga diperlukan seleksi untuk membagi berdasarkan hasil dari peluang yang dilakukan oleh pemain.

# Memisahkan jenis peluang berdasarkan tim
# Seleksi peluang yang menghasilkan gol (goal)
df_home_goal = df_home[(df_home["Event"] == "Goal")]
df_away_goal = df_away[(df_away["Event"] == "Goal")]
# Dataframe gol tim kandang (goal)
df_home_goal
Peluang yang menghasilkan gol untuk Persikabo 1973. (PojokStats)
# Dataframe gol tim tandang (goal)
df_away_goal
Tidak ada peluang yang menghasilkan gol untuk Persebaya. (PojokStats)
# Memisahkan jenis peluang berdasarkan tim
# Dataframe peluang yang berhasil ditangkap kiper (saved)
df_home_saved = df_home[(df_home["Event"] == "Saved")]
df_away_saved = df_away[(df_away["Event"] == "Saved")]
# Dataframe peluang tim kandang yang berhasil ditangkap kiper (saved)
df_home_saved
Peluang Persikabo 1973 yang berhasil ditangkap oleh kiper Persebaya. (PojokStats)
# Dataframe peluang tim tandang yang berhasil ditangkap kiper (saved)
df_away_saved
Peluang Persebaya yang berhasil ditangkap oleh kiper Persikabo 1973. (PojokStats)
# Memisahkan jenis peluang berdasarkan tim
# Seleksi peluang yang berhasil diblok kiper (blocked)
df_home_block = df_home[(df_home["Event"] == "Blocked")]
df_away_block = df_away[(df_away["Event"] == "Blocked")]
# Dataframe peluang tim kandang yang berhasil diblok kiper (blocked)
df_home_block
Tidak ada peluang Persikabo 1973 yang berhasil diblok kiper Persebaya. (PojokStats)
# Dataframe peluang tim tandang yang berhasil diblok kiper (blocked)
df_away_block
Peluang Persebaya yang berhasil diblok kiper Persikabo 1973. (PojokStats)
# Memisahkan jenis peluang berdasarkan tim
# Seleksi peluang yang melenceng dari gawang (miss)
df_home_miss = df_home[(df_home["Event"] == "Miss")]
df_away_miss = df_away[(df_away["Event"] == "Miss")]
# Seleksi peluang tim kandang yang melenceng dari gawang (miss)
df_home_miss
Peluang Persikabo 1973 yang melenceng dari gawang. (PojokStats)
# Seleksi peluang tim tandang yang melenceng dari gawang (miss)
df_away_miss
Peluang Persebaya yang melenceng dari gawang. (PojokStats)

Menyiapkan Visualisasi

Persiapan yang perlu dilakukan di bawah ini antara lain:

  1. Pengaturan warna yang dipakai untuk membedakan warna home_team dengan away_team dengan variabel home_color dan away_color
  2. Daftar lokasi goal berdasarkan x dan y peluang tercipta
  3. Menampilkan nama pencetak goal ke dalam visualisasi
  4. Menghitung masing-masing tipe peluang yang tercipta
# Pengaturan tiap tim
home_color = '#000000'
away_color = '#036933'

# Pengaturan warna map
cmap_home = create_transparent_cmap(color= home_color,n_segments=100,alpha_start=0.2,alpha_end=0.8)
cmap_away = create_transparent_cmap(color= away_color,n_segments=100,alpha_start=0.2,alpha_end=0.8)

# Daftar posisi goal
x = df_home_goal[df_home_goal['Event']=='Goal']['x'].tolist()
x1 = df_away_goal[df_away_goal['Event']=='Goal']['x'].tolist()
y = df_home_goal[df_home_goal['Event']=='Goal']['y'].tolist()
y1 = df_away_goal[df_away_goal['Event']=='Goal']['y'].tolist()

# Anotasi teks nama pencetak gol
text_home = df_home_goal[df_home_goal['Event']=='Goal']['Player'].tolist()
text_away = df_away_goal[df_away_goal['Event']=='Goal']['Player'].tolist()

# Menghitung jumlah masing-masih tipe peluang
# Kandang (Home)
cnt_goal_home = len(df_home_goal)
cnt_saved_home = len(df_home_saved)
cnt_blocked_home = len(df_home_block)
cnt_missed_home = len(df_home_miss)
# Tandang (Away)
cnt_goal_away = len(df_away_goal)
cnt_saved_away = len(df_away_saved)
cnt_blocked_away = len(df_away_block)
cnt_missed_away = len(df_away_miss)

Memulai Visualisasi

Library mplsoccer, dapat digunakan untuk membuat plot lapangan secara otomatis. Standar nilai x dan y yang dipakai menggunakan type dari wyscout.

Dalam pembuatan visualisasi khususnya mengenai pembuatan lapangan dan standar lapangan yang dipakai dapat dilihat melalui link berikut ini: https://mplsoccer.readthedocs.io/en/latest/gallery/pitch_setup/plot_pitches.html

# Menampilkan tipe lapangan dan warna yang digunakan
pitch = Pitch(pitch_type = 'wyscout', pitch_color='#fefefe', line_color='#000000', linewidth=1, goal_type = 'box')
fig,ax = pitch.draw(figsize = (16,9))
fig.set_facecolor('#000000')

# Menampilkan lokasi dan nilai peluang yang menghasilkan saved shot berdasarkan nilai xG
sc1 = plt.scatter(df_home_saved['x'],df_home_saved['y'],
s = ((df_home_saved['xG'] * 1900) + 150),
cmap = cmap_home,
c = df_home_saved['xG'],
edgecolor = 'black',
marker = 'D')
sc2 = plt.scatter(df_away_saved['x'],df_away_saved['y'],
s = ((df_away_saved['xG'] * 1900) + 150),
cmap = cmap_away,
c = df_away_saved['xG'],
edgecolor = 'black',
marker = 'D')

# Menampilkan posisi dan nilai peluang yang menghasilkan blocked shot berdasarkan nilai xG
sc3 = plt.scatter(df_home_block['x'],df_home_block['y'],
s = ((df_home_block['xG'] * 1900) + 150),
cmap = cmap_home,
c = df_home_block['xG'],
edgecolor = 'black',
marker = 'o')
sc4 = plt.scatter(df_away_block['x'],df_away_block['y'],
s = ((df_away_block['xG'] * 1900) + 150),
cmap = cmap_away,
c = df_away_block['xG'],
edgecolor = 'black',
marker = 'o')

# Menampilkan posisi dan nilai peluang yang menghasilkan miss shot berdasarkan nilai xG
sc5 = plt.scatter(df_home_miss['x'],df_home_miss['y'],
s = ((df_home_miss['xG'] * 1900) + 150),
cmap = cmap_home,
c = df_home_miss['xG'],
edgecolor = 'black',
marker = 'X')

sc6 = plt.scatter(df_away_miss['x'],df_away_miss['y'],
s = ((df_away_miss['xG'] * 1900) + 150),
cmap = cmap_away,
c = df_away_miss['xG'],
edgecolor = 'black',
marker = 'X')

#Menampilkan posisi dan nilai peluang yang menghasilkan goal berdasarkan nilai xG
sc7 = plt.scatter(df_home_goal['x'],df_home_goal['y'],
s = (df_home_goal['xG'] * 1900) + 150,
c = home_color,
edgecolor = 'black',
marker = '*')

sc8 = plt.scatter(df_away_goal['x'],df_away_goal['y'],
s = ((df_away_goal['xG'] * 1900) + 150),
c = away_color,
edgecolor = 'black',
marker = '*')

#Teks tambahan
ax.text(x=50, y=7, s='Persikabo 1973 v Persebaya',
size=25, fontproperties=font_1.prop, color='white', backgroundcolor = '#000000' , va='center', ha='center')

ax.text(x=50, y=15, s='{} Goal {}'.format(cnt_goal_home,cnt_goal_away), size=18,
fontproperties=font_2.prop, color='white', backgroundcolor = '#000000' ,
va='center', ha='center')

ax.text(x=50, y=20, s='{} Saved {}'.format(cnt_saved_home,cnt_saved_away), size=18,
fontproperties=font_2.prop, color='white', backgroundcolor = '#000000' ,
va='center', ha='center')

ax.text(x=50, y=25, s='{} Blocked {}'.format(cnt_blocked_home,cnt_blocked_away), size=18,
fontproperties=font_2.prop, color='white', backgroundcolor = '#000000' ,
va='center', ha='center')

ax.text(x=50, y=30, s='{} Missed {}'.format(cnt_missed_home,cnt_missed_away), size=18,
fontproperties=font_2.prop, color='white', backgroundcolor = '#000000' ,
va='center', ha='center')

ax.text(x=50, y=35, s='{} xG {}'.format(xG_home_all_event,xG_away_all_event), size=18,
fontproperties=font_2.prop, color='white', backgroundcolor = '#000000' ,
va='center', ha='center')

ax.text(x=50, y=105, s='Data Visualization by PojokStats',
size=18, fontproperties=font_2.prop, color='white', backgroundcolor = '#000000' ,
va='center', ha='center')

# Legenda
goal = Line2D([0], [0], marker='*', markersize=np.sqrt(30), color='black', linestyle='None')
saved = Line2D([0], [0], marker='D', markersize=np.sqrt(30), color='black', linestyle='None')

blocked = Line2D([0], [0], marker='o', markersize=np.sqrt(30), color='black', linestyle='None')
missed = Line2D([0], [0], marker='X', markersize=np.sqrt(30), color='black', linestyle='None')

plt.legend([goal, saved, blocked, missed], ['Goal', 'Saved', 'Blocked', "Missed"], loc="lower right",
markerscale=1.5, scatterpoints=1, fontsize=10, facecolor = '#fefefe', edgecolor = '#000000')

# Anotasi teks untuk pencetak Gol
for i in range(len(x1)):
plt.annotate(text_away[i], (x1[i], y1[i] + 5.5),c='black',size=14, fontproperties=font_2.prop, ha='center')
for i in range(len(x)):
plt.annotate(text_home[i], (x[i], y[i] - 3.5),c='black',size=14, fontproperties=font_2.prop, ha='center')

#Save Image
plt.savefig("SM_Persikabo1973vPersebaya.png", bbox_inches = "tight", dpi = 400)
Volia! Shot map sudah jadi. (PojokStats)

Analisis

xG Timeline

Dari grafis xG timeline di atas, menunjukkan bahwa bagaimana Persikabo 1973 dan Persebaya menciptakan peluang yang relatih sama, kedua tim masih saling berhati-hati dalam melakukan serangan. Baru selepas 15 menit pertama Persebaya mulai mendominasi dalam menciptakan peluang namun gagal dikonversi menjadi gol hingga turun minum.

Persikabo 1973 selepas turun minum berkat kecerdikan dari Coach Djanur, bisa memanfaatkan 15 menit kedua dengan menekan pertahanan Persebaya hingga mendapatkan penalti dan berbuah gol.

Setelah terjadinya gol Gustavo pada 53 untuk keunggulan Persikabo 1973, permainan lebih terbuka. Kedua tim sama-sama saling menciptakan peluang, sayang sampai akhir pertandingan tidak ada lagi gol yang tercipta. Pertandingan berakhir dengan keunggulan 1–0 untuk Persikabo 1973.

Catatan

Dalam visualisasi data peluang sepak bola ini, perlu dipahami bahwa model xG yang digunakan berdasarkan histori xG dari Premier League 2012/13, 2013/14, dan 2014/15. Sehingga akan kurang relevan jika digunakan untuk menggambarkan nilai xG Liga 1 Indonesia dalam ruang lingkup profesional.

Jika ingin mendapatkan nilai xG yang relevan dengan Liga 1 Indonesia perlu riset mendalam dengan mengumpulkan data xG dari beberapa musim ke belakang. Tapi setidaknya dengan adanya model tersebut, bisa membantu Anda memahami visuliasi data peluang dalam sepak bola Indonesia.

--

--

PojokStats

Research and Analysis | Mainly focus on the #Liga1 and #Persebaya | #FootballAnalytics | #PojokStats