10.3 Project

Bivariate Statistics

For this project, let’s begin to work with some soccer data from the 2018 World Cup. This data is availailable on github, and we can read it directly into a pandas DataFrame.

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

First let’s import the necessary libraries, and read the data. to look at it.

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns

url = "https://raw.githubusercontent.com/nkmwicz/worldcup2018data/refs/heads/main/cleaned_events_world_cup2018.csv"
df = pd.read_csv(url)
df.head()
eventId subEventName tags playerId matchId eventName teamId matchPeriod eventSec subEventId id x1 y1 x2 y2
0 8 Simple pass ['Accurate'] Mohammad Ibrahim Al Sahlawi Russia - Saudi Arabia, 5 - 0 Pass Saudi Arabia 1H 1.656214 Simple pass 258612104 50 50 35.0 53.0
1 8 High pass ['Accurate'] Abdullah Ibrahim Otayf Russia - Saudi Arabia, 5 - 0 Pass Saudi Arabia 1H 4.487814 High pass 258612106 35 53 75.0 19.0
2 1 Air duel ['Won', 'Accurate'] Ilya Kutepov Russia - Saudi Arabia, 5 - 0 Duel Russia 1H 5.937411 Air duel 258612077 25 81 37.0 83.0
3 1 Air duel ['Lost', 'Not accurate'] Yasir Gharsan Al Shahrani Russia - Saudi Arabia, 5 - 0 Duel Saudi Arabia 1H 6.406961 Air duel 258612112 75 19 63.0 17.0
4 8 Simple pass ['Accurate'] Salman Mohammed Al Faraj Russia - Saudi Arabia, 5 - 0 Pass Saudi Arabia 1H 8.562167 Simple pass 258612110 63 17 71.0 15.0

First, this data is event-level data, meaning each row is a single event that happened in a match. Events include passes, shots, fouls, etc. Each event has a matchId and a teamId to indicate which match and team the event belongs to. The important columns for us are eventName, which tells us what type of event it is, and tags, which is a list of tags that give more information about the event. For example, a shot event may have a tag indicating whether or not it was a goal. Unfortunately, however, this data comes from a csv file, so the tags column is a string representation of a list. We can use the ast.literal_eval function to convert this string representation back into a list.

import ast  # for literal_eval of the tags column

df["tags"] = df["tags"].apply(ast.literal_eval)

Now, let’s look at the available data in the eventName column. We can do that by using the unique() function.

df["eventName"].unique()
array(['Pass', 'Duel', 'Free Kick', 'Foul', 'Others on the ball', 'Shot',
       'Save attempt', 'Offside', 'Goalkeeper leaving line'], dtype=object)

We can see that there are a variety of event types. For this project, let’s focus on passes, shots, and goals. We can create new columns in the DataFrame to indicate whether or not each event is a pass, shot, or goal. But the question is how do we identify goals? There is not a goal event type. Let’s look at the tags for a possible answer. The problem is that tags are a list, so .unique() won’t work here. It will not check inside the lists. Instead, we need to flatten the list of lists into a single list. We can do that with a list comprehension. To explain a nested list comprehension, I like to first create the full loop:

all_tags = []
for sublist in df["tags"]:
    for tag in sublist:
        all_tags.append(tag)

Here we can see we need two sequential for loops to get each tag from each sublist. To convert this code into a list comprehension, we just need to put the tag variable at the front, and then line up the second for loop after the first. This gives us:

all_tags = [tag for sublist in df["tags"] for tag in sublist]
unique_tags = list(set(all_tags))  # remove duplicates
unique_tags.sort()  # sorts the list in place
unique_tags
['Accurate',
 'Anticipated',
 'Anticipation',
 'Assist',
 'Blocked',
 'Counter attack',
 'Dangerous ball lost',
 'Direct',
 'Fairplay',
 'Feint',
 'Free space left',
 'Free space right',
 'Goal',
 'Head/body',
 'High',
 'Indirect',
 'Interception',
 'Key pass',
 'Left foot',
 'Lost',
 'Missed ball',
 'Neutral',
 'Not accurate',
 'Opportunity',
 'Own goal',
 'Position: Goal center',
 'Position: Goal center left',
 'Position: Goal center right',
 'Position: Goal high center',
 'Position: Goal high left',
 'Position: Goal high right',
 'Position: Goal low center',
 'Position: Goal low left',
 'Position: Goal low right',
 'Position: Out center left',
 'Position: Out center right',
 'Position: Out high center',
 'Position: Out high left',
 'Position: Out high right',
 'Position: Out low left',
 'Position: Out low right',
 'Position: Post center left',
 'Position: Post center right',
 'Position: Post high center',
 'Position: Post high left',
 'Position: Post high right',
 'Position: Post low left',
 'Position: Post low right',
 'Red card',
 'Right foot',
 'Second yellow card',
 'Sliding tackle',
 'Take on left',
 'Take on right',
 'Through',
 'Won',
 'Yellow card']

