Today we’ll walk through creating football pitches in python using Matplotlib. This is a topic I have previously covered, but I thought a v2 was in order. If you’ve spent any time looking at x,y data from any of the providers you’ll know that their coordinate system is not based on any specific unit of measurement, and that the coordinate system varies from company to company. I work for StatsPerform (we’ll use Opta below to distinguish from ex-Perform data rather than ex-Stats data, which is again a different system), so I’ll run through converting the coordinates to metres and plotting them.

I plot my pitches at 105×68 metres. This is an average pitch size and regulation for all new pitches in the EPL. If you have access to the data and know the exact dimensions for pitches in the leagues you are interested in, you could make this even more accurate, however, you’ll need to change a few of the numbers I’ll run through below.

First we will import the required packages needed:

` `
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.patches import Arc

There’s nothing really overly complex about this once you break it down into its component parts. A pitch is just a collection of lines and shapes. We’ll walk through a step by step guide and wrap it all in a function at the end. I’ll be using Opta’s coordinate measurements and converting them to my desired pitch size. First I’ll set some variables we will use going through the process.

` `
# bounds for my desired pitch size
x_min = 0
x_max = 105
y_min = 0
y_max = 68
line_color = "grey"
line_thickness = 1.5
background = "w" # we can use "w" as a shorthand for "white"
point_size = 20
# we'll come back to these at the end
arc_angle = 0
first = 0
second = 1

Next we will create a list of markings based on where they appear on the pitch.

` `
pitch_x = [0,5.8,11.5,17,50,83,88.5,94.2,100] # pitch x markings
"""
These are where various lines occur on the length of the pitch
[goal line, six yard box, penalty spot, edge of box,
halfway line, edge of box, penalty spot, six yard box, goal line]
"""
pitch_y = [0, 21.1, 36.6, 50, 63.2, 78.9, 100]
"""
These are where various lines occur on the width of the pitch
[sideline, edge of box, six yard box, centre of pitch,
six yard box, edge of box, sideline ]
"""
goal_y = [45.2, 54.8] # goal posts

We need to convert those to metres. as this is 100 x 100, this is not an issue. We can either divide the coordinates by 100 and multiply it by the max size of our x or y, but we can simplify by multiplying the x and y coordinates by the ratio of difference. i.e, 105 / 100 = 1.05, 68 / 100 = 0.68

Instead of needing to loop through each value, we can use list comprehension here:

` `
pitch_x = [item * 1.05 for item in pitch_x]
pitch_y = [item * 0.68 for item in pitch_y]
goal_y = [item * 0.68 for item in goal_y]

Note: for ease of reading, I have used “item” in the above. It doesn’t really matter what you use here, and I commonly just use x. All that this means is that the selected value should be multiplied by the given amount and do it for each item in the list. The above is hardcoded, let’s try to not box ourselves in and make our code as flexible as we can. We can replace the above with:

` `
x_conversion = 105 / 100
y_conversion = 68 / 100
pitch_x = [item * x_conversion for item in pitch_x]
pitch_y = [item * y_conversion for item in pitch_y]
goal_y = [item * y_conversion for item in goal_y]

Now that we have our point converted, we can plot these by creating a list of x and y coordinates. For example, here is how we can plot the side and goal lines

` `
lx1 = [x_min, x_max, x_max, x_min, x_min]
ly1 = [y_min, y_min, y_max, y_max, y_min]
fig, ax = plt.subplots(figsize=(11,7))
ax.axis("off")
ax.plot(lx1,ly1, color=line_color, lw=line_thickness, zorder=-1)
"""
We are explicitly setting a negative zorder on this so we do not need
to set zorders on any data we may plot on top of this
"""
plt.tight_layout()
plt.show()

We could go through each of our markings and plot them like

` `
# side and goal lines
lx1 = [x_min, x_max, x_max, x_min, x_min]
ly1 = [y_min, y_min, y_max, y_max, y_min]
ax.plot(lx1, ly1, color=line_color, lw=line_thickness, zorder=-1)
# outer boxed
lx2 = [x_max, pitch_x[5], pitch_x[5], x_max]
ly2 = [pitch_y[1], pitch_y[1], pitch_y[5], pitch_y[5]]
ax.plot(lx2, ly2, color=line_color, lw=line_thickness, zorder=-1)
lx3 = [0, pitch_x[3], pitch_x[3], 0]
ly3 = [pitch_y[1], pitch_y[1], pitch_y[5], pitch_y[5]]
ax.plot(lx3, ly3, color=line_color, lw=line_thickness, zorder=-1)
# .....

