Build a Player Radar (Pizza) Chart in Python with mplsoccer
A handful of per-90 percentiles, one PyPizza chart, about thirty lines.
The player profile chart you see all over scouting threads — a circle sliced like a pizza, each wedge a different stat, the longer the wedge the better the player ranks — looks like specialist work. It is not. It is one mplsoccer object, a list of metric names, and a list of percentile values, and you can build one in about thirty lines of Python. Here is how, and — just as important — what the chart is actually saying.
What a pizza chart is, and what it isn’t
A pizza chart (mplsoccer’s name for a one-player radar) compares a single player to a reference group on several per-90 metrics at once. Each metric becomes a slice, and the length of the slice is the player’s percentile rank within that group — not the raw number. A slice that fills 90% of the radius means the player is in the 90th percentile for that stat: better than roughly nine in ten of his peers. A short slice means the opposite.
Two things follow from that, and missing either is the most common beginner mistake. First, the chart is only meaningful relative to a sensibly chosen peer group — you compare a central midfielder to other central midfielders in a comparable league, not to every outfield player alive. Second, the values you plot are percentiles between 0 and 100 that you compute from your own dataset. mplsoccer draws the chart; it does not know who is good. The numbers come from you.
Install
mplsoccer ships PyPizza, the class that does all the drawing, and pulls in matplotlib as a dependency. One line of pip and you are ready:
pip install mplsoccer
Step 1 — define the params and the values
A pizza chart needs two parallel lists: the metric names (params) and the player’s percentile for each (values). They must be the same length and in the same order — slice i shows params[i] at length values[i]. The metrics and percentiles below are an illustrative, made-up profile of a hypothetical attacking midfielder, chosen only to show the mechanics:
from mplsoccer import PyPizza
# Metric names — one per slice. These are the stats you chose to profile.
params = [
"Non-Penalty Goals", "npxG", "Shots",
"Assists", "xA", "Key Passes",
"Progressive Passes", "Progressive Carries", "Successful Take-Ons",
"Tackles", "Interceptions", "Pressures",
]
# Percentile ranks (0–100) for ONE player vs. a position group.
# ILLUSTRATIVE numbers — replace with percentiles from your own dataset.
values = [72, 80, 65, 58, 74, 83, 91, 88, 67, 24, 31, 40]
assert len(params) == len(values) # slices and values must line up
A nice touch many profiles use is to group the slices by theme — attacking, possession, defending — and colour each group differently so the chart reads at a glance. Here the first three metrics are attacking, the next six are possession and creation, and the last three are defending; we will colour them in three blocks.
Step 2 — instantiate PyPizza
You create the chart object once, handing it the parameter list and a little styling. The key arguments are params (the slice labels) and the colours for the slice backgrounds and outlines. straight_line_color and last_circle_color control the grid lines and the outer ring:
baker = PyPizza(
params=params, # the slice labels
background_color="#fbfaf3", # paper background
straight_line_color="#cbd3cc", # the radial grid lines
straight_line_lw=1,
last_circle_color="#33503f", # outer ring
last_circle_lw=1.5,
other_circle_lw=0,
inner_circle_size=11, # size of the blank hub in the middle
)
Step 3 — draw it with make_pizza
The make_pizza method does the drawing. You pass it the values list, a figure size, and styling for the wedges and the little value/label text. To colour the slices by theme, hand slice_colors a list with one colour per slice — here, three greens for attacking, a mid green for possession, and a muted tone for defending:
GREEN, GREEN_DK, ACCENT, MUTED = "#0a5a3f", "#073e2c", "#f4c20d", "#9aa39b"
# one colour per slice, grouped by theme (3 attack, 6 possession, 3 defend)
slice_colors = [ACCENT] * 3 + [GREEN] * 6 + [MUTED] * 3
text_colors = ["#16231c"] * 12
fig, ax = baker.make_pizza(
values, # the percentile for each slice
figsize=(8, 8.5),
slice_colors=slice_colors,
value_colors=text_colors,
value_bck_colors=slice_colors, # value bubble matches the slice
kwargs_slices=dict(edgecolor="#fbfaf3", linewidth=1.5, zorder=2),
kwargs_params=dict(color="#16231c", fontsize=10), # slice labels
kwargs_values=dict( # the % numbers
color="#16231c", fontsize=10,
bbox=dict(edgecolor="#16231c", boxstyle="round,pad=0.2", lw=1),
),
)
Each slice is now drawn to its percentile length, labelled with the metric name around the rim, and tagged with its value in a small bubble. The blocks of colour make the profile legible instantly: long gold-and-green wedges and short muted ones is, at a glance, a creative attacker who does little defensive work — which is exactly the made-up profile we fed it.
Step 4 — title it and save
make_pizza hands back standard matplotlib fig and ax objects, so you title and save with the matplotlib you already know. A pizza chart is hard to read without a title naming the player and, critically, the comparison group and sample — the percentiles are meaningless without it:
fig.text(0.05, 0.97, "Player Name — Attacking Midfielder",
fontsize=16, fontweight="bold", color="#073e2c")
fig.text(0.05, 0.94,
"Percentile rank vs. positional peers | per 90 | ILLUSTRATIVE DATA",
fontsize=10, color="#16231c")
fig.savefig("player-pizza.png", dpi=150, bbox_inches="tight",
facecolor="#fbfaf3")
Run the four steps in order and you have a finished pizza chart saved to player-pizza.png. Change the params and values and you have a different player; the drawing code never moves.
Reading the chart honestly
A pizza chart is a fast, honest summary as long as you remember what it is built on. The percentiles are only as fair as the peer group behind them — profile a striker against all midfielders and the goal-scoring slices will balloon for trivial reasons. The metrics you choose decide the story; pick only attacking stats and even a limited player looks elite. And percentiles compress, so a 99th-percentile passer and a 90th-percentile passer can look nearly identical on the rim while differing a lot in raw output.
None of that makes the chart less useful — it makes it a starting point. The right move is to build the percentiles deliberately. The metrics in the example map onto ideas covered elsewhere on this site: the progression slices come straight from progressive passes and carries, and if you want a real dataset to rank players against, the StatsBomb open data tutorial shows how to pull per-90 numbers you can convert into the percentiles this chart needs. Once you can make a pizza, the obvious next step is the pitch maps in the pass map and shot map tutorial — the same library, a different lens on the same player.
Sources & further reading
- Free textbook: Chapter 10: Passing Networks and Analysis — the theory behind this, at DataField.dev.
- mplsoccer documentation — the
PyPizzaclass, everymake_pizzaargument, and styled examples. - mplsoccer on GitHub — source, issues, and the example gallery the styling here draws on.
- FBref — per-90 stats and built-in percentile tables, a natural source for the values.
- StatsBomb open data — event data you can aggregate into your own per-90 percentiles.