import pandas as pd
import numpy as np
import math
from mplsoccer import VerticalPitch, Sbopen
from sklearn.linear_model import LogisticRegression
import matplotlib.pyplot as plt
Giriş
Futbol dünyasında performans analizi her geçen gün daha da önem kazanıyor ve takımın ve oyuncuların potansiyelini daha iyi anlamak ve geliştirmek için kritik bir rol oynuyor. Bu bağlamda, Beklenen Gol veya kısaca xG (Expected Goals) metriği, son yıllarda futbol analizlerinde sıklıkla kullanılan bir araç haline gelmiştir. Bu yazıda, lojistik regresyon ile basit bir model kurarak hesapladığımız xG’leri StatsBomb ile karşılaştıracağız.
Kullanılacak Kütüphaneler
Basit xG Modelinin Oluşturulması
xG Tanımı
Benzer özelliklere sahip şutların tarihsel bilgilerine dayanarak, xG modeli her şuta 0 ile 1 arasında bir değer atar ve bu değer şutun gol üretme olasılığını ifade eder. Örneğin, 0.2 xG, bir şutun gol olma olasılığının %20 olduğunu ifade eder. xG’nin yüksek olması, bir oyuncunun veya takımın gol atma fırsatlarının iyi olduğunu gösterir.
Verilerin Çekilmesi
StatsBomb’dan verileri aşağıdaki gibi çekebiliriz. Euro 2024’ün competition_id
’si 55, season_id
’si 282’dir. Detaylı bilgiye buradan ulaşabilirsiniz.
= Sbopen()
parser = parser.match(competition_id=55, season_id=282) euro2024_matches
= {}
df
for match_id in euro2024_matches['match_id']:
= parser.event(match_id)
event_data, related_data, freeze_data, tactic_data = {
df[match_id] 'event': event_data,
'related': related_data,
'freeze': freeze_data,
'tactic': tactic_data
}
Yukarıdaki kod ile Euro 2024 maçlarının verilerini çektik ve maç ID’lerine göre df
adlı sözlükte sakladık.
= pd.DataFrame(columns=['x', 'y', 'outcome_name', 'shot_statsbomb_xg'])
df_shot
for match_id, match_data in df.items():
= match_data['event']
event_df = (
mask_shot 'type_name'] == 'Shot') &
(event_df['period'] <= 4) &
(event_df['sub_type_name'] == 'Open Play')
(event_df[
)= event_df.loc[mask_shot, ['x', 'y', 'outcome_name', 'shot_statsbomb_xg']]
shots_temp = pd.concat([df_shot, shots_temp], ignore_index=True) df_shot
Yukarıdaki kod ile Euro 2024 maçlarından sadece “open play” içindeki şut verilerini çektik ve bu verileri df_shot
adlı bir veri çerçevesinde topladık. Open play, oyunun durmadığını, serbest ve sürekli akışta olduğunu ifade eder. Ayrıca, penaltı atışlarını dikkate almamak için 4’ten küçük olanları aldık.
Genel olarak dokümantasyonlara buradan ulaşabilirsiniz.
Gol Olan ve Olmayan Şutların Konumlarının Gösterilmesi
= df_shot[df_shot['outcome_name'] == 'Goal'].copy()
df_goals = df_shot[df_shot['outcome_name'] != 'Goal'].copy()
df_non_goals
= VerticalPitch(
pitch =0.5,
pad_bottom=True,
half='box'
goal_type
)
= pitch.draw(figsize=(12, 10))
fig, ax
pitch.scatter('x'], df_non_goals['y'],
df_non_goals[='orange',
c='o',
marker=ax,
ax='No Goal',
label=.5
alpha
)
pitch.scatter('x'], df_goals['y'],
df_goals[='red',
c='o',
marker=ax,
ax='Goal'
label
)
"Shot Locations for Goals and Non-Goals in Euro 2024", fontsize=16)
ax.set_title(='upper right')
ax.legend(loc
plt.show()
Açı ve Uzaklığın Hesaplanması
Belirtilen noktadan iki kale direği arasındaki açıyı ve hedefin (kale direği) en yakın mesafesini hesaplayalım.
Ölçüler aşağıda verilmiştir.
def calculate_angle(x, y):
= np.array([120, 44]), np.array([120, 36]), np.array([x, y])
g0, g1, p = g0 - p, g1 - p
v0, v1 = np.arctan2(np.linalg.det([v0, v1]), np.dot(v0, v1))
angle
return abs(np.degrees(angle))
def calculate_distance(x, y):
= 120 - x
x_dist = 0
y_dist
if y < 36:
= 36 - y
y_dist elif y > 44:
= y - 44
y_dist
return math.sqrt(x_dist**2 + y_dist**2)
'angle'] = df_shot.apply(lambda row:calculate_angle(row['x'], row['y']), axis=1)
df_shot['distance'] = df_shot.apply(lambda row:calculate_distance(row['x'], row['y']), axis=1)
df_shot['goal'] = df_shot.apply(lambda row:1 if row['outcome_name'] == 'Goal' else 0, axis=1) df_shot[
Açı ve Uzaklığa Göre Gol ve Gol Olmayan Durumların Gösterilmesi
=(10, 6))
plt.figure(figsize
plt.scatter('goal'] == 0]['angle'],
df_shot[df_shot['goal'] == 0]['distance'],
df_shot[df_shot[='blue',
color='No Goal',
label=0.3
alpha
)
plt.scatter('goal'] == 1]['angle'],
df_shot[df_shot['goal'] == 1]['distance'],
df_shot[df_shot[='red',
color='Goal',
label=0.9
alpha
)'Angle')
plt.xlabel('Distance')
plt.ylabel('Angle vs Distance by Goal Outcome in Euro 2024')
plt.title(
plt.legend()True)
plt.grid( plt.show()
Modelin Kurulması
= df_shot[['angle', 'distance']]
X = df_shot['goal']
y = LogisticRegression().fit(X, y) model
xG Hesaplayacak Fonksiyonun Yazılması
def calculate_xg(x, y):
= calculate_angle(x, y)
angle = calculate_distance(x, y)
distance = model.predict_proba([[angle, distance]])[:, 1][0]
xg
return xg
Maçların xG Değerlerinin Tahmin Edilmesi
Bir takımın xG’si tüm xG’lerin toplamıdır.
\(xG = \sum_{i=1}^{n} xG_i\)
= euro2024_matches[['match_id', 'home_team_name', 'away_team_name']].copy()
df_summary 'home_goals_open_play'] = None
df_summary['home_xg_open_play'] = None
df_summary['home_xg_sb_open_play'] = None
df_summary['away_goals_open_play'] = None
df_summary['away_xg_open_play'] = None
df_summary['away_xg_sb_open_play'] = None
df_summary[
for i, match_id in enumerate(euro2024_matches['match_id']):
= df[match_id]['event']
df_shots = (df_shots['type_name'] == 'Shot') & (df_shots['period'] <= 4) & (df_shots['sub_type_name'] == 'Open Play')
shot_mask = df_shots[shot_mask].copy()
df_shots 'calculated_xg'] = df_shots.apply(lambda row: calculate_xg(row['x'], row['y']), axis=1)
df_shots[
= euro2024_matches['home_team_name'][i]
home_team = euro2024_matches['away_team_name'][i]
away_team
= df_shots[df_shots['team_name'] == home_team].copy()
df_home 'home_goals_open_play'] = len(df_home[df_home['outcome_name'] == 'Goal'])
df_summary.at[i, 'home_xg_open_play'] = df_home['calculated_xg'].sum()
df_summary.at[i, 'home_xg_sb_open_play'] = df_home['shot_statsbomb_xg'].sum()
df_summary.at[i,
= df_shots[df_shots['team_name'] == away_team].copy()
df_away 'away_goals_open_play'] = len(df_away[df_away['outcome_name'] == 'Goal'])
df_summary.at[i, 'away_xg_open_play'] = df_away['calculated_xg'].sum()
df_summary.at[i, 'away_xg_sb_open_play'] = df_away['shot_statsbomb_xg'].sum() df_summary.at[i,
Tahmin Sonuçlarının StatsBomb ile Karşılaştırılması
=(12, 8))
plt.figure(figsize
plt.scatter('home_xg_open_play'],
df_summary['home_xg_sb_open_play'],
df_summary[=0.7, color='green',
alpha='Home Team: Calculated vs StatsBomb'
label
)
plt.scatter('away_xg_open_play'],
df_summary['away_xg_sb_open_play'],
df_summary[=0.7, color='purple',
alpha='Away Team: Calculated vs StatsBomb'
label
)
'Calculated xG')
plt.xlabel('StatsBomb xG')
plt.ylabel('Comparison of xG: Calculated vs StatsBomb for Home and Away Teams (Euro 2024)')
plt.title(
plt.legend()True)
plt.grid( plt.show()
Gelecek içeriklerde görüşmek dileğiyle.