20% OFF BUNDLES & COURSES - USE CODE "HOLIDAY20" AT CHECKOUT

Visualizing Striker Profiles: Creating Radar Charts in Python

Nov 14, 2024

By Trym Sorum

In this article, I’ll walk you through how to efficiently download data from the popular football statistics website FBref. We’ll focus on extracting data for forwards from Europe’s top 5 leagues and show you how to filter and rank them based on expected goals (xG) for the 2023/24 season.

Website: FBref.com

1 — Downloading Data from FBref

#Importing relevant package 
import pandas as pd

#Retrieving data from top5 european league players
url = "https://fbref.com/en/comps/Big5/2023-2024/stats/players/2023-2024-Big-5-European-Leagues-Stats"

df = pd.read_html(url, attrs={"id": "stats_standard"})

#Getting the first item from the list
df = df[0]

#Displaying dataset
df.head()
Output 1: Raw Data from FBref

In just a few lines of codes we have downloaded the raw data from FBref. The next steps is to filter and organize the data based on what we want. We will filter for ‘forwards’ that have played more then 900 minutes (equal to 10 full games).

2 — Filter and Organize Data

#Dropping per 90 stats and creating our own later
df = df[df.columns.drop(list(df.filter(regex='Per 90')))]

#Dropping top header that we dont need
df = df.droplevel(0, axis = 1)

#Filtering for position, Forward
df = df[df['Pos'].str.contains('FW')]

#Casting datatype to integer (number)
#Filtering for players that have played more than 900 minutes (10 full games)
df = df[df['Min'].astype(int) > 900]

#Casting more datatypes
df[['90s', 'xG', 'xAG']] = df[['90s', 'xG', 'xAG']].astype(float)

df[['Gls', 'Ast', 'G+A', 'PrgC', 'PrgP', 'PrgR']] = df[['Gls', 'Ast', 'G+A', 'PrgC', 'PrgP', 'PrgR']].astype(int)

#Calculating per 90 metrics
df['goals_per_90'] = df['Gls'] / df['90s']
df['assists_per_90'] = df['Ast'] / df['90s']
df['goals_assists_per_90'] = df['G+A'] / df['90s']
df['progressive_carries_per_90'] = df['PrgC'] / df['90s']
df['progressive_passes_per_90'] = df['PrgP'] / df['90s']
df['progressive_receptions_per_90'] = df['PrgR'] / df['90s']
df['expected_goals_per_90'] = df['xG'] / df['90s']
df['expected_assists_per_90'] = df['xAG'] / df['90s']

#Sorting top 5 strikers based on xG per 90
df_sorted = df.sort_values(by='expected_goals_per_90', ascending=False)
df_sorted[['Player', 'Gls', 'Ast', 'G+A', 'expected_goals_per_90']].head()
Output 2: Top 5 strikers in Europe, sorted by xG per 90

3 — Calculating Player Percentiles for Radars

#Import packages for Radar and Data Viz
from mplsoccer import Radar
from mplsoccer import PyPizza
import matplotlib.pyplot as plt
import matplotlib.image as mpimg

#Importing font (optional)
import matplotlib.font_manager as font_manager
font_path = 'Arvo-Regular.ttf'
font_props = font_manager.FontProperties(fname=font_path)

#Percentiles calculation
df['Goals'] = (df['goals_per_90'].rank(pct=True) * 100).astype(int)
df['Assists'] = (df['assists_per_90'].rank(pct=True) * 100).astype(int)
df['Goals + Assists'] = (df['goals_assists_per_90'].rank(pct=True) * 100).astype(int)
df['Progressive Carries'] = (df['progressive_carries_per_90'].rank(pct=True) * 100).astype(int)
df['Progressive Passes'] = (df['progressive_passes_per_90'].rank(pct=True) * 100).astype(int)
df['Progressive Receptions'] = (df['progressive_receptions_per_90'].rank(pct=True) * 100).astype(int)
df['Expected Goals'] = (df['expected_goals_per_90'].rank(pct=True) * 100).astype(int)
df['Expected Assists'] = (df['expected_assists_per_90'].rank(pct=True) * 100).astype(int)

#Filtering and sorting which columns to use in radar
columns_to_plot = [
'Goals', 'Assists', 'Goals + Assists',
'Progressive Carries', 'Progressive Passes', 'Progressive Receptions',
'Expected Goals', 'Expected Assists'
]

#Creating min and max value for radar
radar = Radar(
params=columns_to_plot,
min_range=[0 for _ in columns_to_plot],
max_range=[100 for _ in columns_to_plot]
)

