wa-setpieces 0.4.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- wa_setpieces-0.4.0/LICENSE +21 -0
- wa_setpieces-0.4.0/PKG-INFO +221 -0
- wa_setpieces-0.4.0/README.md +188 -0
- wa_setpieces-0.4.0/pyproject.toml +54 -0
- wa_setpieces-0.4.0/setup.cfg +4 -0
- wa_setpieces-0.4.0/src/wa_setpieces/__init__.py +76 -0
- wa_setpieces-0.4.0/src/wa_setpieces/chains.py +99 -0
- wa_setpieces-0.4.0/src/wa_setpieces/cli.py +85 -0
- wa_setpieces-0.4.0/src/wa_setpieces/constants.py +86 -0
- wa_setpieces-0.4.0/src/wa_setpieces/filters.py +116 -0
- wa_setpieces-0.4.0/src/wa_setpieces/loader.py +139 -0
- wa_setpieces-0.4.0/src/wa_setpieces/metrics.py +124 -0
- wa_setpieces-0.4.0/src/wa_setpieces/phases.py +241 -0
- wa_setpieces-0.4.0/src/wa_setpieces/retention.py +117 -0
- wa_setpieces-0.4.0/src/wa_setpieces/theme.py +99 -0
- wa_setpieces-0.4.0/src/wa_setpieces/viz.py +531 -0
- wa_setpieces-0.4.0/src/wa_setpieces/xt.py +220 -0
- wa_setpieces-0.4.0/src/wa_setpieces/zones.py +157 -0
- wa_setpieces-0.4.0/src/wa_setpieces.egg-info/PKG-INFO +221 -0
- wa_setpieces-0.4.0/src/wa_setpieces.egg-info/SOURCES.txt +32 -0
- wa_setpieces-0.4.0/src/wa_setpieces.egg-info/dependency_links.txt +1 -0
- wa_setpieces-0.4.0/src/wa_setpieces.egg-info/entry_points.txt +2 -0
- wa_setpieces-0.4.0/src/wa_setpieces.egg-info/requires.txt +17 -0
- wa_setpieces-0.4.0/src/wa_setpieces.egg-info/top_level.txt +1 -0
- wa_setpieces-0.4.0/tests/test_chains.py +33 -0
- wa_setpieces-0.4.0/tests/test_filters.py +74 -0
- wa_setpieces-0.4.0/tests/test_loader.py +59 -0
- wa_setpieces-0.4.0/tests/test_metrics.py +56 -0
- wa_setpieces-0.4.0/tests/test_phases.py +50 -0
- wa_setpieces-0.4.0/tests/test_retention.py +45 -0
- wa_setpieces-0.4.0/tests/test_theme.py +49 -0
- wa_setpieces-0.4.0/tests/test_viz.py +142 -0
- wa_setpieces-0.4.0/tests/test_xt.py +76 -0
- wa_setpieces-0.4.0/tests/test_zones.py +70 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Waltzing Analytics
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: wa-setpieces
|
|
3
|
+
Version: 0.4.0
|
|
4
|
+
Summary: Set-piece metrics and mplsoccer pitch visualizations (penalties, kick-offs, free kicks, corners, throw-ins, goal kicks) from Opta F24 event data, including second phases, xT, zones and retention
|
|
5
|
+
Author: Waltzing Analytics
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/marclamberts/waltzinganalytics
|
|
8
|
+
Project-URL: Documentation, https://waltzinganalytics.readthedocs.io
|
|
9
|
+
Keywords: football,soccer,opta,set-pieces,sports-analytics
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Classifier: Topic :: Scientific/Engineering :: Information Analysis
|
|
14
|
+
Classifier: Intended Audience :: Science/Research
|
|
15
|
+
Requires-Python: >=3.9
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
License-File: LICENSE
|
|
18
|
+
Requires-Dist: pandas>=1.5
|
|
19
|
+
Requires-Dist: numpy>=1.23
|
|
20
|
+
Provides-Extra: dev
|
|
21
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
22
|
+
Provides-Extra: viz
|
|
23
|
+
Requires-Dist: matplotlib>=3.6; extra == "viz"
|
|
24
|
+
Requires-Dist: mplsoccer>=1.2; extra == "viz"
|
|
25
|
+
Provides-Extra: docs
|
|
26
|
+
Requires-Dist: sphinx>=7.0; extra == "docs"
|
|
27
|
+
Requires-Dist: pydata-sphinx-theme>=0.15; extra == "docs"
|
|
28
|
+
Requires-Dist: sphinx-gallery>=0.15; extra == "docs"
|
|
29
|
+
Requires-Dist: myst-parser; extra == "docs"
|
|
30
|
+
Requires-Dist: matplotlib>=3.6; extra == "docs"
|
|
31
|
+
Requires-Dist: mplsoccer>=1.2; extra == "docs"
|
|
32
|
+
Dynamic: license-file
|
|
33
|
+
|
|
34
|
+
# wa-setpieces
|
|
35
|
+
|
|
36
|
+
Set-piece metrics for football (soccer) matches from **Opta / Stats Perform F24**
|
|
37
|
+
event-feed JSON exports: penalties, kick-offs, free kicks, corners,
|
|
38
|
+
throw-ins and goal kicks.
|
|
39
|
+
|
|
40
|
+
Given a raw F24 match file, this package tags every set-piece restart,
|
|
41
|
+
aggregates attempts/success rates by team and player, tracks pass end
|
|
42
|
+
locations for delivery maps, and links each set piece to the shot or goal
|
|
43
|
+
it produced (via Opta's assist-chain qualifier). It also covers, for
|
|
44
|
+
corners and free kicks specifically: second-phase detection, Expected
|
|
45
|
+
Threat (xT), pitch zones/thirds/channels, and possession retention —
|
|
46
|
+
with pitch plots built on [mplsoccer](https://mplsoccer.readthedocs.io).
|
|
47
|
+
|
|
48
|
+
<p align="center">
|
|
49
|
+
<img src="docs/source/_static/hero_corners.png" alt="Corner delivery map drawn with mplsoccer" width="640">
|
|
50
|
+
</p>
|
|
51
|
+
|
|
52
|
+
**Full documentation, with a runnable plot gallery: https://waltzinganalytics.readthedocs.io**
|
|
53
|
+
|
|
54
|
+
## Install
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
pip install wa-setpieces
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Not yet published to PyPI? Install from source instead:
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
git clone https://github.com/marclamberts/waltzinganalytics.git
|
|
64
|
+
cd waltzinganalytics
|
|
65
|
+
pip install -e .
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Quickstart
|
|
69
|
+
|
|
70
|
+
```python
|
|
71
|
+
from wa_setpieces import load_events, set_piece_summary
|
|
72
|
+
|
|
73
|
+
match = load_events("match.json")
|
|
74
|
+
summary = set_piece_summary(match.events)
|
|
75
|
+
print(summary)
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
```
|
|
79
|
+
contestantId set_piece_type attempts successful success_rate shots goals
|
|
80
|
+
cxb4hqite921i... corner 2 1 0.500 1 0
|
|
81
|
+
cxb4hqite921i... free_kick 12 9 0.750 0 0
|
|
82
|
+
cxb4hqite921i... goal_kick 8 5 0.625 0 0
|
|
83
|
+
cxb4hqite921i... kick_off 1 1 1.000 0 0
|
|
84
|
+
cxb4hqite921i... throw_in 20 16 0.800 1 0
|
|
85
|
+
...
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## Second phases, xT, zones and retention
|
|
89
|
+
|
|
90
|
+
```python
|
|
91
|
+
from wa_setpieces import (
|
|
92
|
+
second_phases, second_phase_summary, # corner/free-kick second-phase shots
|
|
93
|
+
retention_detail, retention_rate, # possession retained N seconds later
|
|
94
|
+
add_thirds, add_channels, add_zone_grid, # pitch location tagging
|
|
95
|
+
XTModel, set_piece_delivery_xt, set_piece_xt_summary, # Expected Threat
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
second_phases(match.events, "corner") # per-corner: cleared / first-phase shot / second-phase shot
|
|
99
|
+
second_phase_summary(match.events, "free_kick") # per-team roll-up
|
|
100
|
+
|
|
101
|
+
retention_rate(match.events, "corner") # per-team: % of corners where the ball is retained ~8s later
|
|
102
|
+
|
|
103
|
+
tagged = add_thirds(match.events) # defensive_third / middle_third / attacking_third
|
|
104
|
+
tagged = add_channels(tagged, n=5) # wide / half-space / central
|
|
105
|
+
|
|
106
|
+
model = XTModel.fit(match.events) # fit an xT grid (fit on many matches for real use!)
|
|
107
|
+
set_piece_xt_summary(match.events, "corner", model) # total/average xT added per team
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
All four are **derived heuristics**, not raw Opta fields — see
|
|
111
|
+
`docs/source/advanced.rst` (or the hosted docs) for the exact assumptions
|
|
112
|
+
and tunable thresholds behind each one.
|
|
113
|
+
|
|
114
|
+
## Plots
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
pip install -e ".[viz]" # matplotlib + mplsoccer
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
```python
|
|
121
|
+
from wa_setpieces.viz import (
|
|
122
|
+
plot_delivery_map, # arrow map of deliveries, colored by outcome
|
|
123
|
+
plot_zone_heatmap, # where events happen, gridded onto the pitch
|
|
124
|
+
plot_xt_grid, # a fitted XTModel's grid, as a heatmap
|
|
125
|
+
plot_second_phase, # one corner/free-kick's phase sequence, numbered
|
|
126
|
+
plot_team_comparison, # grouped bars: both teams, every set-piece type
|
|
127
|
+
plot_xt_added_bars, # diverging bar chart of xT added per delivery
|
|
128
|
+
plot_corner_sonar, # polar plot of delivery angle + distance
|
|
129
|
+
plot_match_timeline, # every set piece on one shared match-minute axis
|
|
130
|
+
plot_dashboard, # one-figure report card combining several of the above
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
plot_delivery_map(delivery_locations(match.events, "corner"), title="Corner deliveries")
|
|
134
|
+
plot_dashboard(match.events, team_id, set_piece_type="corner") # the "hero" figure
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
Every plotting function returns `(fig, ax)` (`plot_dashboard` returns just
|
|
138
|
+
`fig`, being multi-panel) for further customization. Colors are assigned by
|
|
139
|
+
the job they do — a validated categorical palette for team identity, a
|
|
140
|
+
status pair for success/fail, single-hue sequential ramps for magnitude,
|
|
141
|
+
and a diverging pair for signed quantities like xT added — not picked for
|
|
142
|
+
looks; see `wa_setpieces/theme.py`. See the
|
|
143
|
+
[gallery](https://waltzinganalytics.readthedocs.io/en/latest/gallery/index.html)
|
|
144
|
+
for all nine plots with full source code.
|
|
145
|
+
|
|
146
|
+
## Command line
|
|
147
|
+
|
|
148
|
+
```bash
|
|
149
|
+
wa-setpieces match.json
|
|
150
|
+
wa-setpieces match.json --csv summary.csv
|
|
151
|
+
wa-setpieces match.json --xt # also fit + print xT for this match (illustrative on one match)
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
## What counts as a set piece
|
|
155
|
+
|
|
156
|
+
| Type | Detected on | Opta qualifierId |
|
|
157
|
+
|-------------|-------------------------------------------|-------------------|
|
|
158
|
+
| Penalty | shot event (miss/post/saved/goal) | 9 |
|
|
159
|
+
| Kick-off | pass event | 279 |
|
|
160
|
+
| Free kick | pass event (corners excluded) | 5 |
|
|
161
|
+
| Corner | pass event | 6 |
|
|
162
|
+
| Throw-in | pass event | 107 |
|
|
163
|
+
| Goal kick | pass event | 124 |
|
|
164
|
+
|
|
165
|
+
These qualifier IDs are the standard Opta/Stats Perform F24 vocabulary and
|
|
166
|
+
were cross-checked against a real match export (see `tests/data/sample_match.json`
|
|
167
|
+
and `tests/test_filters.py`): tagged events line up with their expected pitch
|
|
168
|
+
location (corner arc, touchline, centre spot, six-yard line).
|
|
169
|
+
|
|
170
|
+
## Package layout
|
|
171
|
+
|
|
172
|
+
- `wa_setpieces.loader` — parse F24 JSON into a tidy `pandas.DataFrame`; `load_events_multi` stacks a whole season.
|
|
173
|
+
- `wa_setpieces.constants` — Opta typeId / qualifierId reference.
|
|
174
|
+
- `wa_setpieces.filters` — extract/tag each set-piece type.
|
|
175
|
+
- `wa_setpieces.metrics` — team/player counts, success rates, delivery locations.
|
|
176
|
+
- `wa_setpieces.chains` — link set pieces to the shots/goals they produced.
|
|
177
|
+
- `wa_setpieces.zones` — pitch thirds, channels and a configurable zone grid.
|
|
178
|
+
- `wa_setpieces.phases` — second-phase detection for corners/free kicks.
|
|
179
|
+
- `wa_setpieces.retention` — possession retention after any restart.
|
|
180
|
+
- `wa_setpieces.xt` — grid-based Expected Threat (xT), fit from data.
|
|
181
|
+
- `wa_setpieces.viz` — mplsoccer/matplotlib plots: delivery maps, heatmaps, sonar, timeline, dashboard (optional `viz` extra).
|
|
182
|
+
- `wa_setpieces.theme` — the validated color palette every plot draws from.
|
|
183
|
+
- `wa_setpieces.cli` — `wa-setpieces` command-line tool.
|
|
184
|
+
|
|
185
|
+
## Development
|
|
186
|
+
|
|
187
|
+
```bash
|
|
188
|
+
pip install -e ".[dev]"
|
|
189
|
+
pytest
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
## Releasing
|
|
193
|
+
|
|
194
|
+
Publishing to PyPI is automated via GitHub Actions trusted publishing
|
|
195
|
+
(`.github/workflows/publish.yml`) — no API token stored anywhere. **One-time
|
|
196
|
+
setup** (only a PyPI project owner can do this, since it requires logging
|
|
197
|
+
into PyPI):
|
|
198
|
+
|
|
199
|
+
1. On PyPI: <https://pypi.org/manage/account/publishing/> → add a pending
|
|
200
|
+
trusted publisher with project name `wa-setpieces`, owner
|
|
201
|
+
`marclamberts`, repository `waltzinganalytics`, workflow `publish.yml`,
|
|
202
|
+
environment `pypi`.
|
|
203
|
+
2. From then on, publishing a GitHub Release (or pushing a `v*` tag) builds
|
|
204
|
+
the sdist/wheel and uploads them automatically.
|
|
205
|
+
|
|
206
|
+
## Docs
|
|
207
|
+
|
|
208
|
+
Docs are built with Sphinx (`pydata-sphinx-theme` + `sphinx-gallery`, the
|
|
209
|
+
same stack mplsoccer's docs use) and hosted on Read the Docs
|
|
210
|
+
(`.readthedocs.yaml` at the repo root). The gallery under `examples_gallery/`
|
|
211
|
+
is executed at build time, so its plots and DataFrame outputs are always
|
|
212
|
+
current. To build locally:
|
|
213
|
+
|
|
214
|
+
```bash
|
|
215
|
+
pip install -e ".[docs]"
|
|
216
|
+
sphinx-build -b html docs/source docs/_build/html
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
## License
|
|
220
|
+
|
|
221
|
+
MIT
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
# wa-setpieces
|
|
2
|
+
|
|
3
|
+
Set-piece metrics for football (soccer) matches from **Opta / Stats Perform F24**
|
|
4
|
+
event-feed JSON exports: penalties, kick-offs, free kicks, corners,
|
|
5
|
+
throw-ins and goal kicks.
|
|
6
|
+
|
|
7
|
+
Given a raw F24 match file, this package tags every set-piece restart,
|
|
8
|
+
aggregates attempts/success rates by team and player, tracks pass end
|
|
9
|
+
locations for delivery maps, and links each set piece to the shot or goal
|
|
10
|
+
it produced (via Opta's assist-chain qualifier). It also covers, for
|
|
11
|
+
corners and free kicks specifically: second-phase detection, Expected
|
|
12
|
+
Threat (xT), pitch zones/thirds/channels, and possession retention —
|
|
13
|
+
with pitch plots built on [mplsoccer](https://mplsoccer.readthedocs.io).
|
|
14
|
+
|
|
15
|
+
<p align="center">
|
|
16
|
+
<img src="docs/source/_static/hero_corners.png" alt="Corner delivery map drawn with mplsoccer" width="640">
|
|
17
|
+
</p>
|
|
18
|
+
|
|
19
|
+
**Full documentation, with a runnable plot gallery: https://waltzinganalytics.readthedocs.io**
|
|
20
|
+
|
|
21
|
+
## Install
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
pip install wa-setpieces
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Not yet published to PyPI? Install from source instead:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
git clone https://github.com/marclamberts/waltzinganalytics.git
|
|
31
|
+
cd waltzinganalytics
|
|
32
|
+
pip install -e .
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Quickstart
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
from wa_setpieces import load_events, set_piece_summary
|
|
39
|
+
|
|
40
|
+
match = load_events("match.json")
|
|
41
|
+
summary = set_piece_summary(match.events)
|
|
42
|
+
print(summary)
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
```
|
|
46
|
+
contestantId set_piece_type attempts successful success_rate shots goals
|
|
47
|
+
cxb4hqite921i... corner 2 1 0.500 1 0
|
|
48
|
+
cxb4hqite921i... free_kick 12 9 0.750 0 0
|
|
49
|
+
cxb4hqite921i... goal_kick 8 5 0.625 0 0
|
|
50
|
+
cxb4hqite921i... kick_off 1 1 1.000 0 0
|
|
51
|
+
cxb4hqite921i... throw_in 20 16 0.800 1 0
|
|
52
|
+
...
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Second phases, xT, zones and retention
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
from wa_setpieces import (
|
|
59
|
+
second_phases, second_phase_summary, # corner/free-kick second-phase shots
|
|
60
|
+
retention_detail, retention_rate, # possession retained N seconds later
|
|
61
|
+
add_thirds, add_channels, add_zone_grid, # pitch location tagging
|
|
62
|
+
XTModel, set_piece_delivery_xt, set_piece_xt_summary, # Expected Threat
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
second_phases(match.events, "corner") # per-corner: cleared / first-phase shot / second-phase shot
|
|
66
|
+
second_phase_summary(match.events, "free_kick") # per-team roll-up
|
|
67
|
+
|
|
68
|
+
retention_rate(match.events, "corner") # per-team: % of corners where the ball is retained ~8s later
|
|
69
|
+
|
|
70
|
+
tagged = add_thirds(match.events) # defensive_third / middle_third / attacking_third
|
|
71
|
+
tagged = add_channels(tagged, n=5) # wide / half-space / central
|
|
72
|
+
|
|
73
|
+
model = XTModel.fit(match.events) # fit an xT grid (fit on many matches for real use!)
|
|
74
|
+
set_piece_xt_summary(match.events, "corner", model) # total/average xT added per team
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
All four are **derived heuristics**, not raw Opta fields — see
|
|
78
|
+
`docs/source/advanced.rst` (or the hosted docs) for the exact assumptions
|
|
79
|
+
and tunable thresholds behind each one.
|
|
80
|
+
|
|
81
|
+
## Plots
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
pip install -e ".[viz]" # matplotlib + mplsoccer
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
```python
|
|
88
|
+
from wa_setpieces.viz import (
|
|
89
|
+
plot_delivery_map, # arrow map of deliveries, colored by outcome
|
|
90
|
+
plot_zone_heatmap, # where events happen, gridded onto the pitch
|
|
91
|
+
plot_xt_grid, # a fitted XTModel's grid, as a heatmap
|
|
92
|
+
plot_second_phase, # one corner/free-kick's phase sequence, numbered
|
|
93
|
+
plot_team_comparison, # grouped bars: both teams, every set-piece type
|
|
94
|
+
plot_xt_added_bars, # diverging bar chart of xT added per delivery
|
|
95
|
+
plot_corner_sonar, # polar plot of delivery angle + distance
|
|
96
|
+
plot_match_timeline, # every set piece on one shared match-minute axis
|
|
97
|
+
plot_dashboard, # one-figure report card combining several of the above
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
plot_delivery_map(delivery_locations(match.events, "corner"), title="Corner deliveries")
|
|
101
|
+
plot_dashboard(match.events, team_id, set_piece_type="corner") # the "hero" figure
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Every plotting function returns `(fig, ax)` (`plot_dashboard` returns just
|
|
105
|
+
`fig`, being multi-panel) for further customization. Colors are assigned by
|
|
106
|
+
the job they do — a validated categorical palette for team identity, a
|
|
107
|
+
status pair for success/fail, single-hue sequential ramps for magnitude,
|
|
108
|
+
and a diverging pair for signed quantities like xT added — not picked for
|
|
109
|
+
looks; see `wa_setpieces/theme.py`. See the
|
|
110
|
+
[gallery](https://waltzinganalytics.readthedocs.io/en/latest/gallery/index.html)
|
|
111
|
+
for all nine plots with full source code.
|
|
112
|
+
|
|
113
|
+
## Command line
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
wa-setpieces match.json
|
|
117
|
+
wa-setpieces match.json --csv summary.csv
|
|
118
|
+
wa-setpieces match.json --xt # also fit + print xT for this match (illustrative on one match)
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## What counts as a set piece
|
|
122
|
+
|
|
123
|
+
| Type | Detected on | Opta qualifierId |
|
|
124
|
+
|-------------|-------------------------------------------|-------------------|
|
|
125
|
+
| Penalty | shot event (miss/post/saved/goal) | 9 |
|
|
126
|
+
| Kick-off | pass event | 279 |
|
|
127
|
+
| Free kick | pass event (corners excluded) | 5 |
|
|
128
|
+
| Corner | pass event | 6 |
|
|
129
|
+
| Throw-in | pass event | 107 |
|
|
130
|
+
| Goal kick | pass event | 124 |
|
|
131
|
+
|
|
132
|
+
These qualifier IDs are the standard Opta/Stats Perform F24 vocabulary and
|
|
133
|
+
were cross-checked against a real match export (see `tests/data/sample_match.json`
|
|
134
|
+
and `tests/test_filters.py`): tagged events line up with their expected pitch
|
|
135
|
+
location (corner arc, touchline, centre spot, six-yard line).
|
|
136
|
+
|
|
137
|
+
## Package layout
|
|
138
|
+
|
|
139
|
+
- `wa_setpieces.loader` — parse F24 JSON into a tidy `pandas.DataFrame`; `load_events_multi` stacks a whole season.
|
|
140
|
+
- `wa_setpieces.constants` — Opta typeId / qualifierId reference.
|
|
141
|
+
- `wa_setpieces.filters` — extract/tag each set-piece type.
|
|
142
|
+
- `wa_setpieces.metrics` — team/player counts, success rates, delivery locations.
|
|
143
|
+
- `wa_setpieces.chains` — link set pieces to the shots/goals they produced.
|
|
144
|
+
- `wa_setpieces.zones` — pitch thirds, channels and a configurable zone grid.
|
|
145
|
+
- `wa_setpieces.phases` — second-phase detection for corners/free kicks.
|
|
146
|
+
- `wa_setpieces.retention` — possession retention after any restart.
|
|
147
|
+
- `wa_setpieces.xt` — grid-based Expected Threat (xT), fit from data.
|
|
148
|
+
- `wa_setpieces.viz` — mplsoccer/matplotlib plots: delivery maps, heatmaps, sonar, timeline, dashboard (optional `viz` extra).
|
|
149
|
+
- `wa_setpieces.theme` — the validated color palette every plot draws from.
|
|
150
|
+
- `wa_setpieces.cli` — `wa-setpieces` command-line tool.
|
|
151
|
+
|
|
152
|
+
## Development
|
|
153
|
+
|
|
154
|
+
```bash
|
|
155
|
+
pip install -e ".[dev]"
|
|
156
|
+
pytest
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
## Releasing
|
|
160
|
+
|
|
161
|
+
Publishing to PyPI is automated via GitHub Actions trusted publishing
|
|
162
|
+
(`.github/workflows/publish.yml`) — no API token stored anywhere. **One-time
|
|
163
|
+
setup** (only a PyPI project owner can do this, since it requires logging
|
|
164
|
+
into PyPI):
|
|
165
|
+
|
|
166
|
+
1. On PyPI: <https://pypi.org/manage/account/publishing/> → add a pending
|
|
167
|
+
trusted publisher with project name `wa-setpieces`, owner
|
|
168
|
+
`marclamberts`, repository `waltzinganalytics`, workflow `publish.yml`,
|
|
169
|
+
environment `pypi`.
|
|
170
|
+
2. From then on, publishing a GitHub Release (or pushing a `v*` tag) builds
|
|
171
|
+
the sdist/wheel and uploads them automatically.
|
|
172
|
+
|
|
173
|
+
## Docs
|
|
174
|
+
|
|
175
|
+
Docs are built with Sphinx (`pydata-sphinx-theme` + `sphinx-gallery`, the
|
|
176
|
+
same stack mplsoccer's docs use) and hosted on Read the Docs
|
|
177
|
+
(`.readthedocs.yaml` at the repo root). The gallery under `examples_gallery/`
|
|
178
|
+
is executed at build time, so its plots and DataFrame outputs are always
|
|
179
|
+
current. To build locally:
|
|
180
|
+
|
|
181
|
+
```bash
|
|
182
|
+
pip install -e ".[docs]"
|
|
183
|
+
sphinx-build -b html docs/source docs/_build/html
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
## License
|
|
187
|
+
|
|
188
|
+
MIT
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "wa-setpieces"
|
|
7
|
+
version = "0.4.0"
|
|
8
|
+
description = "Set-piece metrics and mplsoccer pitch visualizations (penalties, kick-offs, free kicks, corners, throw-ins, goal kicks) from Opta F24 event data, including second phases, xT, zones and retention"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [{ name = "Waltzing Analytics" }]
|
|
13
|
+
keywords = ["football", "soccer", "opta", "set-pieces", "sports-analytics"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Programming Language :: Python :: 3",
|
|
16
|
+
"License :: OSI Approved :: MIT License",
|
|
17
|
+
"Operating System :: OS Independent",
|
|
18
|
+
"Topic :: Scientific/Engineering :: Information Analysis",
|
|
19
|
+
"Intended Audience :: Science/Research",
|
|
20
|
+
]
|
|
21
|
+
dependencies = [
|
|
22
|
+
"pandas>=1.5",
|
|
23
|
+
"numpy>=1.23",
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
[project.optional-dependencies]
|
|
27
|
+
dev = [
|
|
28
|
+
"pytest>=7.0",
|
|
29
|
+
]
|
|
30
|
+
viz = [
|
|
31
|
+
"matplotlib>=3.6",
|
|
32
|
+
"mplsoccer>=1.2",
|
|
33
|
+
]
|
|
34
|
+
docs = [
|
|
35
|
+
"sphinx>=7.0",
|
|
36
|
+
"pydata-sphinx-theme>=0.15",
|
|
37
|
+
"sphinx-gallery>=0.15",
|
|
38
|
+
"myst-parser",
|
|
39
|
+
"matplotlib>=3.6",
|
|
40
|
+
"mplsoccer>=1.2",
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
[project.scripts]
|
|
44
|
+
wa-setpieces = "wa_setpieces.cli:main"
|
|
45
|
+
|
|
46
|
+
[project.urls]
|
|
47
|
+
Homepage = "https://github.com/marclamberts/waltzinganalytics"
|
|
48
|
+
Documentation = "https://waltzinganalytics.readthedocs.io"
|
|
49
|
+
|
|
50
|
+
[tool.setuptools.packages.find]
|
|
51
|
+
where = ["src"]
|
|
52
|
+
|
|
53
|
+
[tool.pytest.ini_options]
|
|
54
|
+
testpaths = ["tests"]
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""wa-setpieces: set-piece metrics (penalties, kick-offs, free kicks,
|
|
2
|
+
corners, throw-ins, goal kicks) from Opta / Stats Perform F24 event data.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from .chains import link_set_piece_shots, set_piece_goal_summary
|
|
6
|
+
from .filters import (
|
|
7
|
+
extract_all,
|
|
8
|
+
extract_corners,
|
|
9
|
+
extract_free_kicks,
|
|
10
|
+
extract_goal_kicks,
|
|
11
|
+
extract_kick_offs,
|
|
12
|
+
extract_penalties,
|
|
13
|
+
extract_throw_ins,
|
|
14
|
+
tag_set_pieces,
|
|
15
|
+
)
|
|
16
|
+
from .loader import Match, load_events, load_events_multi
|
|
17
|
+
from .metrics import (
|
|
18
|
+
delivery_locations,
|
|
19
|
+
player_set_piece_counts,
|
|
20
|
+
set_piece_summary,
|
|
21
|
+
team_set_piece_counts,
|
|
22
|
+
)
|
|
23
|
+
from .phases import (
|
|
24
|
+
PhaseResult,
|
|
25
|
+
classify_phase,
|
|
26
|
+
second_phase_summary,
|
|
27
|
+
second_phases,
|
|
28
|
+
)
|
|
29
|
+
from .retention import retention_detail, retention_rate
|
|
30
|
+
from .xt import XTModel, set_piece_delivery_xt, set_piece_xt_summary
|
|
31
|
+
from .zones import (
|
|
32
|
+
add_channels,
|
|
33
|
+
add_thirds,
|
|
34
|
+
add_zone_grid,
|
|
35
|
+
to_reference_frame,
|
|
36
|
+
zone_counts,
|
|
37
|
+
zone_id,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
__version__ = "0.4.0"
|
|
41
|
+
|
|
42
|
+
__all__ = [
|
|
43
|
+
"Match",
|
|
44
|
+
"load_events",
|
|
45
|
+
"load_events_multi",
|
|
46
|
+
"extract_all",
|
|
47
|
+
"extract_corners",
|
|
48
|
+
"extract_free_kicks",
|
|
49
|
+
"extract_goal_kicks",
|
|
50
|
+
"extract_kick_offs",
|
|
51
|
+
"extract_penalties",
|
|
52
|
+
"extract_throw_ins",
|
|
53
|
+
"tag_set_pieces",
|
|
54
|
+
"link_set_piece_shots",
|
|
55
|
+
"set_piece_goal_summary",
|
|
56
|
+
"team_set_piece_counts",
|
|
57
|
+
"player_set_piece_counts",
|
|
58
|
+
"delivery_locations",
|
|
59
|
+
"set_piece_summary",
|
|
60
|
+
"PhaseResult",
|
|
61
|
+
"classify_phase",
|
|
62
|
+
"second_phases",
|
|
63
|
+
"second_phase_summary",
|
|
64
|
+
"retention_detail",
|
|
65
|
+
"retention_rate",
|
|
66
|
+
"XTModel",
|
|
67
|
+
"set_piece_delivery_xt",
|
|
68
|
+
"set_piece_xt_summary",
|
|
69
|
+
"add_thirds",
|
|
70
|
+
"add_channels",
|
|
71
|
+
"add_zone_grid",
|
|
72
|
+
"to_reference_frame",
|
|
73
|
+
"zone_id",
|
|
74
|
+
"zone_counts",
|
|
75
|
+
"__version__",
|
|
76
|
+
]
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"""Link set pieces to the shots and goals they produce.
|
|
2
|
+
|
|
3
|
+
Opta connects a shot to the pass that created it via qualifier 55
|
|
4
|
+
("related event id"), which stores the ``eventId`` of the assisting pass.
|
|
5
|
+
A shot can also *be* a set piece directly (e.g. a direct free kick or a
|
|
6
|
+
penalty), in which case there is no separate assisting pass to look up.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import pandas as pd
|
|
12
|
+
|
|
13
|
+
from . import constants as c
|
|
14
|
+
from .filters import tag_set_pieces
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def link_set_piece_shots(events: pd.DataFrame) -> pd.DataFrame:
|
|
18
|
+
"""Return one row per shot (incl. goals) with its set-piece origin, if any.
|
|
19
|
+
|
|
20
|
+
Columns:
|
|
21
|
+
eventId, typeId, outcome, is_goal, contestantId, playerId, playerName,
|
|
22
|
+
x, y, timeMin, timeSec, set_piece_type, set_piece_event_id.
|
|
23
|
+
|
|
24
|
+
``set_piece_type`` is ``None`` for shots that did not originate from a
|
|
25
|
+
tagged set piece (open play, or a phase the assist qualifier does not
|
|
26
|
+
reach back to).
|
|
27
|
+
"""
|
|
28
|
+
tagged = tag_set_pieces(events)
|
|
29
|
+
shots = tagged.loc[tagged["typeId"].isin(c.SHOT_TYPE_IDS)].copy()
|
|
30
|
+
|
|
31
|
+
by_event_id = tagged.set_index("eventId", drop=False)
|
|
32
|
+
|
|
33
|
+
set_piece_type: list[str | None] = []
|
|
34
|
+
set_piece_event_id: list[int | None] = []
|
|
35
|
+
for _, shot in shots.iterrows():
|
|
36
|
+
# Case 1: the shot itself is the set piece (direct free kick, penalty).
|
|
37
|
+
if shot["set_piece_type"] is not None:
|
|
38
|
+
set_piece_type.append(shot["set_piece_type"])
|
|
39
|
+
set_piece_event_id.append(shot["eventId"])
|
|
40
|
+
continue
|
|
41
|
+
|
|
42
|
+
# Case 2: walk back to the assisting pass via qualifier 55.
|
|
43
|
+
related_raw = shot.get(f"q_{c.QUALIFIER_RELATED_EVENT_ID}")
|
|
44
|
+
origin_type = None
|
|
45
|
+
origin_event_id = None
|
|
46
|
+
if related_raw is not None:
|
|
47
|
+
try:
|
|
48
|
+
related_id = int(float(related_raw))
|
|
49
|
+
except (TypeError, ValueError):
|
|
50
|
+
related_id = None
|
|
51
|
+
if related_id is not None and related_id in by_event_id.index:
|
|
52
|
+
origin = by_event_id.loc[related_id]
|
|
53
|
+
if isinstance(origin, pd.DataFrame): # duplicate eventId guard
|
|
54
|
+
origin = origin.iloc[0]
|
|
55
|
+
if origin["set_piece_type"] is not None:
|
|
56
|
+
origin_type = origin["set_piece_type"]
|
|
57
|
+
origin_event_id = int(related_id)
|
|
58
|
+
set_piece_type.append(origin_type)
|
|
59
|
+
set_piece_event_id.append(origin_event_id)
|
|
60
|
+
|
|
61
|
+
shots["is_goal"] = shots["typeId"] == c.TYPE_GOAL
|
|
62
|
+
shots["set_piece_type"] = set_piece_type
|
|
63
|
+
shots["set_piece_event_id"] = set_piece_event_id
|
|
64
|
+
|
|
65
|
+
cols = [
|
|
66
|
+
"eventId",
|
|
67
|
+
"typeId",
|
|
68
|
+
"outcome",
|
|
69
|
+
"is_goal",
|
|
70
|
+
"contestantId",
|
|
71
|
+
"playerId",
|
|
72
|
+
"playerName",
|
|
73
|
+
"x",
|
|
74
|
+
"y",
|
|
75
|
+
"timeMin",
|
|
76
|
+
"timeSec",
|
|
77
|
+
"set_piece_type",
|
|
78
|
+
"set_piece_event_id",
|
|
79
|
+
]
|
|
80
|
+
return shots[cols].reset_index(drop=True)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def set_piece_goal_summary(events: pd.DataFrame) -> pd.DataFrame:
|
|
84
|
+
"""Goals scored per set-piece type per team.
|
|
85
|
+
|
|
86
|
+
Returns a DataFrame indexed by ``(contestantId, set_piece_type)`` with a
|
|
87
|
+
``goals`` count, restricted to set pieces that produced at least one goal.
|
|
88
|
+
"""
|
|
89
|
+
linked = link_set_piece_shots(events)
|
|
90
|
+
goals = linked.loc[linked["is_goal"] & linked["set_piece_type"].notna()]
|
|
91
|
+
if goals.empty:
|
|
92
|
+
return pd.DataFrame(columns=["contestantId", "set_piece_type", "goals"])
|
|
93
|
+
summary = (
|
|
94
|
+
goals.groupby(["contestantId", "set_piece_type"])
|
|
95
|
+
.size()
|
|
96
|
+
.rename("goals")
|
|
97
|
+
.reset_index()
|
|
98
|
+
)
|
|
99
|
+
return summary
|