But this is not the best approach, as it means we need to adjust each of these if we wanted to plot a pitch vertically. Also, the above may look a bit scary, but looking at lx2 as an example, pitch_x[5] is just the index position of the pitch_x. Python is a 0 index language, meaning that indexes start at zero rather than one. Looking at our pitch_x list, this would be the **sixth** item.

Instead of plotting these one by one, lets build out our list of coordinates and add them to a master list:

` `
# side and goal lines
lx1 = [x_min, x_max, x_max, x_min, x_min]
ly1 = [y_min, y_min, y_max, y_max, y_min]
# outer boxed
lx2 = [x_max, pitch_x[5], pitch_x[5], x_max]
ly2 = [pitch_y[1], pitch_y[1], pitch_y[5], pitch_y[5]]
lx3 = [0, pitch_x[3], pitch_x[3], 0]
ly3 = [pitch_y[1], pitch_y[1], pitch_y[5], pitch_y[5]]
# goals
lx4 = [x_max, x_max+2, x_max+2, x_max]
ly4 = [goal_y[0], goal_y[0], goal_y[1], goal_y[1]]
lx5 = [0, -2, -2, 0]
ly5 = [goal_y[0], goal_y[0], goal_y[1], goal_y[1]]
# 6 yard boxes
lx6 = [x_max, pitch_x[7], pitch_x[7], x_max]
ly6 = [pitch_y[2], pitch_y[2], pitch_y[4], pitch_y[4]]
lx7 = [0, pitch_x[1], pitch_x[1], 0]
ly7 = [pitch_y[2], pitch_y[2], pitch_y[4], pitch_y[4]]
# Halfway line, penalty spots, and kickoff spot
lx8 = [pitch_x[4], pitch_x[4]]
ly8 = [0, y_max]
lines = [
[lx1, ly1],
[lx2, ly2],
[lx3, ly3],
[lx4, ly4],
[lx5, ly5],
[lx6, ly6],
[lx7, ly7],
[lx8, ly8],
]

by organising our lines like this, we get the index position of each pairing – either 0 or 1 – and use those to set which would equal the x and y arguements in matplotlib’s plot function. We’ll come back to this point, but just know there’s method in the madness… We can plot the above by looping through our newly created lines list:

` `
fig, ax = plt.subplots(figsize=(11,7))
ax.axis("off")
for line in lines:
ax.plot(line[0], line[1],
color=line_color,
lw=line_thickness,
zorder=-1)
plt.tight_layout()
plt.show()

That makes our plotting code look a lot cleaner. Let’s do the same for the points for the kickoff and penalty spots:

` `
points = [
[pitch_x[6], pitch_y[3]],
[pitch_x[2], pitch_y[3]],
[pitch_x[4], pitch_y[3]]
]

We can then loop through the points in our plotting code:

` `
fig, ax = plt.subplots(figsize=(11,7))
ax.axis("off")
for line in lines:
ax.plot(line[0], line[1],
color=line_color,
lw=line_thickness,
zorder=-1)
for point in points:
ax.scatter(point[0], point[1],
color=line_color,
s=point_size,
zorder=-1)
plt.tight_layout()
plt.show()

Continuing with this method, we can add the centre circle and Ds on each box

` `
circle_points = [pitch_x[4], pitch_y[3]]
arc_points1 = [pitch_x[6], pitch_y[3]]
arc_points2 = [pitch_x[2], pitch_y[3]]

` `
fig, ax = plt.subplots(figsize=(11,7))
ax.axis("off")
for line in lines:
ax.plot(line[0], line[1],
color=line_color,
lw=line_thickness,
zorder=-1)
for point in points:
ax.scatter(point[0], point[1],
color=line_color,
s=point_size,
zorder=-1)
circle = plt.Circle((circle_points[0], circle_points[1]),
x_max * 0.088,
lw=line_thickness,
color=line_color,
fill=False,
zorder=-1)
ax.add_artist(circle)
arc1 = Arc((arc_points1[0], arc_points1[1]),
height=x_max * 0.088 * 2,
width=x_max * 0.088 * 2,
angle=arc_angle,
theta1=128.75,
theta2=231.25,
color=line_color,
lw=line_thickness,
zorder=-1)
ax.add_artist(arc1)
arc2 = Arc((arc_points2[0], arc_points2[1]),
height=x_max * 0.088 * 2,
width=x_max * 0.088 * 2,
angle=arc_angle,
theta1=308.75,
theta2=51.25,
color=line_color,
lw=line_thickness,
zorder=-1)
ax.add_artist(arc2)
ax.set_aspect("equal") # keeps circles in correct shape ~regardless of of fig dimensions
plt.tight_layout()
plt.show()