#Players to plot
haaland = df[df['Player'] == 'Erling Haaland']
mbappe = df[df['Player'] == 'Kylian Mbappé']
kane = df[df['Player'] == 'Harry Kane']
guirassy = df[df['Player'] == 'Serhou Guirassy']
boniface = df[df['Player'] == 'Victor Boniface']

In the previous code, we calculated what we call player percentiles in order to compare strikers between leagues. A player that scores 99 in goals scored are in the top percentile of all players in Europe.

4 — Plotting Radar Charts

# Setup the pizza plot
pizza = PyPizza(
params=columns_to_plot,
background_color='#1A1A1D',
straight_line_color='white',
straight_line_lw=1,
last_circle_lw=0,
other_circle_color='white',
other_circle_ls='-',
other_circle_lw=1
)

# Create the figure and plot
fig, ax = pizza.make_pizza(
figsize=(8, 8), # Increase the figure size
values=list(haaland[columns_to_plot].values.flatten()), #Insert player
kwargs_values=dict(
color='#FFFFFF', fontsize=9, fontproperties=font_props,
bbox={
'edgecolor': 'black',
'facecolor': '#034694',
"boxstyle": 'round, pad= .2',
"lw": 1
}
),

kwargs_slices=dict(
facecolor="#6CABDD", # New color (example: mancity blue)
edgecolor="black", # black edge for the slices
linewidth=1
),

kwargs_params=dict(
color='#FFFFFF', # White color for the labels
fontsize=10, # Font size for the labels
fontproperties='monospace' # Custom font
)
)

# Adjust the margins to add space around the plot
fig.subplots_adjust(top=0.8, bottom=0.2, left=0.2, right=0.8) # Adjust as needed for space around the figure

# Add the title
ax.text(x=.5, y=1.1, s='Erling Haaland', fontsize=20, c='#6CABDD', ha='center', fontproperties=font_props, va='center', transform=ax.transAxes)

ax.text(
x=.5, y=-0.1, # Position below the plot (adjust y as needed)
s='Data: FBref, Compared with Forwards in Top5 Leagues 2024', # Example subtext
fontsize=8, # Smaller font size for subtext
color='#FFFFFF', # Subtext color
ha='center',
va='center',
fontproperties='monospace',
transform=ax.transAxes
)

5 — Analysis

For the analysis of striker profiles, I have taken inspiration from John Mullers in The Athletic . In his article on player profiles he has created great visuals and explanations based on different player roles in each position on the field. I have picked three attacking profiles which is the finisher, the wide threat and the roamer.

The Athletics 18 Player Roles (Muller, 2022)

Erling Haaland — “The Finisher”

Output 3: Radar Chart of Erling Haaland

Here is the final ouput of the code which is the radar chart of ‘Erling Haaland’. We can see that he is the best goalscorer in Europe based on goals and expected goals. He is what we call a typical finisher or poacher. His job is straightforward, score goals. Other forward will have different skillsets based on their teams style of play and roles in the team.

In order to get the radar charts of other players, we can just shift out one line of code in the plotting code (section 4). For example, we can switch Haaland with Mbappé.

Kylian Mbappé — “The Wide Threat”

Output 4: Radar Chart of Kylian Mbappe

At first glance, we can see the brilliance of Mbappes all around game. As opposed to Haaland, the finisher, Mbappe is a wide threat or inside forward which means that he will be much more dynamic and active in the build up to a goal. This reflect in the fact that he scores highly in all progressive metrics combined with being in the top 1 percent of goals and assists. It will be interesting to see how he adapts his game in Real Madrid as he seems to be deployed in a more traditional central striker role. However, it is likely that he still will rotate with the rest of the attacking talent and be quite dynamic.

Lets finish by looking at Harry Kane who had the most goals + assists in Europe last season.

Harry Kane — “The Roamer”

Output 5: Radar Chart of Harry Kane

We can see that Harry Kane is something in between a goalscorer and a creator. We call him a ‘Roamer’ as he likes to drop and combine in the build up play which reflects in his score on progressive passes and assists. However, teammates will not look to get the ball to him as much as they would for Mbappe.

Conclusion

In this post, we have seen how we can create radar charts for strikers in the top 5 European leagues. We looked at three different striker profiles which was the finisher (Haaland), wide/inside forward (Mbappe) and the roamer (Kane). By creating radar charts, we can get good visualization to seperate player profiles. One limitation with these radar charts was that we only looked at attacking metrics. To create more solid models, we could include defensive metrics such as pressing, duels and possession metrics. We could also include more advanced shooting metrics than just goals, assists and xG.

Source

 

Stay connected with news and updates!

Join our mailing list to receive the latest news and updates from our team.

We hate SPAM. We will never sell your information, for any reason.