Skip to main content

Set-piece metrics, benchmarked team/player ratings, added-value scoring and mplsoccer pitch visualizations (penalties, kick-offs, free kicks, corners, throw-ins, goal kicks) from Opta F24 and StatsBomb event data, including second phases, xT, zones and retention

Project description

wa-setpieces

Set-piece metrics for football (soccer) matches from Opta / Stats Perform F24 event-feed JSON exports (natively) and StatsBomb open-data exports (via an adapter, see below): penalties, kick-offs, free kicks, corners, throw-ins and goal kicks.

Given a match file, this package tags every set-piece restart, aggregates attempts/success rates by team and player, tracks pass end locations for delivery maps, and links each set piece to the shot or goal it produced (via the provider's own assist-chain data). It also covers, for corners and free kicks specifically: second-phase detection, Expected Threat (xT), pitch zones/thirds/channels, possession retention, and a benchmarked team/player rating — with pitch plots built on mplsoccer.

Corner delivery map drawn with mplsoccer

Full documentation, with a runnable plot gallery: https://waltzinganalytics.readthedocs.io

Install

pip install wa-setpieces

Or install from source:

git clone https://github.com/marclamberts/waltzinganalytics.git
cd waltzinganalytics
pip install -e .

Quickstart

from wa_setpieces import load_events, set_piece_summary

match = load_events("match.json")
summary = set_piece_summary(match.events)
print(summary)
              contestantId set_piece_type  attempts  successful  success_rate  shots  goals
cxb4hqite921i...      corner         2           1         0.500      1      0
cxb4hqite921i...   free_kick        12           9         0.750      0      0
cxb4hqite921i...   goal_kick         8           5         0.625      0      0
cxb4hqite921i...    kick_off         1           1         1.000      0      0
cxb4hqite921i...    throw_in        20          16         0.800      1      0
...

Second phases, xT, zones, retention, added value and outcomes

from wa_setpieces import (
    second_phases, second_phase_summary,   # corner/free-kick second-phase shots
    retention_detail, retention_rate,      # possession retained N seconds later
    add_thirds, add_channels, add_zone_grid,  # pitch location tagging
    XTModel, set_piece_delivery_xt, set_piece_xt_summary,  # Expected Threat
    set_piece_added_value, set_piece_value_summary,  # xT + shot quality + goals, blended
    corner_report, free_kick_report,       # all of the above, merged into one table per team
    delivery_outcomes, outcome_summary,    # per-delivery outcome category, for a shot map
)

second_phases(match.events, "corner")           # per-corner: cleared / first-phase shot / second-phase shot
second_phase_summary(match.events, "free_kick") # per-team roll-up

retention_rate(match.events, "corner")          # per-team: % of corners where the ball is retained ~8s later

tagged = add_thirds(match.events)               # defensive_third / middle_third / attacking_third
tagged = add_channels(tagged, n=5)              # wide / half-space / central

model = XTModel.fit(match.events)               # fit an xT grid (fit on many matches for real use!)
set_piece_xt_summary(match.events, "corner", model)  # total/average xT added per team

set_piece_added_value(match.events, "corner", model)  # per-delivery: xT added + resulting shot quality + goal
corner_report(match.events, model=model)              # attempts, success/retention/second-phase rate, added value -- one table

delivery_outcomes(match.events, "corner")  # per-delivery: short_corner / direct_shot / second_phase_shot /
                                            # aerial_duel (50/50) / cleared / first_touch_won / first_touch_lost

All of the above are derived heuristics, not raw Opta fields — see docs/source/advanced.rst (or the hosted docs) for the exact assumptions and tunable thresholds behind each one. That page also documents a real bug this uncovered and fixed: F24's eventId is only unique within one team's own event stream, not globally — every delivery/shot lookup in this package is scoped accordingly.

Shot value (experimental)

Five pre-trained gradient-boosted models, bundled with the package, score every shot in a match:

pip install -e ".[ml]"   # xgboost + scikit-learn + joblib
from wa_setpieces.ml.shot_value import ShotValueModels, shot_value

models = ShotValueModels.load()          # loads once; reuse across matches
shots = shot_value(match.events, models)
# eventId, playerName, is_goal, set_piece_type, on_target_prob, xgot, psxg,
# situational_prob, outcome_class_0..3, shot_value (blended)

Read wa_setpieces/shot_value.py's module docstring before trusting this for anything real. The five models were trained elsewhere against a feature schema this package has to reconstruct from Opta F24 qualifiers on each shot event; some inputs (shot geometry, set-piece origin, assist, left/right foot, goal-mouth placement) are confidently derived from already-tested logic elsewhere in this package, but several situational flags (big chance, one-on-one, fast break, scramble, header/volley) have no reliable qualifier signal in the two real matches this was checked against and default to False rather than a guessed-and-possibly-wrong qualifier ID — that gap is documented, not hidden, but it does mean predictions are degraded relative to the models' original training data.

Ratings

wa_setpieces.core.rating turns a report into a single 0-100 "how good" score, benchmarked (z-scored) against whoever else is in the table — always rate against a full season/competition, not one match; a two-row sample just tells you which of those two had the better match, not how good either team actually is.

from wa_setpieces.core.rating import team_rating, player_rating

team_rating(corner_report(season_events, model=model))
# ... success_rate, avg_added_value, retention_rate, plus a *_score column
# per metric and a composite `rating` (50 = this table's own average)

player_rating(season_events, "corner", model, min_deliveries=5, min_shots=3)
# delivery_score (taker quality) and finishing_score (shooter quality),
# merged -- a pure taker or pure finisher is rated on the component they
# have, not penalized for the one they don't

Plots

pip install -e ".[viz]"   # matplotlib + mplsoccer
from wa_setpieces.viz.plots import (
    plot_delivery_map,      # arrow map of deliveries, colored by outcome
    plot_zone_heatmap,      # where events happen, gridded onto the pitch
    plot_xt_grid,           # a fitted XTModel's grid, as a heatmap
    plot_second_phase,      # one corner/free-kick's phase sequence, numbered
    plot_team_comparison,   # grouped bars: both teams, every set-piece type
    plot_xt_added_bars,     # diverging bar chart of xT added per delivery
    plot_corner_sonar,      # polar plot of delivery angle + distance
    plot_match_timeline,    # every set piece on one shared match-minute axis
    plot_dashboard,         # one-figure report card combining several of the above
    plot_set_piece_radar,   # two-team radar over a corner_report/free_kick_report
    plot_set_piece_outcomes,  # shot map: every delivery, colored by outcome category
    plot_rating_benchmark,   # team/player rating vs. the sample-average baseline
)

plot_delivery_map(
    delivery_locations(match.events, "corner"), title="Corner deliveries",
    subtitle="20 June 2026 · Delivery map", footer="Data: Opta", dark=False,  # or dark=True (default)
)
plot_dashboard(match.events, team_id, set_piece_type="corner")  # the "hero" figure
plot_set_piece_radar(corner_report(match.events, model=model))  # team A vs. team B, one glance
plot_rating_benchmark(team_rating(corner_report(season_events, model=model)))

Every plotting function returns (fig, ax) (plot_dashboard returns just fig, being multi-panel) for further customization, and takes dark: bool = True -- the whole figure switches between a validated dark (navy) and light (white) palette with that one argument, see wa_setpieces.viz.theme.get_palette. Colors are assigned by the job they do — a validated categorical palette for team identity (team-vs-team charts use a fixed orange-then-blue pairing in both modes), a status pair for success/fail, gold for goals, single-hue sequential ramps for magnitude, and a diverging pair for signed quantities like xT added — not picked for looks; see wa_setpieces/viz/theme.py. subtitle (a muted line under the title) and footer (a small credit/source line, bottom-right) are optional on every plot. See the gallery for all sixteen plots (in both modes) with full source code.

Other data providers

Opta F24 is the native format (wa_setpieces.core.loader.load_events, handled directly, no adapter needed). wa_setpieces.providers converts other providers' feeds into that same internal frame, so every other module — filters, metrics, chains, phases, retention, xT, value, rating, viz — works unchanged regardless of source:

from wa_setpieces import load_statsbomb_events

events = load_statsbomb_events("statsbomb_events_export.json")
set_piece_summary(events)  # same functions, same DataFrame shape

Read wa_setpieces/providers/statsbomb.py's module docstring for exactly what is (and isn't) faithfully mapped — set-piece detection, the assist-chain shot link, retention, xT and rating are all faithful; one narrow edge case in second-phase timing is documented as an approximation.

