Project

Bivariate Visualization

Let’s build a scatter plot with our Soccer data.

import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
import ast

url = "https://raw.githubusercontent.com/nkmwicz/worldcup2018data/refs/heads/main/cleaned_events_world_cup2018.csv"

df = pd.read_csv(url)

df['tags'] = df['tags'].apply(ast.literal_eval)
shots = df.loc[df['eventName'] == 'Shot']
shots['goal'] = shots['tags'].apply(lambda x: "Goal" in x).astype(int)
shots
/tmp/ipykernel_356794/2378109605.py:12: SettingWithCopyWarning:


A value is trying to be set on a copy of a slice from a DataFrame.
Try using .loc[row_indexer,col_indexer] = value instead

See the caveats in the documentation: https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#returning-a-view-versus-a-copy
eventId subEventName tags playerId matchId eventName teamId matchPeriod eventSec subEventId id x1 y1 x2 y2 goal
117 10 Shot [Right foot, Blocked, Not accurate] Yasir Gharsan Al Shahrani Russia - Saudi Arabia, 5 - 0 Shot Saudi Arabia 1H 327.142941 Shot 258612248 87 27 100.0 100.0 0
154 10 Shot [Right foot, Blocked, Not accurate] Alexandr Samedov Russia - Saudi Arabia, 5 - 0 Shot Russia 1H 407.123899 Shot 258612244 90 69 0.0 0.0 0
197 10 Shot [Left foot, Blocked, Not accurate] Yuri Zhirkov Russia - Saudi Arabia, 5 - 0 Shot Russia 1H 526.276996 Shot 258612307 86 30 0.0 0.0 0
232 10 Shot [Goal, Head/body, Opportunity, Position: Goal ... Yury Gazinskiy Russia - Saudi Arabia, 5 - 0 Shot Russia 1H 693.396917 Shot 258612368 93 60 0.0 0.0 1
372 10 Shot [Left foot, Blocked, Not accurate] Mohammad Ibrahim Al Sahlawi Russia - Saudi Arabia, 5 - 0 Shot Saudi Arabia 1H 1266.276267 Shot 258612558 86 38 100.0 100.0 0
... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
101371 10 Shot [Goal, Right foot, Opportunity, Position: Goal... Mario Mand\u017euki\u0107 France - Croatia, 4 - 2 Shot Croatia 2H 1389.182669 Shot 263885208 92 53 100.0 100.0 1
101457 10 Shot [Right foot, Opportunity, Position: Out center... \u0160ime Vrsaljko France - Croatia, 4 - 2 Shot Croatia 2H 1802.299261 Shot 263885299 65 61 100.0 100.0 0
101493 10 Shot [Left foot, Blocked, Not accurate] Ivan Rakiti\u0107 France - Croatia, 4 - 2 Shot Croatia 2H 1933.996341 Shot 263885348 81 33 100.0 100.0 0
101620 10 Shot [Left foot, Opportunity, Position: Goal center... Nabil Fekir France - Croatia, 4 - 2 Shot France 2H 2487.443304 Shot 263885485 78 76 0.0 0.0 0
101653 10 Shot [Right foot, Opportunity, Position: Out high r... Ivan Rakiti\u0107 France - Croatia, 4 - 2 Shot Croatia 2H 2589.084194 Shot 263885575 70 29 100.0 100.0 0

1419 rows × 16 columns

Now we have the shots. Let’s find out how close the shots are from their target. This dataset creates a grid from 0-100 on the x and y axes. The attacking team is always shooting toward the goal on the 100 x-axis. The center point of the goal is (100,50). The grid, then, works as a percentage system. Knowing that, we can use the Euclidean distance computation to find the distance after we convert all coordinates into real meters.

To convert the location on the pitch into a meter marker. We can set the average length to 105 meters and average width to 70.

avg_ln = 105
avg_wd = 70

shots["m_x1"] = shots["x1"].apply(lambda x: (x / 100) * avg_ln)
shots["m_x2"] = shots["x2"].apply(lambda x: (x / 100) * avg_ln)
shots["m_y1"] = shots["y1"].apply(lambda x: (x / 100) * avg_wd)
shots["targetx"] = 105
shots["targety"] = 70 * 0.5
shots.head()
/tmp/ipykernel_356794/2739217068.py:4: SettingWithCopyWarning:


A value is trying to be set on a copy of a slice from a DataFrame.
Try using .loc[row_indexer,col_indexer] = value instead

See the caveats in the documentation: https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#returning-a-view-versus-a-copy

/tmp/ipykernel_356794/2739217068.py:5: SettingWithCopyWarning:


A value is trying to be set on a copy of a slice from a DataFrame.
Try using .loc[row_indexer,col_indexer] = value instead

See the caveats in the documentation: https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#returning-a-view-versus-a-copy

/tmp/ipykernel_356794/2739217068.py:6: SettingWithCopyWarning:


A value is trying to be set on a copy of a slice from a DataFrame.
Try using .loc[row_indexer,col_indexer] = value instead

See the caveats in the documentation: https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#returning-a-view-versus-a-copy

/tmp/ipykernel_356794/2739217068.py:7: SettingWithCopyWarning:


A value is trying to be set on a copy of a slice from a DataFrame.
Try using .loc[row_indexer,col_indexer] = value instead

See the caveats in the documentation: https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#returning-a-view-versus-a-copy

/tmp/ipykernel_356794/2739217068.py:8: SettingWithCopyWarning:


A value is trying to be set on a copy of a slice from a DataFrame.
Try using .loc[row_indexer,col_indexer] = value instead

See the caveats in the documentation: https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#returning-a-view-versus-a-copy
eventId subEventName tags playerId matchId eventName teamId matchPeriod eventSec subEventId ... x1 y1 x2 y2 goal m_x1 m_x2 m_y1 targetx targety
117 10 Shot [Right foot, Blocked, Not accurate] Yasir Gharsan Al Shahrani Russia - Saudi Arabia, 5 - 0 Shot Saudi Arabia 1H 327.142941 Shot ... 87 27 100.0 100.0 0 91.35 105.0 18.9 105 35.0
154 10 Shot [Right foot, Blocked, Not accurate] Alexandr Samedov Russia - Saudi Arabia, 5 - 0 Shot Russia 1H 407.123899 Shot ... 90 69 0.0 0.0 0 94.50 0.0 48.3 105 35.0
197 10 Shot [Left foot, Blocked, Not accurate] Yuri Zhirkov Russia - Saudi Arabia, 5 - 0 Shot Russia 1H 526.276996 Shot ... 86 30 0.0 0.0 0 90.30 0.0 21.0 105 35.0
232 10 Shot [Goal, Head/body, Opportunity, Position: Goal ... Yury Gazinskiy Russia - Saudi Arabia, 5 - 0 Shot Russia 1H 693.396917 Shot ... 93 60 0.0 0.0 1 97.65 0.0 42.0 105 35.0
372 10 Shot [Left foot, Blocked, Not accurate] Mohammad Ibrahim Al Sahlawi Russia - Saudi Arabia, 5 - 0 Shot Saudi Arabia 1H 1266.276267 Shot ... 86 38 100.0 100.0 0 90.30 105.0 26.6 105 35.0

5 rows × 21 columns

Now that we have the new meter markers for the location, we can apply the distance of the shot.

dist applies the Euclidean distance formula to find the distance between two points in a 2D space. We can use the math.dist function to calculate the distance between the shot and the target goal.

It essentially does this: ((x2 - x1)**2 + (y2 - y1)**2)**(1/2)

from math import dist
shots['dist'] = shots.apply(lambda row: dist([row['m_x1'], row['m_y1']], [105, 35]), axis=1)
shots['dist']
/tmp/ipykernel_356794/4109726942.py:2: SettingWithCopyWarning:


A value is trying to be set on a copy of a slice from a DataFrame.
Try using .loc[row_indexer,col_indexer] = value instead

See the caveats in the documentation: https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#returning-a-view-versus-a-copy
117       21.107641
154       16.945206
197       20.300000
232       10.150000
372       16.930741
            ...    
101371     8.658522
101457    37.548003
101493    23.229561
101620    29.408332
101653    34.761185
Name: dist, Length: 1419, dtype: float64

Now we have the distance of every shot that was taken and if it was a goal or not. We can now plot a number of variables here. First let’s plot the locations on the pitch from which a goal was shot. We can do this in matplotlib.

hm = {1: 'green', 0: 'red'}
shots['color'] = shots['goal'].map(hm)
plt.figure(figsize=(10,5))
plt.scatter(x=shots['x1'], y=shots['y1'], c=shots['color'])
plt.xlim([0,100])
plt.ylim([0,100])
plt.xlabel('Pitch Length')
plt.ylabel('Pitch Width')
plt.title('Locations of Shots on Goal')
/tmp/ipykernel_356794/1183847092.py:2: SettingWithCopyWarning:


A value is trying to be set on a copy of a slice from a DataFrame.
Try using .loc[row_indexer,col_indexer] = value instead

See the caveats in the documentation: https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#returning-a-view-versus-a-copy
Text(0.5, 1.0, 'Locations of Shots on Goal')

We can probably make this much better by separating goals from non-goals

goals = shots.loc[shots["goal"] == 1]
ngoals = shots.loc[shots["goal"] == 0]
ngoals["x1"] = ngoals["x1"].apply(lambda x: 100 - x)
ngoals["y1"] = ngoals["y1"].apply(lambda x: 100 - x)
goals["cluster"] = pd.cut(goals["dist"], bins=6, labels=range(6))

plt.figure(figsize=(8, 5))
plt.scatter(x=goals["x1"], y=goals["y1"], c="g")
plt.scatter(x=ngoals["x1"], y=ngoals["y1"], c="r")
plt.xlim([0, 100])
plt.ylim([0, 100])
plt.axvline(x=(6 / 105) * 105, color="black", linestyle="--", linewidth=2)
plt.axvline(x=(18 / 100) * 105, color="black", linestyle="--", linewidth=2)
plt.axvline(x=(100-(6 / 100) * 105), color="black", linestyle="--", linewidth=2)
plt.axvline(x=(100- (18 / 100) * 105), color="black", linestyle="--", linewidth=2)
plt.show()
/tmp/ipykernel_356794/1938120467.py:3: SettingWithCopyWarning:


A value is trying to be set on a copy of a slice from a DataFrame.
Try using .loc[row_indexer,col_indexer] = value instead

See the caveats in the documentation: https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#returning-a-view-versus-a-copy

/tmp/ipykernel_356794/1938120467.py:4: SettingWithCopyWarning:


A value is trying to be set on a copy of a slice from a DataFrame.
Try using .loc[row_indexer,col_indexer] = value instead

See the caveats in the documentation: https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#returning-a-view-versus-a-copy

/tmp/ipykernel_356794/1938120467.py:5: SettingWithCopyWarning:


A value is trying to be set on a copy of a slice from a DataFrame.
Try using .loc[row_indexer,col_indexer] = value instead

See the caveats in the documentation: https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#returning-a-view-versus-a-copy

Okay, now let’s have a look at some of the passing.

df['pass'] = df['eventName'].apply(lambda x: 'Pass' in x).astype(int)
df['acc_pass'] = df.apply(lambda row: row['eventName'] == 'Pass' and 'Accurate' in row['tags'], axis=1).astype(int)

df['shot'] = df['eventName'] == 'Shot'
df['goal'] = df.apply(lambda row: row['eventName']=='Shot' and 'Goal' in row['tags'] ,axis=1)

g_df = df.groupby(['matchId', 'teamId']).agg({
    'eventId': 'count',
    'pass': 'sum',
    'acc_pass': 'sum',
    'shot': 'sum',
    'goal': 'sum'
}).reset_index()
g_df['pass_perc'] = g_df['acc_pass'] / g_df['pass']
g_df.corr(numeric_only=True)
eventId pass acc_pass shot goal pass_perc
eventId 1.000000 0.944824 0.924844 0.610822 0.084189 0.603061
pass 0.944824 1.000000 0.995563 0.564229 0.120015 0.741263
acc_pass 0.924844 0.995563 1.000000 0.555309 0.125879 0.788865
shot 0.610822 0.564229 0.555309 1.000000 0.210675 0.392210
goal 0.084189 0.120015 0.125879 0.210675 1.000000 0.162325
pass_perc 0.603061 0.741263 0.788865 0.392210 0.162325 1.000000
import scipy.stats as stats

m, b, r, p, err = stats.linregress(g_df["acc_pass"], g_df["shot"])

nums = range(g_df["acc_pass"].max())

plt.plot(nums, m * nums + b, "r--")

text = f"""
m: {round(m, 4)}\n
r: {round(r, 4)}\n
p: {round(p, 4)}
"""

ax = sns.scatterplot(data=g_df, x="acc_pass", y="shot")
ax.text(0.8, 0.1, text, transform=ax.transAxes)
Text(0.8, 0.1, '\nm: 0.0201\n\nr: 0.5553\n\np: 0.0\n')

We can do this much more easily by useing the Seaborn regplot

ax = sns.regplot(data=g_df, x="acc_pass", y="shot", line_kws={'color': 'red'})
m, b, r, p, err = stats.linregress(g_df["acc_pass"], g_df["shot"])

text = f"""
m: {round(m, 3)}\n
p: {round(p, 3)}\n
r: {round(r, 3)}
"""
ax.text(0.8, 0.1, text, transform=ax.transAxes)
sns.despine()

We could also create a joint plot, which will join a histogram along the opposite x and y axes of the scatter plot to better show distribution.

ax = sns.jointplot(data=g_df, x='acc_pass', y='shot')

ax = sns.jointplot(data=g_df, x='acc_pass', y='shot', kind='kde')

x = g_df["acc_pass"]
y = g_df["shot"]
z = g_df["pass_perc"]

ax = plt.figure().add_subplot(projection="3d")
ax.scatter(x, y, z)
ax.set_title("Passes")
plt.show()