Plotting football data on goal-scoring with the help of GoalBusters
By: Zsombor Hegedus
Tags: Football, Seaborn, Exploratory Data Analysis, Football Analytics, Goalbuster

Table of Contents

Introduction Describing the data Exploratory data analysis Summary

Introduction

I started following football in 2004 after the memorable win of Greece in the UEFA Euro tournament. It was fascinating to see how 22 players running around with a ball can make people feel such deep emotions. Fans know the names, the tactics, the errors; they enjoy the attacks, blame the referees when their teams are penalized — it’s a huge mix of affections and I slowly fell for it. When I developed my appreciation for data, football naturally became one of my favorite fields to experiment with, but I always struggled to get anywhere because of the lack of publicly available data. This is when I met GoalBusters who gave me the opportunity to work with them on a short while who gave me access to their rich database.

In this article, I will work with a data set on football matches as part of my University Capstone project. I wish to show a few plots about goal-scoring probabilities using the seaborn package and to give a glimpse into what GoalBusters has at their disposal. I also embedded some Python codes that I used for analysis may it help for the others in similar matters.

If you share my passion for football and wish to see an amazing analytics site, make sure you hit up GoalBusters website on BetAlertPro.com . You can sign-up for a free subscription and with that the doors will open to you not only to some intra-match statistics but also to their sophisticated analytics as well.

Describing the data

Okay so let’s see what we are dealing with. I received 2 datasets from them dated between 2020 November and December, with tons of data on:

— Match statistics: This is a dataset with historic match statistics with minute granularity, so one can track the development of a game pretty easily. Most sites offer aggregate match statistics with end-of-match results only, so this one is truly a wonderful source to be analyzed. The dataset has a stunning 52 variables that include not only the core characteristics like shots on goal, number of corners, etc… but to mention a few, we have the number of dangerous attacks, attacking scores, substitutions even odds data.

— Team statistics: This dataset has historic data on teams from 23 different leagues (main leagues like La Liga, Premier League, Bundesliga, Ligue 1 and Serie A included) with 72 variables. Other than standard information like how many goals given team has scored or how many times they suffered defeat, information like how many times given team equalized or how many own goals they scored is there as well.

This is plenty of data that allows GoalBusters to provide subscribers with plenty of accurate predictions using cutting-edge technology. I will concentrate only on answering two simple questions that I had in mind when I received this big chunk of data. I will also reduce the scope to some important variables only.

EDA

First I needed to do some transformations on my match stats data as extra times were encoded in a way that it was not easy to order my observations. I then filtered out a few variables to concentrate on the most important ones. The below screenshot shows the structure of the table used for the analysis.

It’s important to note that each observation corresponds to one minute of a match. Let’s see how the numeric variables correlate with each other if we look at the whole dataset.

import pandas as pd

import seaborn as sns #df is read from disk df = df.astype({

'league_id':'object',

'home_shots': 'int32',

'away_shots': 'int32',

'home_corner': 'int32',

'away_corner': 'int32',

'home_rc' : 'int32',

'away_rc': 'int32'

}) qual = ['status', 'league_id']

quant = ['away_shots', 'home_corner', 'away_corner', 'home_rc', 'away_rc'] sns.heatmap(df[quant+Y].corr(), annot=True)

I think there are two interesting factors here. On the one hand, nothing seems to be correlated to the goal_event dummy variable. This is probably due to goals not occurring very often in the data (only 3 % of the observations have goals, so a goal on average occurs every 33 minutes), but it also signals that it is not easy to predict with these variables only when a goal will occur. On the other hand, it’s interesting to see how corners and shots correlate, but it shouldn’t fool us. Higher correlation is probably partly due to how these variables are represented in the data — they are positive numbers with monotonous increases so of course, they will move together. But there might be some causality there as well since shots and headers many times occur after a corner is kicked.

Staying on the subject of correlation, let’s see what the pairplot function can show us:

