Heatmaps Grid Walkthrough

Sam Gustafson
8 min readJun 25, 2021

--

In this walkthrough, I’ll be making some heatmaps in Python with the mplsoccer package. I’ll be using Jupyter Notebooks for my code. The data I’m using is some pass reception data for La Liga, which you can download here if you want to follow along.

Step 1: Importing packages + loading data

#imports
import pandas as pd
import math
import matplotlib.pyplot as plt
from mplsoccer import Pitch, VerticalPitch
from matplotlib.colors import LinearSegmentedColormap
import ipywidgets as widgets
#read data
df = pd.read_csv("WalkthroughLaLiga.csv")

Note: You can copy and paste these code blocks into your notebook.

Pretty basic stuff here, just importing the packages we will use to read the data, create pitches, create a colormap, etc., and then loading in the csv.

Step 2: Making a list of the different player names (optional)

#make a list of the unique receiver names
options = df["receiver"].unique()
options = options.tolist()

This is part of an optional process I will walk through that uses widgets to help you pick which players you want to include. This code takes all of the unique/different player names in the “receiver” column, and then turns that into a list.

Step 3: Making a widget with the player list (optional)

#use a widget to select a playeroptions = [x for x in options if str(x) != 'nan']playerlist = widgets.Combobox(
# value='John',
placeholder='Enter a name',
options=options,
description='Player:',
ensure_option=True,
disabled=False
)
playerlist

This creates a combo box widget that will allow you to select from all of the different player names. Upon running this, you should get a textbox pop up below the cell like this:

When you click in that box, a scroll bar thingy will pop up with each player’s name. You can scroll up and down and pick the player you want, and, as you start typing in the box, the list will cut down to only the players whose names include those letters. For instance, if I type “ssi” in the box, the only options remaining will be Yassine Bounou, Lionel Messi, and Oussama Idrissi.

When you find the player you want, select them and move on. You should leave that cell with it looking something like this:

Step 4: Assign a variable to a player name

player1 = playerlist.value# or just do it like this
player1 = "Lionel Messi"

Here, we are making a variable called “player1” and setting it to the name of the player we want. We can do this with the “playerlist” widget we just created by setting the variable to the value we put into the box, or we can just type out the player’s name in quotes.

Obviously the second one seems simpler here, but I like to just set up the widget because it prevents time wasting with things like misspellings, lack of accents, or having to type out the entirety of a name like André-Frank Zambo Anguissa.

Step 5: Filter the dataframe

#filter dataframe to only include passes received by that player
player1df = df[df['receiver'] == player1]
#filter for passes received in the attacking half
player1df = player1df[player1df['end_x'] >= 50]

This bit of code creates a new dataframe, “player1df”, which only includes passes received by our desired player. The last line then uses the x coordinate of the end location of the event to filter for only passes that were received in the attacking half — in this case, the player’s own goal line is 0 and the opposition goal line is 100.

Your new dataframe should look like this:

Step 6: Create a colormap

#create your colormap (insert your own list of hex codes or color names)
#the lowest level of this one (#edece9) is the same color as the pitch, and then it progresses up to dark red
customcmap = LinearSegmentedColormap.from_list('custom cmap', ['#edece9', '#fee5d9', '#fc9e80', '#db2824', '#67000d'])

Here, you can make a custom colormap, so your heatmaps can have exactly the colors you want. The hex codes I have within the bracket here set the colors. In this heatmap, the lowest values/frequencies will be #edece9, while the highest will be #67000d.

Step 7: Plotting

There is a lot that gets run in the same cell here, but I’ll try to break it up into sections below the code.