We can see that Goal exists in the tags, so we can use that to identify goals.

Now let’s create a few new columns to indicate whether or not each event is a pass, shot, or goal. We can either use boolean indexing or the apply() function. Here, I’ll use a combination of both.

df["pass"] = df['eventName'] == "Pass"
df["shot"] = df.apply(lambda x: "Shot" == x["eventName"], axis=1).astype(int)
df["goal"] = df.apply(lambda x: "Goal" in x['tags'] and x["eventName"] == "Shot", axis=1).astype(int)
df['pass'] = df['pass'].astype(int)

df.head()
eventId subEventName tags playerId matchId eventName teamId matchPeriod eventSec subEventId id x1 y1 x2 y2 pass shot goal
0 8 Simple pass [Accurate] Mohammad Ibrahim Al Sahlawi Russia - Saudi Arabia, 5 - 0 Pass Saudi Arabia 1H 1.656214 Simple pass 258612104 50 50 35.0 53.0 1 0 0
1 8 High pass [Accurate] Abdullah Ibrahim Otayf Russia - Saudi Arabia, 5 - 0 Pass Saudi Arabia 1H 4.487814 High pass 258612106 35 53 75.0 19.0 1 0 0
2 1 Air duel [Won, Accurate] Ilya Kutepov Russia - Saudi Arabia, 5 - 0 Duel Russia 1H 5.937411 Air duel 258612077 25 81 37.0 83.0 0 0 0
3 1 Air duel [Lost, Not accurate] Yasir Gharsan Al Shahrani Russia - Saudi Arabia, 5 - 0 Duel Saudi Arabia 1H 6.406961 Air duel 258612112 75 19 63.0 17.0 0 0 0
4 8 Simple pass [Accurate] Salman Mohammed Al Faraj Russia - Saudi Arabia, 5 - 0 Pass Saudi Arabia 1H 8.562167 Simple pass 258612110 63 17 71.0 15.0 1 0 0

Now that we have these columns we can, let’s group the data by matchId and teamId. We can use the sum aggregation function to get the total number of passes, shots, and goals for each team in each match. This works because we created binary columns where 1 indicates the event happened, and 0 indicates it did not. Pandas recognized that our new columns are booleans, so when we used .astype(int), we converted them to 0s and 1s. Therefore, summing these columns gives us the total number of each event type.

g_passes = df.groupby(["matchId", "teamId"]).agg({
    "pass": "sum",
    "goal": "sum",
    "shot": "sum",
})
g_passes.reset_index(inplace=True)
g_passes
matchId teamId pass goal shot
0 Argentina - Croatia, 0 - 3 Argentina 476 0 10
1 Argentina - Croatia, 0 - 3 Croatia 334 3 13
2 Argentina - Iceland, 1 - 1 Argentina 744 1 22
3 Argentina - Iceland, 1 - 1 Iceland 183 1 8
4 Australia - Peru, 0 - 2 Australia 512 0 11
... ... ... ... ... ...
123 Uruguay - Portugal, 2 - 1 Uruguay 238 2 6
124 Uruguay - Russia, 3 - 0 Russia 327 0 3
125 Uruguay - Russia, 3 - 0 Uruguay 466 1 12
126 Uruguay - Saudi Arabia, 1 - 0 Saudi Arabia 553 0 7
127 Uruguay - Saudi Arabia, 1 - 0 Uruguay 473 1 13

128 rows × 5 columns

We can now group these all by teamId to get total games and total number of goals, shots, etc.