Impect is not supported. It's a closed, proprietary feed with no public schema to build and verify an adapter against — contributing one needs a real sample export or an official schema reference to check the mapping against, the same way the StatsBomb adapter and the Opta constants in wa_setpieces/core/constants.py were verified against real exports.

Command line

wa-setpieces match.json
wa-setpieces match.json --csv summary.csv
wa-setpieces match.json --xt   # also fit + print xT for this match (illustrative on one match)

What counts as a set piece

Type Detected on Opta qualifierId
Penalty shot event (miss/post/saved/goal) 9
Kick-off pass event 279
Free kick pass event (corners excluded) 5
Corner pass event 6
Throw-in pass event 107
Goal kick pass event 124

These qualifier IDs are the standard Opta/Stats Perform F24 vocabulary and were cross-checked against a real match export (see tests/data/sample_match.json and tests/test_filters.py): tagged events line up with their expected pitch location (corner arc, touchline, centre spot, six-yard line).

Package layout

  • wa_setpieces.core.loader — parse F24 JSON into a tidy pandas.DataFrame; load_events_multi stacks a whole season.
  • wa_setpieces.core.constants — Opta typeId / qualifierId reference.
  • wa_setpieces.core.filters — extract/tag each set-piece type.
  • wa_setpieces.core.metrics — team/player counts, success rates, delivery locations.
  • wa_setpieces.core.chains — link set pieces to the shots/goals they produced.
  • wa_setpieces.core.zones — pitch thirds, channels and a configurable zone grid.
  • wa_setpieces.core.phases — second-phase detection for corners/free kicks.
  • wa_setpieces.core.retention — possession retention after any restart.
  • wa_setpieces.core.xt — grid-based Expected Threat (xT), fit from data.
  • wa_setpieces.core.value — set-piece added value: delivery xT + resulting shot quality + goals, blended.
  • wa_setpieces.core.outcomes — per-delivery outcome classification (short corner, direct/second-phase shot, aerial duel, cleared, first/lost touch) for a shot-map scatter.
  • wa_setpieces.ml.shot_value — five bundled pre-trained models (on-target probability, xGOT, post-shot xG, situational quality, outcome class) for a richer per-shot value score (optional ml extra; experimental, read the module docstring).
  • wa_setpieces.core.reportcorner_report/free_kick_report: everything above, merged into one table per team.
  • wa_setpieces.core.rating — benchmarked 0-100 team/player "how good" scores from a report (see Ratings below).
  • wa_setpieces.providers.statsbomb — convert a StatsBomb open-data export into the same internal frame Opta F24 produces.
  • wa_setpieces.viz.plots — mplsoccer/matplotlib plots: delivery maps, heatmaps, sonar, timeline, dashboard, radar, rating benchmark (optional viz extra).
  • wa_setpieces.viz.theme — the validated dark/light color palettes every plot draws from.
  • wa_setpieces.convert.corners — batch-convert a directory of Opta F24 exports plus a match-list CSV into a flat corners table for tools that expect that schema (optional convert extra).
  • wa_setpieces.cliwa-setpieces command-line tool.

