import pandas as pd
import numpy as np
import math
from mplsoccer import Sbopen
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import mean_squared_error
import matplotlib.pyplot as plt
Giriş
Lojistik Regresyon ile xG Modelinin Basit Bir Şekilde Oluşturulması başlıklı yazıda xG modelini basit bir şekilde oluşturmuştuk. Bu yazıda ise xG modelini biraz daha ileri düzeye taşıyacağız.
Kullanılacak Kütüphaneler
İleri Düzey xG Modelinin Oluşturulması
Modele Eklenecek Yeni Değişkenler
Önceki modelde, şutu çeken oyuncunun kaleye olan açısını (angle
) ve uzaklığını (distance
) modele eklemiştik. Peki, başka hangi değişkenleri modele ekleyebiliriz? StatsBomb’un dokümantasyonundan görüntülerle destekleyerek yazalım.
Şutun tipi (
sub_type_name
)- Corner: Köşe vuruşundan doğrudan yapılan şut.
- Free Kick: Doğrudan serbest vuruşla yapılan şut.
- Open Play: Açık oyunda yapılan şut.
- Penalty: Penaltı vuruşundan yapılan şut.
- Kick Off: Maçın başında veya ikinci yarının başında yapılan direkt vuruş.
Şutun tekniği (
technique_name
)- Backheel: Topukla yapılan şut.
- Diving Header: Oyuncunun topa dalarak kafa vuruşu yaptığı şut.
- Half Volley: Top yere çarptıktan sonra, havada iken yapılan şut.
- Lob: Topun yüksek bir yay çizerek, rakip oyuncunun üstünden geçmesi için yapılan şut.
- Normal: Diğer tekniklerden hiçbirine uymayan şut.
- Overhead Kick: Oyuncunun sırtı kaleye dönük olarak yaptığı şut.
- Volley: Top yere değmeden önce yapılan şut.
Vücudun kullanılan yeri (
body_part_name
)- Head: Kafa ile yapılan şut.
- Left Foot: Sol ayak ile yapılan şut.
- Other: Diğer vücut parçaları (örneğin diz, göğüs vb.) ile yapılan şut.
- Right Foot: Sağ ayak ile yapılan şut.
- Baskı altında mı değil mi? (
under_pressure
)
Rakip tarafından baskı altında yapılan bir şut olup olmadığıdır.
Genel olarak dokümantasyonlara buradan ulaşabilirsiniz.
Verilerin Çekilmesi
Veri çerçevesini yeni kolonlar ile genişletiyoruz.
= 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
}
= [
new_columns 'x',
'y',
'outcome_name',
'sub_type_name',
'body_part_name',
'under_pressure',
'technique_name',
'shot_statsbomb_xg'
]= pd.DataFrame(columns=new_columns)
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[
)= event_df.loc[mask_shot, new_columns]
shots_temp = pd.concat([df_shot, shots_temp], ignore_index=True) df_shot
Açı ve Uzaklığın Hesaplanması
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[
Gol Durumunun Belirlenmesi
'goal'] = df_shot.apply(lambda row:1 if row['outcome_name'] == 'Goal' else 0, axis=1) df_shot[
Kolonların NaN Kontrolünün Yapılması ve Düzeltilmesi
= ['sub_type_name', 'body_part_name', 'under_pressure', 'technique_name']
columns_to_count
for col in columns_to_count:
= df_shot[col].isna().sum()
nan_count print(f"Column '{col}' has {nan_count} NaN values.")
# Column 'sub_type_name' has 0 NaN values.
# Column 'body_part_name' has 0 NaN values.
# Column 'under_pressure' has 929 NaN values.
# Column 'technique_name' has 0 NaN values.
'under_pressure'] = df_shot['under_pressure'].fillna(0) df_shot[
Modele Eklenecek Değişkenlerin Dağılımı
= plt.subplots(nrows=2, ncols=2, figsize=(14, 16))
fig, axes = axes.flatten()
axes
for i, col in enumerate(columns_to_count):
= df_shot[col].value_counts().sort_values()
count_series ='barh', ax=axes[i], color='skyblue')
count_series.plot(kind'_', ' ').title())
axes[i].set_title(col.replace('Count')
axes[i].set_xlabel('')
axes[i].set_ylabel(
"Shot Types in Various Categories at Euro 2024", fontsize=16)
fig.suptitle(
plt.tight_layout() plt.show()
Kategorik Veriler İçin One Hot Encoding Yapılması
= pd.get_dummies(df_shot, columns=['sub_type_name', 'body_part_name', 'technique_name']) df_shot
Modelin Kurulması
= [
X_cols 'angle',
'distance',
'under_pressure',
'sub_type_name_Corner',
'sub_type_name_Free Kick',
'sub_type_name_Open Play',
'sub_type_name_Penalty',
'body_part_name_Head',
'body_part_name_Left Foot',
'body_part_name_Right Foot',
'technique_name_Backheel',
'technique_name_Diving Header',
'technique_name_Half Volley',
'technique_name_Lob',
'technique_name_Normal',
'technique_name_Overhead Kick',
'technique_name_Volley'
]
= df_shot[X_cols]
X = df_shot['goal']
y
= LogisticRegression().fit(X, y) model
xG Hesaplayacak Fonksiyonun Yazılması
def calculate_xg(x, y, under_pressure, sub_type_name_dummies, body_part_name_dummies, technique_name_dummies):
= calculate_angle(x, y)
angle = calculate_distance(x, y)
distance = np.array(
features +
[angle, distance, under_pressure] list(sub_type_name_dummies) +
list(body_part_name_dummies) +
list(technique_name_dummies)
1, -1)
).reshape(= model.predict_proba(features)[:, 1][0]
xg
return xg
Maçların xG Değerlerinin Tahmin Edilmesi
= 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 = (
shot_mask 'type_name'] == 'Shot') &
(df_shots['period'] <= 4)
(df_shots[
)= df_shots[shot_mask].copy()
df_shots
= pd.get_dummies(df_shots['sub_type_name'], prefix='sub_type_name')
sub_type_dummies = pd.get_dummies(df_shots['body_part_name'], prefix='body_part_name')
body_part_dummies = pd.get_dummies(df_shots['technique_name'], prefix='technique_name')
technique_dummies
= sub_type_dummies.reindex(
sub_type_dummies =['sub_type_name_Corner', 'sub_type_name_Free Kick', 'sub_type_name_Open Play', 'sub_type_name_Penalty'], fill_value=0
columns
)= body_part_dummies.reindex(
body_part_dummies =['body_part_name_Head', 'body_part_name_Left Foot', 'body_part_name_Right Foot'], fill_value=0
columns
)= technique_dummies.reindex(
technique_dummies =['technique_name_Backheel', 'technique_name_Diving Header', 'technique_name_Half Volley', 'technique_name_Lob', 'technique_name_Normal', 'technique_name_Overhead Kick', 'technique_name_Volley'], fill_value=0
columns
)
= pd.concat([df_shots, sub_type_dummies, body_part_dummies, technique_dummies], axis=1)
df_shots
'under_pressure'] = df_shots['under_pressure'].fillna(0)
df_shots[
'calculated_xg'] = df_shots.apply(
df_shots[lambda row: calculate_xg(
'x'],
row['y'],
row['under_pressure'],
row[
sub_type_dummies.loc[row.name].values,
body_part_dummies.loc[row.name].values,
technique_dummies.loc[row.name].values
),=1
axis
)
= 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()
RMSE’nin Hesaplanması
\(\text{RMSE} = \sqrt{\frac{1}{n} \sum_{i=1}^{n} (y_i - \hat{y}_i)^2}\)
= df_summary['home_xg_open_play'] + df_summary['away_xg_open_play']
combined_xg_open_play = df_summary['home_xg_sb_open_play'] + df_summary['away_xg_sb_open_play']
combined_xg_sb_open_play
= mean_squared_error(combined_xg_open_play, combined_xg_sb_open_play)
rmse_advanced print(f"RMSE of the advanced model: {rmse_advanced}")
Yaklaşık 0.20’lik bir RMSE elde ettik. Önceki basit model ile 0.22’lik bir RMSE elde ediyoruz. Her ne kadar önemli bir fark olmasa da RMSE’yi düşürmüş olduk.
Gelecek içeriklerde görüşmek dileğiyle.