Tutorials

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.

Where the percentiles come from
Take per-90 stats for a position group — from FBref, StatsBomb, or any source you trust — and for each metric rank your player against that group, converting the rank to a percentile from 0 to 100. Those percentiles are the input to the chart. The worked example below uses a small, clearly-invented set of metrics and percentiles to demonstrate the plotting; it is not any real player’s actual data. Swap in your own and the chart becomes real.

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