#set your mplsoccer pitch characteristics
pitch = VerticalPitch(pitch_type='opta', figsize = (8,7),
pitch_color='#edece9', line_color='#082630',half = True,
constrained_layout=True, tight_layout=True, line_zorder=1, linewidth=4.5, spot_scale=0.006)
#create your grid of pitches
#nrows sets the number of rows, ncols sets the number of cols
#title height sets the portion of the fig height left for the title, grid height does the same for the grid of pitches
#space lets you determine how much height is between each individual pitch
fig, ax = pitch.grid(nrows=4, ncols=3, figheight=30,
endnote_height=0.0, endnote_space=0,
# Turn off the endnote/title axis. I usually do this after
# I am happy with the chart layout and text placement
#those comments from the mplsoccer docs themselves, figured I'd just leave them in
axis=False,
title_height=0.03, grid_height=0.87, space=0.1)
#set the face color of the figure, I like having this as the same color as my pitches
fig.set_facecolor('#edece9')
#set your title
fig.suptitle("Where do players receive passes?",fontsize=55, color="#082630",
fontfamily = "Century Gothic", fontweight = "bold")
#plot a heatmap in the first pitch (top left)
#for this, you will set ax = ax[pitch][0,0]
kde = pitch.kdeplot(player1df.end_x, player1df.end_y, statistic='count', ax=ax['pitch'][0,0], cmap=customcmap, shade=True,
shade_lowest=False, n_levels=400, linewidths=3, alpha=1, zorder=0.99)
#the [0,0] after the [pitch] determines which of the pitches is being plotted on
#the first number specifies the row, and the second number specifies the column
#[0,0] means first row, first column, so it is in the top left
#to plot in the second row, you would change that first number to a 1 (third row would be 2, fourth would be 3, etc.)
kde2 = pitch.kdeplot(player1df.end_x, player1df.end_y, statistic='count', ax=ax['pitch'][1,0], cmap=customcmap, shade=True,
shade_lowest=False, n_levels=400, linewidths=3, alpha=1, zorder=0.99)
#to plot in the second column, you would change the second number to a 1 (third column would be 2, fourth would be 3, etc.)
kde3 = pitch.kdeplot(player1df.end_x, player1df.end_y, statistic='count', ax=ax['pitch'][0,1], cmap=customcmap, shade=True,
shade_lowest=False, n_levels=400, linewidths=3, alpha=1, zorder=0.99)
#setting the titles of each individual pitch
#you specify the pitch in the same way inside the [] and since we have our player name as a variable, we can use that
#to automatically set that as the text without typing it in each time
ax['pitch'][0,0].set_title(str(player1), fontsize=40, color="#082630",
fontfamily = "Century Gothic", fontweight = "bold", va="center", ha="center", pad=15)
ax['pitch'][1,0].set_title(str(player1), fontsize=40, color="#082630",
fontfamily = "Century Gothic", fontweight = "bold", va="center", ha="center", pad=15)
#if you want to manually set the title, just do something like this
ax['pitch'][0,1].set_title("Lionel Messi", fontsize=40, color="#082630",
fontfamily = "Century Gothic", fontweight = "bold", va="center", ha="center", pad=15)
#save your figure as a high quality image
plt.savefig("WalkthroughViz.png", dpi = 300, bbox_inches='tight')

The first two sections are general setup stuff.

First, we determine what kind of pitches we want and how we want them to look. Set the pitch_type to match your data provider, pitch_color to the color you want your pitches to be, line_color to the color you want your pitch lines to be, half=True here because we want half pitches, linewidth to the width you want your pitch lines to be, and spot_scale to the size you want the circles on your pitch (penalty spot, centre spot) to be.

Then, we plot a grid of those pitches. We set the number of pitches we want with nrows and ncols, and then set how much of the figure we want allocated for the title, the grid of pitches itself, and space between the pitches. Running just that gives you this output:

To fill in that space between the pitches, we set the face color of the figure:

To give the overall figure a title, we can use fig.suptitle:

Now, for some actual heatmap plotting. We specify where we want the data for the heatmap to come from (the “end_x” and “end_y” columns of our “player1df”), set our cmap to the custom one we created, and set some other arguments. To start, we can plot this heatmap on the top left pitch by setting ax=ax[‘pitch’][0][0].

We can plot on pitches in lower rows by increasing the first number in those brackets:

To move to pitches in the next columns, you increase the number in the second pair of brackets:

You can use a variable to automatically set the title of a specific pitch:

Or you can set them manually:

Finally, to save your creation in a nice crisp, high-resolution image, you can use:

plt.savefig("WalkthroughViz.png", dpi = 300, bbox_inches='tight')

The higher the dpi, the higher the resolution will be, but if you put it way up, the image can be too big to post or share on something like Twitter.

Here is my final output:

That should be all you need to know. From there, you can of course plot on other pitches, incorporate different players, maybe even try different types of visualizations on these pitches. If you have any questions, feel free to ask!

--

--