team_stats = g_passes.groupby("teamId").agg({
    "matchId": "count",
    "pass": "mean",
    "goal": "mean",
    "shot": "mean"
})
team_stats.reset_index(inplace=True)
team_stats
teamId matchId pass goal shot
0 Argentina 4 562.750000 1.500000 11.750000
1 Australia 3 470.000000 0.000000 8.333333
2 Belgium 7 498.857143 2.000000 14.000000
3 Brazil 5 535.400000 1.600000 19.000000
4 Colombia 4 443.000000 1.250000 8.750000
5 Costa Rica 3 341.333333 0.333333 8.666667
6 Croatia 7 529.714286 1.714286 14.714286
7 Denmark 4 392.000000 0.750000 9.500000
8 Egypt 3 394.666667 0.333333 8.666667
9 England 7 518.714286 1.142857 11.000000
10 France 7 418.714286 1.285714 9.714286
11 Germany 3 645.333333 0.333333 21.666667
12 Iceland 3 251.333333 0.333333 10.000000
13 Iran 3 195.666667 0.000000 5.000000
14 Japan 4 489.500000 1.250000 9.000000
15 Korea Republic 3 295.333333 1.000000 10.333333
16 Mexico 4 393.750000 0.500000 13.000000
17 Morocco 3 369.000000 0.666667 11.333333
18 Nigeria 3 359.666667 0.666667 11.666667
19 Panama 3 318.000000 0.333333 6.666667
20 Peru 3 426.000000 0.666667 9.000000
21 Poland 3 457.666667 0.666667 8.666667
22 Portugal 4 457.000000 1.000000 10.250000
23 Russia 5 329.400000 1.600000 8.400000
24 Saudi Arabia 3 567.000000 0.333333 10.666667
25 Senegal 3 312.333333 1.000000 8.000000
26 Serbia 3 360.333333 0.333333 10.000000
27 Spain 4 822.750000 1.500000 15.000000
28 Sweden 5 283.600000 0.600000 9.400000
29 Switzerland 4 504.750000 1.250000 12.750000
30 Tunisia 3 446.666667 1.333333 10.666667
31 Uruguay 5 412.000000 1.000000 10.200000

Now that we have the stats, we can begin to check the correlation.

team_stats.corr(numeric_only=True)
matchId pass goal shot
matchId 1.000000 0.242743 0.673839 0.260202
pass 0.242743 1.000000 0.427120 0.666682
goal 0.673839 0.427120 1.000000 0.360757
shot 0.260202 0.666682 0.360757 1.000000
from scipy.stats import pearsonr

for col in team_stats.columns:
    if pd.api.types.is_numeric_dtype(team_stats[col]):
        r, p = pearsonr(team_stats["pass"], team_stats[col])
        print(f"pass to {col}--r: {r}, p: {round(p, 4)}")
pass to matchId--r: 0.24274261628733085, p: 0.1807
pass to pass--r: 1.0, p: 0.0
pass to goal--r: 0.42711954522711015, p: 0.0148
pass to shot--r: 0.666682272587523, p: 0.0
ax = sns.regplot(x=team_stats["pass"], y=team_stats["shot"], line_kws={"color": "r"})
ax.set_title("Correlation: passes/game to shots/game")
text = f"r={round(r, 4)}\np={round(p, 4)}"
ax.text(0.75, 0.1, text, fontsize=12, ha="left", va="baseline", transform=ax.transAxes)
Text(0.75, 0.1, 'r=0.6667\np=0.0')

g_passes.corr(numeric_only=True)
pass goal shot
pass 1.000000 0.120015 0.564229
goal 0.120015 1.000000 0.210675
shot 0.564229 0.210675 1.000000
r, p = pearsonr(x=g_passes["pass"], y=g_passes["shot"])
print(r, round(p, 6))
0.5642294416907079 0.0
gx = sns.regplot(x=g_passes["pass"], y=g_passes["shot"], line_kws={"color": "r"})
gx.set_title("Correlation between Passes and Shots")
Text(0.5, 1.0, 'Correlation between Passes and Shots')

f, (plt1, plt2) = plt.subplots(2, gridspec_kw={"height_ratios": (0.15, 0.85)})

sns.boxplot(g_passes["pass"], ax=plt1, orient="h")
sns.histplot(data=g_passes, x="pass", kde=True, color="blue")

plt1.set(yticks=[])
plt1.set(xticks=[])
plt1.set_xlabel("")
plt.axvline(g_passes["pass"].mean(), color="r")
sns.despine(ax=plt2)
sns.despine(ax=plt1, left=True, bottom=True)