By value model
“Value model” isn’t one thing in this package – it’s a small chain of them, each building on the last. This page is the same kind of reference as By set piece, just organized by value model instead of set piece: pick one below, see what it measures, what it needs, and how to compute it.
How they connect
XTModel.value -- "how dangerous is this pitch zone"
|
+-- XTModel.action_value -- xT added by one delivery
|
+-- XTModel.shot_value -- P(goal) if a shot is taken from here
|
v
set_piece_added_value = delivery's xT added + shot_value of the shot it produced
|
v
team_rating / player_rating -- both benchmarked 0-100 against a sample
wa_setpieces.ml.shot_value is a separate, independent shot-scorer –
a bundle of five pre-trained gradient-boosted models, richer than
XTModel.shot_value but not (yet) wired into added_value or the
ratings above it. See its own section below for why the two aren’t the
same thing.
Expected Threat (xT)
- What it measures
How dangerous a pitch zone is: the probability that possession starting there eventually leads to a goal, expressed 0-1 per zone on a grid. Karun Singh’s grid-based method – fit once from data, then value any pass as
xT[end_zone] - xT[start_zone].- Requirements
As many matches as you can fit on – a single match is nowhere near enough data for a trustworthy grid. No optional extras needed; pure
pandas/numpy.
Compute it
from wa_setpieces import XTModel, load_events_multi
season_events = load_events_multi(match_files)
model = XTModel.fit(season_events)
model.value(x=95, y=40) # xT of one pitch location
model.action_value(80, 30, 95, 40) # xT added by one pass
model.shot_value(95, 40) # P(goal) if shot from here -- fit() only
model.save("xt_grid.npz") # full model, including shot/goal grids
model2 = XTModel.load("xt_grid.npz") # reload later, no refit needed
- Where it’s used
Everything below this section. Only
cornerandfree_kickhave a wired-in xT path today – see By set piece’s coverage matrix.
Added value
- What it measures
One number per set-piece delivery, blending the delivery’s own xT added with – if it produced a shot – how good that chance was:
added_value = delivery_xt_added + shot_value. Always a real number (0 where nothing happened), so it’s summable across a match or season.- Requirements
A fitted
XTModel(fromXTModel.fit(), notXTModel.from_csv()– the shot-value grids aren’t persisted byto_csv/from_csv, only bysave/load). Only meaningful for"corner"/"free_kick"– seeset_piece_added_value()’s own docstring.
Compute it
from wa_setpieces import set_piece_added_value, set_piece_value_summary
set_piece_added_value(events, "corner", model) # per-delivery breakdown
set_piece_value_summary(events, "corner", model) # per-team total/average
The shot a delivery “produced” is resolved via Opta’s own assist-chain
qualifier (link_set_piece_shots()), not a positional
guess – see By metric’s “Linking to shots and goals” entry.
- Where it’s used
Feeds directly into
team_rating’savg_added_valuecomponent andplayer_rating’s delivery-score half, for corner/free kick. Also the basis for most of this package’s value-focused charts in the Gallery:plot_set_piece_value_flow()(cumulative, over match time),plot_value_waterfall()(decomposed by restart type), and three ways of showing the same per-delivery spread –plot_value_distribution()(violins),plot_value_ridgeline()(KDE curves) andplot_value_boxplot()(quartiles and outliers).
Shot value (experimental, ML-based)
- What it measures
A richer, five-model shot quality score –
wa_setpieces.ml.shot_value– distinct fromXTModel.shot_valueabove. Five gradient-boosted models (P(on target), xG-on-target, post-shot xG, a situational signal, a 4-class outcome distribution), blended into oneshot_valuecolumn per shot.
Important
This is not what set_piece_added_value uses – that’s the
grid-based XTModel.shot_value, fit from your own data, dependency-free.
This module is pre-trained, heavier, and richer, but several of its
input features are reconstructed best-effort guesses from Opta
qualifiers rather than confirmed ground truth. Read the module
docstring in full before trusting the output – it documents exactly
which columns are confidently derived versus experimental.
- Requirements
The
mlextra:pip install "wa-setpieces[ml]"(xgboost, scikit-learn, joblib). Models ship inside the package (~4.5MB), no external download or training required.
Compute it
from wa_setpieces.ml.shot_value import shot_value, ShotValueModels
models = ShotValueModels.load() # load once, reuse across matches
shot_value(events, models) # one row per shot: on_target_prob, xgot, psxg, shot_value
- Where it’s used
Standalone today – not wired into
added_valueor the ratings below. Use it when you want a second, independent shot-quality opinion to compare against the xT-grid-based one, not as a drop-in replacement.
Team rating
- What it measures
A single 0-100 “how good” score per team, benchmarked against whoever else is in the table (z-scored,
50 + z * 15, clipped to[0, 100]). There is no universal 50 – always rate a full season/competition, not one match.- Requirements
A
set_piece_report()table. Works with or without a fitted model – without one, the rating drops the added-value component and scores off success rate and retention rate alone (this is how throw-in/penalty/goal-kick/kick-off get a rating despite having no xT path – see By set piece).
Compute it
from wa_setpieces import team_rating, corner_report
team_rating(corner_report(season_events, model=model)) # with added value
team_rating(corner_report(season_events)) # without -- success/retention only
- Where it’s used
Every set-piece type gets one – the one rating in this package with no coverage gaps.
Player rating
- What it measures
Two independently benchmarked halves: a delivery score (taker quality, from
added_valuegrouped by who took the set piece) and a finishing score (shooter quality, fromXTModel.shot_valueon every shot traced back to this set-piece type, grouped by who took the shot). Merged so a pure taker or pure finisher is rated on the component they actually have.- Requirements
A fitted
XTModel. Only meaningful for"corner"/"free_kick"– same coverage as added value, since it’s built from it.
Compute it
from wa_setpieces import player_rating
player_rating(season_events, "corner", model, min_deliveries=5, min_shots=3)
# columns: delivery_score / finishing_score / rating, per player
min_deliveries/min_shots exclude too-small samples from being
rated on noise rather than skill – see wa_setpieces.core.rating’s
module docstring.
- Where it’s used
Corner/free-kick player breakdowns only –
Nonefor every other type inrun_workflow()’s output.
Defensive rating
- What it measures
The conceding-side mirror of team rating – 0-100, but lower concessions score better (the metrics are negated before z-scoring, so “100” always means “best defensively,” same direction as attacking ratings).
- Requirements
A
defensive_set_piece_summary()table (needsopponent_success_rate,shots_conceded_per_100,goals_conceded).
Compute it
from wa_setpieces import defensive_set_piece_summary, defensive_rating
defensive_rating(defensive_set_piece_summary(events))
- Where it’s used
Standalone – the opponent-scouting side of a defensive workflow, not blended into the attacking
team_ratingabove.