sns.pairplot(df[quant+ ['goal_event']], hue='goal_event', vars=quant);

pairplot s are great even for such discrete variables as the ones we have here. Blue dots correspond to data points where no goal happened, while orange ones represent the goals. These plots are just to further show that there are no clear patterns in the data neither for minutes where goal occurred or minutes without goals.

Now that we saw this let’s answer a simple question — when is it most likely that a goal will occur. A match is 90 minutes, but more minutes might come in extra time after min 45 and min 90. They will be encoded with decimals e.g. 1st minute of extra time is going to be 45.01 now. Let’s see what we can see in the data.

# Set up a grid to plot goal probability against several variables

g = sns.PairGrid(df, y_vars="goal_event",

x_vars=['status'],

height=9, aspect = 2) # Draw a seaborn pointplot onto each Axes

g.map(sns.pointplot, scale=1.3, errwidth=4, color="xkcd:gray")

g.set(ylim=(0, .2))

plt.xticks(rotation=45)

sns.despine(fig=g.fig, left=True

This is not very informative is it? Maybe grouping variables by 10 minute frequencies can show a more comprehensible picture? Let’s see about that:

def encode_status(status):

status = np.where(status < 10, 'first 10 minutes',

np.where(status < 20, '10-20 minutes',

np.where(status < 30, '20-30 minutes',

np.where(status < 40, '30-40 minutes',

np.where(status<45,'40-45 minutes',

np.where(status<46,'first half extratime',

np.where(status < 50, '45-50 minutes',

np.where(status < 60, '50-60 minutes',

np.where(status < 70, '60-70 minutes',

np.where(status < 80, '70-80 minutes',

np.where(status < 90,'80-90 minutes','second half extra time')))))))))))

return status df['encoded_status'] = encode_status(df['status']) fig, ax = plt.subplots(figsize=(12,6))

plt.rcParams.update({'font.size': 50}) sns.set_context("paper")

sns.set_style("ticks")

sns.pointplot(data= df, x = 'encoded_status', y = 'goal_event', join=True)

ax.set_ylabel('Goal frequency', fontsize = 14)

ax.set_xlabel('')

ax.tick_params(axis = 'x', labelsize = 12)

ax.tick_params(axis = 'y', labelsize = 12)

plt.xticks(rotation=45)

This is much nicer and it is quite informative. In my sample, goals occured more times in the second half of the match, but they had the highest frequency in extra times. I really like to use seaborn’s pinpoint s as they also show the confidence intervals (in our case this is 95%) — they also show that the highest frequency is not simply a coincidence but it is significantly higher than in other minute categories. Teams should withstand the pressure the most when they probably are the most tired, at the very end of each half.

Let’s answer the second simple question — are goal frequency similar for every league?

fig, ax = plt.subplots(figsize=(12,8))

sns.set_context("paper")

sns.set_style("ticks")

sns.pointplot(data= df, x = 'goal_event', y = 'league_name', orient = 'h', join=False)

ax.set_ylabel('')

ax.set_xlabel('Goal frequency', fontsize = 14)

ax.tick_params(axis = 'x', labelsize = 12)

ax.tick_params(axis = 'y', labelsize = 12)

As quoted earlier goal frequency is on average 3%, or we can say that on average goals occur every 33 minutes. But we still see some differences between leagues that are notable — for example, if someone prefers matches with a lot of goals, they should watch the Bundesliga, the Brasilian league. Something interesting is going on with the Finish first league as it has a huge confidence interval around an average that is twice the size of the others. After giving it a closer look it turns out I had only two matches out of which one had a massive goal count.

Summary

In this article I wanted to give a quick tour and showcase some visualisation from the aggregates of the data that I received from GoalBusters. I only used a very narrow set of variables to answer two simple questions related to goal scoring probabilities. Seaborn has great visual elements as demonstrated in this article that could show meaningful results with a few lines of code. If you want to know more, don’t forget to check out BetAlertPro.com.