And that’s the run through of creating a football pitch from scratch! However, that’s not the end. We added a couple of variables and organised our code to make it more flexible. Lets make a couple of changes to the final plotting code to see how this can be useful:

` `
fig, ax = plt.subplots(figsize=(11,7))
ax.axis("off")
for line in lines:
ax.plot(line[first], line[second],
color=line_color,
lw=line_thickness,
zorder=-1)
for point in points:
ax.scatter(point[first], point[second],
color=line_color,
s=point_size,
zorder=-1)
circle = plt.Circle((circle_points[first], circle_points[second]),
x_max * 0.088,
lw=line_thickness,
color=line_color,
fill=False,
zorder=-1)
ax.add_artist(circle)
arc1 = Arc((arc_points1[first], arc_points1[second]),
height=x_max * 0.088 * 2,
width=x_max * 0.088 * 2,
angle=arc_angle,
theta1=128.75,
theta2=231.25,
color=line_color,
lw=line_thickness,
zorder=-1)
ax.add_artist(arc1)
arc2 = Arc((arc_points2[first], arc_points2[second]),
height=x_max * 0.088 * 2,
width=x_max * 0.088 * 2,
angle=arc_angle,
theta1=308.75,
theta2=51.25,
color=line_color,
lw=line_thickness,
zorder=-1)
ax.add_artist(arc2)
ax.set_aspect("equal")
plt.tight_layout()
plt.show()

At the start of this tutial we set a few variables: arc_angle = 0, first = 0, and second = 1. By setting these as variables instead of hardcoding them in our plotting code, We now only need to change those to plot a horizontal pitch. Let’s take care to resize the figure to be taller than wide too!

` `
first = 1
second = 0
arc_angle = 90
fig, ax = plt.subplots(figsize=(7,11))
ax.axis("off")
for line in lines:
ax.plot(line[first], line[second],
color=line_color,
lw=line_thickness,
zorder=-1)
for point in points:
ax.scatter(point[first], point[second],
color=line_color,
s=point_size,
zorder=-1)
circle = plt.Circle((circle_points[first], circle_points[second]),
x_max * 0.088,
lw=line_thickness,
color=line_color,
fill=False,
zorder=-1)
ax.add_artist(circle)
arc1 = Arc((arc_points1[first], arc_points1[second]),
height=x_max * 0.088 * 2,
width=x_max * 0.088 * 2,
angle=arc_angle,
theta1=128.75,
theta2=231.25,
color=line_color,
lw=line_thickness,
zorder=-1)
ax.add_artist(arc1)
arc2 = Arc((arc_points2[first], arc_points2[second]),
height=x_max * 0.088 * 2,
width=x_max * 0.088 * 2,
angle=arc_angle,
theta1=308.75,
theta2=51.25,
color=line_color,
lw=line_thickness,
zorder=-1)
ax.add_artist(arc2)
ax.set_aspect("equal")
plt.tight_layout()
plt.show()

## Something a bit more functional

So that’s all good, but it’s still annoying needing to change those variables every time we want to plot something either vertically or horizontally. Even more of a pain would be needed to copy and past the above code each time we wanted to draw a pitch! Lets take care of that now by creating a function to handle most of the above.