Development

pip install -e ".[dev]"
pytest

Releasing

Publishing to PyPI is automated via GitHub Actions trusted publishing (.github/workflows/publish.yml) — no API token stored anywhere. One-time setup (only a PyPI project owner can do this, since it requires logging into PyPI):

  1. On PyPI: https://pypi.org/manage/account/publishing/ → add a pending trusted publisher with project name wa-setpieces, owner marclamberts, repository waltzinganalytics, workflow publish.yml, environment pypi.
  2. From then on, publishing a GitHub Release (or pushing a v* tag) builds the sdist/wheel and uploads them automatically.

Docs

Docs are built with Sphinx (pydata-sphinx-theme + sphinx-gallery, the same stack mplsoccer's docs use) and hosted on Read the Docs (.readthedocs.yaml at the repo root). The gallery under examples_gallery/ is executed at build time, so its plots and DataFrame outputs are always current. To build locally:

pip install -e ".[docs]"
sphinx-build -b html docs/source docs/_build/html

License

MIT

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

wa_setpieces-0.10.0.tar.gz (1.3 MB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

wa_setpieces-0.10.0-py3-none-any.whl (1.4 MB view details)

Uploaded Python 3

File details

Details for the file wa_setpieces-0.10.0.tar.gz.

File metadata

  • Download URL: wa_setpieces-0.10.0.tar.gz
  • Upload date:
  • Size: 1.3 MB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.14

File hashes

Hashes for wa_setpieces-0.10.0.tar.gz
Algorithm Hash digest
SHA256 50a87d236d5ac6985a2edc5b594486becca142ac296aa39e60c6ea17c1f5f96c
MD5 223148629468068b5490d529c0cc03a4
BLAKE2b-256 52004c67f24939d79d831204595c701bc93738bc87c4f34bb86945f9bb662b7a

See more details on using hashes here.

Provenance

The following attestation bundles were made for wa_setpieces-0.10.0.tar.gz:

Publisher: publish.yml on marclamberts/waltzinganalytics

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file wa_setpieces-0.10.0-py3-none-any.whl.

File metadata

  • Download URL: wa_setpieces-0.10.0-py3-none-any.whl
  • Upload date:
  • Size: 1.4 MB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.14

File hashes

Hashes for wa_setpieces-0.10.0-py3-none-any.whl
Algorithm Hash digest
SHA256 3084cbd6e2e327748f46c24c8750b76292fdfad7f1ed4536668c3b53807ecb82
MD5 328e41c282bd7a086897d3b3b966baf7
BLAKE2b-256 a8fc793172cac50e2050d17f9c6b8fc48861c89041625c3212547aa2726cf712

See more details on using hashes here.

Provenance

The following attestation bundles were made for wa_setpieces-0.10.0-py3-none-any.whl:

Publisher: publish.yml on marclamberts/waltzinganalytics

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page