` `
def draw_pitch(x_min=0, x_max=105,
y_min=0, y_max=68,
pitch_color="w",
line_color="grey",
line_thickness=1.5,
point_size=20,
orientation="horizontal",
aspect="full",
ax=None
):
if not ax:
raise TypeError("This function is intended to be used with an existing fig and ax in order to allow flexibility in plotting of various sizes and in subplots.")
if orientation.lower().startswith("h"):
first = 0
second = 1
arc_angle = 0
if aspect == "half":
ax.set_xlim(x_max / 2, x_max + 5)
elif orientation.lower().startswith("v"):
first = 1
second = 0
arc_angle = 90
if aspect == "half":
ax.set_ylim(x_max / 2, x_max + 5)
else:
raise NameError("You must choose one of horizontal or vertical")
ax.axis("off")
rect = plt.Rectangle((x_min, y_min),
x_max, y_max,
facecolor=pitch_color,
edgecolor="none",
zorder=-2)
ax.add_artist(rect)
x_conversion = x_max / 100
y_conversion = y_max / 100
pitch_x = [0,5.8,11.5,17,50,83,88.5,94.2,100] # pitch x markings
pitch_x = [x * x_conversion for x in pitch_x]
pitch_y = [0, 21.1, 36.6, 50, 63.2, 78.9, 100] # pitch y markings
pitch_y = [x * y_conversion for x in pitch_y]
goal_y = [45.2, 54.8] # goal posts
goal_y = [x * y_conversion for x in goal_y]
# side and goal lines
lx1 = [x_min, x_max, x_max, x_min, x_min]
ly1 = [y_min, y_min, y_max, y_max, y_min]
# outer boxed
lx2 = [x_max, pitch_x[5], pitch_x[5], x_max]
ly2 = [pitch_y[1], pitch_y[1], pitch_y[5], pitch_y[5]]
lx3 = [0, pitch_x[3], pitch_x[3], 0]
ly3 = [pitch_y[1], pitch_y[1], pitch_y[5], pitch_y[5]]
# goals
lx4 = [x_max, x_max+2, x_max+2, x_max]
ly4 = [goal_y[0], goal_y[0], goal_y[1], goal_y[1]]
lx5 = [0, -2, -2, 0]
ly5 = [goal_y[0], goal_y[0], goal_y[1], goal_y[1]]
# 6 yard boxes
lx6 = [x_max, pitch_x[7], pitch_x[7], x_max]
ly6 = [pitch_y[2],pitch_y[2], pitch_y[4], pitch_y[4]]
lx7 = [0, pitch_x[1], pitch_x[1], 0]
ly7 = [pitch_y[2],pitch_y[2], pitch_y[4], pitch_y[4]]
# Halfway line, penalty spots, and kickoff spot
lx8 = [pitch_x[4], pitch_x[4]]
ly8 = [0, y_max]
lines = [
[lx1, ly1],
[lx2, ly2],
[lx3, ly3],
[lx4, ly4],
[lx5, ly5],
[lx6, ly6],
[lx7, ly7],
[lx8, ly8],
]
points = [
[pitch_x[6], pitch_y[3]],
[pitch_x[2], pitch_y[3]],
[pitch_x[4], pitch_y[3]]
]
circle_points = [pitch_x[4], pitch_y[3]]
arc_points1 = [pitch_x[6], pitch_y[3]]
arc_points2 = [pitch_x[2], pitch_y[3]]
for line in lines:
ax.plot(line[first], line[second],
color=line_color,
lw=line_thickness,
zorder=-1)
for point in points:
ax.scatter(point[first], point[second],
color=line_color,
s=point_size,
zorder=-1)
circle = plt.Circle((circle_points[first], circle_points[second]),
x_max * 0.088,
lw=line_thickness,
color=line_color,
fill=False,
zorder=-1)
ax.add_artist(circle)
arc1 = Arc((arc_points1[first], arc_points1[second]),
height=x_max * 0.088 * 2,
width=x_max * 0.088 * 2,
angle=arc_angle,
theta1=128.75,
theta2=231.25,
color=line_color,
lw=line_thickness,
zorder=-1)
ax.add_artist(arc1)
arc2 = Arc((arc_points2[first], arc_points2[second]),
height=x_max * 0.088 * 2,
width=x_max * 0.088 * 2,
angle=arc_angle,
theta1=308.75,
theta2=51.25,
color=line_color,
lw=line_thickness,
zorder=-1)
ax.add_artist(arc2)
ax.set_aspect("equal")
return ax

Now we just need to call the function to plot our pitches!

` `
background = "#303640"
fig, ax = plt.subplots(figsize=(11, 7))
fig.set_facecolor(background)
draw_pitch(orientation="h",
aspect="full",
pitch_color=background,
line_color="lightgrey",
ax=ax)
plt.tight_layout()
plt.show()

Notice that I did not pass arguments for everything in the function – such as min x and min y. In the function creation, when we create those arguments with =<something> we set that something as the default value. For example, x_min = 0. I do not add x_min when I call the function so it just takes that minimum value.

` `
background = "#303640"
fig, ax = plt.subplots(figsize=(11, 7))
fig.set_facecolor(background)
draw_pitch(orientation="vertical",
aspect="half",
pitch_color=background,
line_color="lightgrey",
ax=ax)
plt.tight_layout()
plt.show()

That was a bit of a long one, but I hope you found it useful. I don’t think there’s a massive point in creating your own functions for drawing pitches as some really good packages have emerged, such as mplsoccer, but I do think it is fundamental to understand *how* they are done. This will help you in your knowledge of Matplotlib. If you are interested in using some form of the above code in packages, feel free, but please credit when using. This is the first tutorial I have written in a long time – and it’s not even a new topic. I forgot how long these can take sometimes, but I’ll continue to try getting version two of old tutorials from my old site over here. If there’s anything you would like me to cover you can reach me over on twitter.