football-docs 0.1.2 → 0.2.0
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.
- package/README.md +94 -30
- package/data/docs.db +0 -0
- package/dist/crawl.d.ts +34 -0
- package/dist/crawl.js +419 -0
- package/dist/discover.d.ts +53 -0
- package/dist/discover.js +121 -0
- package/dist/http.d.ts +8 -0
- package/dist/http.js +36 -0
- package/dist/index.d.ts +6 -4
- package/dist/index.js +126 -18
- package/dist/ingest.d.ts +36 -12
- package/dist/ingest.js +120 -52
- package/docs/databallpy/data-model.md +146 -0
- package/docs/databallpy/overview.md +71 -0
- package/docs/databallpy/usage.md +280 -0
- package/docs/mplsoccer/overview.md +25 -0
- package/docs/mplsoccer/pitch-types.md +88 -0
- package/docs/mplsoccer/visualizations.md +280 -0
- package/docs/soccerdata/data-sources.md +131 -0
- package/docs/soccerdata/overview.md +54 -0
- package/docs/soccerdata/usage.md +157 -0
- package/package.json +24 -5
- package/providers.json +170 -0
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
# DataBallPy Usage Guide
|
|
2
|
+
|
|
3
|
+
## Installation
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
pip install databallpy
|
|
7
|
+
|
|
8
|
+
# With kloppy integration (expands provider support)
|
|
9
|
+
pip install 'databallpy[kloppy]'
|
|
10
|
+
|
|
11
|
+
# With dangerous accessible space metric
|
|
12
|
+
pip install 'databallpy[accessible-space]'
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Loading Data
|
|
16
|
+
|
|
17
|
+
### From Files with Native Parsers
|
|
18
|
+
|
|
19
|
+
```python
|
|
20
|
+
from databallpy import get_game
|
|
21
|
+
|
|
22
|
+
# Tracab tracking + Opta events (most common combo)
|
|
23
|
+
game = get_game(
|
|
24
|
+
tracking_data_loc="../data/tracking_data.dat",
|
|
25
|
+
tracking_metadata_loc="../data/tracking_metadata.xml",
|
|
26
|
+
tracking_data_provider="tracab",
|
|
27
|
+
event_data_loc="../data/event_data_f24.xml",
|
|
28
|
+
event_metadata_loc="../data/event_metadata_f7.xml",
|
|
29
|
+
event_data_provider="opta",
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
# StatsBomb events only (requires extra files)
|
|
33
|
+
game = get_game(
|
|
34
|
+
event_data_loc="events.json",
|
|
35
|
+
event_match_loc="match.json",
|
|
36
|
+
event_lineup_loc="lineup.json",
|
|
37
|
+
event_data_provider="statsbomb",
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
# Metrica tracking + events
|
|
41
|
+
game = get_game(
|
|
42
|
+
tracking_data_loc="tracking_home.csv",
|
|
43
|
+
tracking_metadata_loc="metadata.xml",
|
|
44
|
+
tracking_data_provider="metrica",
|
|
45
|
+
event_data_loc="events.json",
|
|
46
|
+
event_data_provider="metrica",
|
|
47
|
+
)
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### From Kloppy Datasets
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
from kloppy import sportec
|
|
54
|
+
from databallpy import get_game_from_kloppy
|
|
55
|
+
|
|
56
|
+
tracking_dataset = sportec.load_open_tracking_data(match_id="J03WPY", only_alive=False)
|
|
57
|
+
event_dataset = sportec.load_open_event_data(match_id="J03WPY")
|
|
58
|
+
game = get_game_from_kloppy(
|
|
59
|
+
tracking_dataset=tracking_dataset,
|
|
60
|
+
event_dataset=event_dataset
|
|
61
|
+
)
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### Open Data (DFL/Bundesliga)
|
|
65
|
+
|
|
66
|
+
```python
|
|
67
|
+
from databallpy import get_open_game
|
|
68
|
+
|
|
69
|
+
# 7 available matches (2 Bundesliga 1 + 5 Bundesliga 2)
|
|
70
|
+
game = get_open_game(provider="sportec", game_id="J03WMX") # Köln vs Bayern
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### Save and Load Games
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
# Save (creates parquet + JSON files)
|
|
77
|
+
game.save_game(name="my_game", path="/data")
|
|
78
|
+
|
|
79
|
+
# Load
|
|
80
|
+
from databallpy import get_saved_game
|
|
81
|
+
game = get_saved_game(name="my_game", path="/data")
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Accessing Data
|
|
85
|
+
|
|
86
|
+
```python
|
|
87
|
+
# DataFrames
|
|
88
|
+
game.tracking_data # TrackingData (custom DataFrame)
|
|
89
|
+
game.event_data # EventData (custom DataFrame)
|
|
90
|
+
game.shot_events # DataFrame of ShotEvent objects
|
|
91
|
+
game.pass_events # DataFrame of PassEvent objects
|
|
92
|
+
game.dribble_events # DataFrame of DribbleEvent objects
|
|
93
|
+
|
|
94
|
+
# Player column IDs (for tracking data)
|
|
95
|
+
home_col_ids = game.get_column_ids(team="home")
|
|
96
|
+
defenders = game.get_column_ids(team="home", positions=["defender"])
|
|
97
|
+
|
|
98
|
+
# Get specific frames
|
|
99
|
+
frame_data = game.get_frames(12345, playing_direction="team_oriented")
|
|
100
|
+
frame_data = game.get_frames(12345, playing_direction="possession_oriented")
|
|
101
|
+
|
|
102
|
+
# Get event and its tracking frame
|
|
103
|
+
event = game.get_event(event_id=42)
|
|
104
|
+
frame = game.get_event_frame(event_id=42)
|
|
105
|
+
|
|
106
|
+
# Match metadata
|
|
107
|
+
game.home_team_name, game.away_team_name
|
|
108
|
+
game.home_score, game.away_score
|
|
109
|
+
game.home_formation, game.away_formation
|
|
110
|
+
game.pitch_dimensions # [length, width] in metres
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## Synchronization
|
|
114
|
+
|
|
115
|
+
Aligns tracking and event data using the Needleman-Wunsch algorithm. After sync, tracking data gets `event_id`, `databallpy_event`, `sync_certainty` columns; event data gets `tracking_frame`, `sync_certainty` columns.
|
|
116
|
+
|
|
117
|
+
```python
|
|
118
|
+
game.synchronise_tracking_and_event_data(
|
|
119
|
+
n_batches="smart", # or int for manual batch count
|
|
120
|
+
verbose=True,
|
|
121
|
+
offset=1, # seconds offset for alignment
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
# Check sync status
|
|
125
|
+
game.is_synchronised # bool
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
Quality checks must pass for sync to be allowed (kick-off near origin, ball alive >50% of game, player positions within bounds).
|
|
129
|
+
|
|
130
|
+
## Preprocessing
|
|
131
|
+
|
|
132
|
+
### Filtering Tracking Data
|
|
133
|
+
|
|
134
|
+
```python
|
|
135
|
+
game.tracking_data.filter_tracking_data(
|
|
136
|
+
column_ids=home_col_ids,
|
|
137
|
+
filter_type="savitzky_golay",
|
|
138
|
+
window_length=7,
|
|
139
|
+
polyorder=2,
|
|
140
|
+
)
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### Velocity and Acceleration
|
|
144
|
+
|
|
145
|
+
```python
|
|
146
|
+
# Add velocity columns (_vx, _vy)
|
|
147
|
+
game.tracking_data.add_velocity(
|
|
148
|
+
column_ids=home_col_ids,
|
|
149
|
+
filter_type=None, # optional smoothing
|
|
150
|
+
max_velocity=np.inf,
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
# Add acceleration columns (_ax, _ay)
|
|
154
|
+
game.tracking_data.add_acceleration(
|
|
155
|
+
column_ids=home_col_ids,
|
|
156
|
+
filter_type=None,
|
|
157
|
+
max_acceleration=np.inf,
|
|
158
|
+
)
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
## Analytics and Metrics
|
|
162
|
+
|
|
163
|
+
### Covered Distance (by velocity/acceleration zones)
|
|
164
|
+
|
|
165
|
+
```python
|
|
166
|
+
result_df = game.tracking_data.get_covered_distance(
|
|
167
|
+
column_ids=home_col_ids,
|
|
168
|
+
velocity_intervals=((0, 5), (5, 20)), # m/s
|
|
169
|
+
acceleration_intervals=((2, np.inf),), # m/s²
|
|
170
|
+
)
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
### Pressure (Herold & Kempe 2022)
|
|
174
|
+
|
|
175
|
+
```python
|
|
176
|
+
pressure = game.tracking_data.get_pressure_on_player(
|
|
177
|
+
index=12345, # frame index
|
|
178
|
+
column_id="home_1", # player to measure pressure on
|
|
179
|
+
pitch_size=game.pitch_dimensions,
|
|
180
|
+
d_front="variable", # or float
|
|
181
|
+
d_back=3.0,
|
|
182
|
+
q=1.75,
|
|
183
|
+
)
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
### Team Possession (from synchronized events)
|
|
187
|
+
|
|
188
|
+
```python
|
|
189
|
+
game.tracking_data.add_team_possession(game.event_data, game.home_team_id)
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
### Individual Player Possession (Vidal-Codina et al. 2022)
|
|
193
|
+
|
|
194
|
+
```python
|
|
195
|
+
game.tracking_data.add_individual_player_possession(
|
|
196
|
+
pz_radius=1.5,
|
|
197
|
+
bv_threshold=5.0,
|
|
198
|
+
ba_threshold=10.0,
|
|
199
|
+
min_frames_pz=0,
|
|
200
|
+
)
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
### Pitch Control (Gaussian influence model)
|
|
204
|
+
|
|
205
|
+
```python
|
|
206
|
+
pitch_control = game.tracking_data.get_pitch_control(
|
|
207
|
+
pitch_dimensions=game.pitch_dimensions,
|
|
208
|
+
n_x_bins=106,
|
|
209
|
+
n_y_bins=68,
|
|
210
|
+
)
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
### Voronoi Space Occupation
|
|
214
|
+
|
|
215
|
+
```python
|
|
216
|
+
distances, assigned = game.tracking_data.get_approximate_voronoi(
|
|
217
|
+
pitch_dimensions=game.pitch_dimensions,
|
|
218
|
+
n_x_bins=106,
|
|
219
|
+
n_y_bins=68,
|
|
220
|
+
)
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
### Dangerous Accessible Space
|
|
224
|
+
|
|
225
|
+
Requires `pip install 'databallpy[accessible-space]'`:
|
|
226
|
+
|
|
227
|
+
```python
|
|
228
|
+
game.tracking_data.add_dangerous_accessible_space(mask=None)
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
## Visualization
|
|
232
|
+
|
|
233
|
+
```python
|
|
234
|
+
from databallpy.visualize import (
|
|
235
|
+
plot_soccer_pitch, plot_tracking_data, plot_events, save_tracking_video
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
# Plot empty pitch
|
|
239
|
+
fig, ax = plot_soccer_pitch(field_dimen=(106.0, 68.0), pitch_color="mediumseagreen")
|
|
240
|
+
|
|
241
|
+
# Plot single tracking frame
|
|
242
|
+
fig, ax = plot_tracking_data(
|
|
243
|
+
game, idx=12345,
|
|
244
|
+
team_colors=["green", "red"],
|
|
245
|
+
events=["pass", "shot"],
|
|
246
|
+
add_velocities=True,
|
|
247
|
+
add_player_possession=True,
|
|
248
|
+
heatmap_overlay=some_array,
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
# Plot events on pitch
|
|
252
|
+
fig, ax = plot_events(game, events=["shot"], outcome=True, team_id=game.home_team_id)
|
|
253
|
+
|
|
254
|
+
# Save video clip (requires ffmpeg)
|
|
255
|
+
save_tracking_video(
|
|
256
|
+
game, start_idx=1000, end_idx=2000,
|
|
257
|
+
save_folder="./clips", title="goal_clip",
|
|
258
|
+
events=["shot"],
|
|
259
|
+
)
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
## Video Analysis Export
|
|
263
|
+
|
|
264
|
+
Export events to SportsCode/Longomatch-compatible XML:
|
|
265
|
+
|
|
266
|
+
```python
|
|
267
|
+
xml_str = game.event_data.to_video_analysis_xml(
|
|
268
|
+
databallpy_events=["shot", "pass"],
|
|
269
|
+
before_seconds=3.0,
|
|
270
|
+
after_seconds=3.0,
|
|
271
|
+
)
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
## Long Format Conversion
|
|
275
|
+
|
|
276
|
+
Convert tracking data from wide format (one column per player axis) to long format (one row per frame/player):
|
|
277
|
+
|
|
278
|
+
```python
|
|
279
|
+
long_df = game.tracking_data.to_long_format()
|
|
280
|
+
```
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# mplsoccer
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
mplsoccer is a Python library for plotting soccer/football charts using Matplotlib and loading StatsBomb open data. Created by Andrew Rowlinson and Anmol Durgapal, licensed under MIT. Current version: 1.6.1.
|
|
6
|
+
|
|
7
|
+
- **Install**: `pip install mplsoccer` or `conda install -c conda-forge mplsoccer`
|
|
8
|
+
- **Dependencies**: matplotlib>=3.6, numpy, pandas, pillow, requests, scipy, seaborn
|
|
9
|
+
|
|
10
|
+
## Key Features
|
|
11
|
+
|
|
12
|
+
**Pitch-based visualizations**: scatter plots, heatmaps, hexbin, KDE plots, arrows (pass maps), comet lines, flow maps, Voronoi diagrams, Delaunay tessellation, convex hulls, polygons, goal angle visualization, pass networks, shot freeze frames, formations, Juego de Posición heatmaps, pass sonars, animations.
|
|
13
|
+
|
|
14
|
+
**Non-pitch chart types**: radar charts (`Radar`), pizza/Nightingale charts (`PyPizza`), bumpy charts (`Bumpy`), turbine charts.
|
|
15
|
+
|
|
16
|
+
**Utilities**: `FontManager` (Google Fonts), `Standardizer` (coordinate conversion), `add_image`/`inset_image`, `grid()`/`jointgrid()` layout helpers, `Sbopen`/`Sbapi`/`Sblocal` (StatsBomb data loading).
|
|
17
|
+
|
|
18
|
+
## Limitations and Caveats
|
|
19
|
+
|
|
20
|
+
- **Y-axis inversion varies by provider**: StatsBomb, Wyscout, and Metrica have inverted y-axes. mplsoccer handles this internally.
|
|
21
|
+
- **Tracking data pitch types require explicit dimensions**: `tracab`, `metricasports`, `skillcorner`, `secondspectrum`, and `custom` all need `pitch_length` and `pitch_width`.
|
|
22
|
+
- **Wyscout goal width discrepancy**: mplsoccer uses ggsoccer's 12 units (vs socceraction's 10 units).
|
|
23
|
+
- **Opta vs Wyscout**: Both use 0-100 coordinates but Wyscout has inverted y-axis. Easy to mix up.
|
|
24
|
+
- **Matplotlib-based**: Performance for very large datasets or interactive use can be limited.
|
|
25
|
+
- **No built-in data fetching beyond StatsBomb**: For Opta, Wyscout, etc., load data yourself; mplsoccer only handles visualization and coordinate standardization.
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# mplsoccer Pitch Types and Coordinate Systems
|
|
2
|
+
|
|
3
|
+
## Supported Pitch Types
|
|
4
|
+
|
|
5
|
+
| Pitch Type | Provider | X Range | Y Range | Dimensions Required | Notes |
|
|
6
|
+
|---|---|---|---|---|---|
|
|
7
|
+
| `statsbomb` (default) | StatsBomb | 0-120 | 80-0 (inverted y) | No (fixed) | Default pitch type |
|
|
8
|
+
| `opta` | Stats Perform / Opta | 0-100 | 0-100 | No (fixed) | Also used by Sofascore, WhoScored |
|
|
9
|
+
| `wyscout` | Wyscout / Hudl | 0-100 | 0-100 (inverted y) | No (fixed) | Similar to Opta but y-inverted |
|
|
10
|
+
| `tracab` | TRACAB / ChyronHego | Centered, cm | Centered, cm | Yes | Values in centimetres |
|
|
11
|
+
| `uefa` | UEFA | 0-105 | 0-68 | No (fixed) | Standard 105m x 68m |
|
|
12
|
+
| `metricasports` | Metrica Sports | 0-1 | 0-1 (inverted y) | Yes | Normalised coordinates |
|
|
13
|
+
| `skillcorner` | SkillCorner | Centered, m | Centered, m | Yes | Values in metres |
|
|
14
|
+
| `secondspectrum` | Second Spectrum | Centered, m | Centered, m | Yes | Values in metres |
|
|
15
|
+
| `impect` | Impect | -52.5 to 52.5 | -34 to 34 | No (fixed) | Centered |
|
|
16
|
+
| `custom` | Any | 0 to pitch_length | 0 to pitch_width | Yes | Fully configurable |
|
|
17
|
+
|
|
18
|
+
Tracking data providers (`tracab`, `metricasports`, `skillcorner`, `secondspectrum`, `custom`) require explicit `pitch_length` and `pitch_width` (typically 105 and 68).
|
|
19
|
+
|
|
20
|
+
## Pitch Constructor
|
|
21
|
+
|
|
22
|
+
```python
|
|
23
|
+
from mplsoccer import Pitch, VerticalPitch
|
|
24
|
+
|
|
25
|
+
# Basic pitch
|
|
26
|
+
pitch = Pitch(pitch_type='statsbomb')
|
|
27
|
+
|
|
28
|
+
# Tracking data pitch (requires dimensions)
|
|
29
|
+
pitch = Pitch(pitch_type='tracab', pitch_length=105, pitch_width=68)
|
|
30
|
+
|
|
31
|
+
# Customisation
|
|
32
|
+
pitch = Pitch(
|
|
33
|
+
pitch_type='statsbomb',
|
|
34
|
+
pitch_color='grass', # 'grass' or any color
|
|
35
|
+
line_color='white',
|
|
36
|
+
stripe=True, # alternating grass stripes
|
|
37
|
+
stripe_color='#c2d59d',
|
|
38
|
+
half=True, # show only half pitch
|
|
39
|
+
goal_type='box', # 'line', 'box', 'circle', or None
|
|
40
|
+
positional=True, # Juego de Posición zone lines
|
|
41
|
+
corner_arcs=True,
|
|
42
|
+
pad_left=5, pad_right=5, # padding in data units
|
|
43
|
+
pad_bottom=5, pad_top=5,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
# Vertical orientation (rotated 90°)
|
|
47
|
+
vpitch = VerticalPitch(pitch_type='opta')
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Coordinate Standardization
|
|
51
|
+
|
|
52
|
+
The `Standardizer` converts coordinates between any two pitch types by mapping pitch markings (penalty areas, six-yard boxes, etc.) -- not just linear scaling. This is more accurate for event data but means the mapping is piecewise-linear.
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
from mplsoccer import Standardizer
|
|
56
|
+
|
|
57
|
+
# Convert Opta (0-100) to StatsBomb (120x80)
|
|
58
|
+
std = Standardizer(pitch_from='opta', pitch_to='statsbomb')
|
|
59
|
+
x_new, y_new = std.transform(x, y)
|
|
60
|
+
|
|
61
|
+
# Reverse direction
|
|
62
|
+
x_orig, y_orig = std.transform(x_new, y_new, reverse=True)
|
|
63
|
+
|
|
64
|
+
# For tracking data, specify dimensions
|
|
65
|
+
std = Standardizer(
|
|
66
|
+
pitch_from='tracab', pitch_to='statsbomb',
|
|
67
|
+
length_from=105, width_from=68
|
|
68
|
+
)
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Custom Dimensions
|
|
72
|
+
|
|
73
|
+
For normalised ML coordinate systems, use `center_scale_dims()`:
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
from mplsoccer.dimensions import center_scale_dims
|
|
77
|
+
|
|
78
|
+
# Create a -1 to 1 coordinate space
|
|
79
|
+
custom_dims = center_scale_dims(
|
|
80
|
+
pitch_width=68, pitch_length=105,
|
|
81
|
+
width=2, length=2,
|
|
82
|
+
invert_y=False
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
pitch = Pitch(pitch_type=custom_dims)
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
For fully custom dimensions, subclass `BaseSoccerDims`.
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
# mplsoccer Visualizations
|
|
2
|
+
|
|
3
|
+
## Drawing a Pitch
|
|
4
|
+
|
|
5
|
+
```python
|
|
6
|
+
from mplsoccer import Pitch, VerticalPitch
|
|
7
|
+
|
|
8
|
+
pitch = Pitch()
|
|
9
|
+
fig, ax = pitch.draw()
|
|
10
|
+
|
|
11
|
+
# Grid of pitches
|
|
12
|
+
fig, axs = pitch.draw(nrows=2, ncols=3)
|
|
13
|
+
|
|
14
|
+
# On existing axes
|
|
15
|
+
pitch.draw(ax=existing_ax)
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
All plotting methods are called on the pitch object, passing `ax=ax` as a parameter.
|
|
19
|
+
|
|
20
|
+
## Scatter Plots (Shot Maps, Player Positions)
|
|
21
|
+
|
|
22
|
+
```python
|
|
23
|
+
pitch.scatter(x, y, ax=ax, s=100, c='red', edgecolors='black', zorder=2)
|
|
24
|
+
|
|
25
|
+
# With rotation (e.g., for player orientation arrows)
|
|
26
|
+
pitch.scatter(x, y, rotation_degrees=angle, ax=ax)
|
|
27
|
+
|
|
28
|
+
# Football marker
|
|
29
|
+
pitch.scatter(x, y, marker='football', ax=ax)
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Heatmaps (Binned Statistics)
|
|
33
|
+
|
|
34
|
+
Two-step workflow: bin the data, then plot.
|
|
35
|
+
|
|
36
|
+
```python
|
|
37
|
+
# Step 1: Bin the data
|
|
38
|
+
bin_statistic = pitch.bin_statistic(x, y, statistic='count', bins=(25, 25))
|
|
39
|
+
|
|
40
|
+
# Optional: Gaussian smoothing
|
|
41
|
+
from scipy.ndimage import gaussian_filter
|
|
42
|
+
bin_statistic['statistic'] = gaussian_filter(bin_statistic['statistic'], sigma=1)
|
|
43
|
+
|
|
44
|
+
# Step 2: Plot
|
|
45
|
+
pcm = pitch.heatmap(bin_statistic, ax=ax, cmap='hot', edgecolors='#22312b')
|
|
46
|
+
|
|
47
|
+
# Optional: Add labels
|
|
48
|
+
pitch.label_heatmap(bin_statistic, ax=ax, str_format='{:.0f}', color='white',
|
|
49
|
+
fontsize=8, exclude_zeros=True)
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Juego de Posición Heatmaps
|
|
53
|
+
|
|
54
|
+
Positional play zones (used by Pep Guardiola-style analysis):
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
bin_statistic = pitch.bin_statistic_positional(x, y, statistic='count')
|
|
58
|
+
pitch.heatmap_positional(bin_statistic, ax=ax, cmap='coolwarm', edgecolors='#22312b')
|
|
59
|
+
pitch.label_heatmap(bin_statistic, ax=ax, str_format='{:.0f}')
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Hexbin Plots
|
|
63
|
+
|
|
64
|
+
```python
|
|
65
|
+
pitch.hexbin(x, y, ax=ax, gridsize=20, cmap='Reds', edgecolors='grey')
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
NaN values are filtered out internally.
|
|
69
|
+
|
|
70
|
+
## KDE Plots (Kernel Density Estimation)
|
|
71
|
+
|
|
72
|
+
Uses seaborn internally, auto-clipped to pitch boundaries.
|
|
73
|
+
|
|
74
|
+
```python
|
|
75
|
+
pitch.kdeplot(x, y, ax=ax, fill=True, levels=100, cmap='Reds', thresh=0)
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Arrows (Pass Maps)
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
pitch.arrows(xstart, ystart, xend, yend, ax=ax,
|
|
82
|
+
color='blue', width=2, headwidth=5, headlength=5)
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Lines and Comet Lines
|
|
86
|
+
|
|
87
|
+
```python
|
|
88
|
+
# Standard lines
|
|
89
|
+
pitch.lines(xstart, ystart, xend, yend, ax=ax, lw=2, color='blue')
|
|
90
|
+
|
|
91
|
+
# Comet lines (transparency gradient from start to end)
|
|
92
|
+
pitch.lines(xstart, ystart, xend, yend, ax=ax, comet=True,
|
|
93
|
+
transparent=True, alpha_start=0.01, alpha_end=1.0)
|
|
94
|
+
|
|
95
|
+
# Comet with colormap
|
|
96
|
+
pitch.lines(xstart, ystart, xend, yend, ax=ax, comet=True,
|
|
97
|
+
cmap='plasma', n_segments=100)
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Note: comet lines split each line into `n_segments` (default 100). Large datasets may be slow.
|
|
101
|
+
|
|
102
|
+
## Flow Maps
|
|
103
|
+
|
|
104
|
+
Binned average direction and distance as arrows:
|
|
105
|
+
|
|
106
|
+
```python
|
|
107
|
+
pitch.flow(xstart, ystart, xend, yend, ax=ax,
|
|
108
|
+
bins=(6, 4), arrow_type='same', arrow_length=10, color='blue')
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## Voronoi Diagrams
|
|
112
|
+
|
|
113
|
+
```python
|
|
114
|
+
team1, team2 = pitch.voronoi(x, y, teams)
|
|
115
|
+
pitch.polygon(team1, ax=ax, fc='red', alpha=0.3, ec='black')
|
|
116
|
+
pitch.polygon(team2, ax=ax, fc='blue', alpha=0.3, ec='black')
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Uses a reflection trick to extend Voronoi cells to pitch edges. Players outside pitch boundaries are clipped.
|
|
120
|
+
|
|
121
|
+
## Convex Hulls
|
|
122
|
+
|
|
123
|
+
```python
|
|
124
|
+
hull = pitch.convexhull(x, y)
|
|
125
|
+
pitch.polygon(hull, ax=ax, facecolor='cornflowerblue', alpha=0.3)
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## Delaunay Tessellation
|
|
129
|
+
|
|
130
|
+
```python
|
|
131
|
+
pitch.triplot(x, y, ax=ax, color='blue', linewidth=2)
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
## Goal Angle Visualization
|
|
135
|
+
|
|
136
|
+
```python
|
|
137
|
+
pitch.goal_angle(x, y, ax=ax, goal='right', alpha=0.3, color='red')
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
## Pass Sonars
|
|
141
|
+
|
|
142
|
+
```python
|
|
143
|
+
bin_statistic = pitch.bin_statistic_sonar(x, y, angle, bins=(6, 4, 8))
|
|
144
|
+
pitch.sonar(bin_statistic, ax=ax, fc='cornflowerblue')
|
|
145
|
+
pitch.sonar_grid(bin_statistic, ax=ax)
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
## Radar Charts
|
|
149
|
+
|
|
150
|
+
```python
|
|
151
|
+
from mplsoccer import Radar
|
|
152
|
+
|
|
153
|
+
radar = Radar(
|
|
154
|
+
params=['Goals', 'Assists', 'xG', 'xA', 'Passes'],
|
|
155
|
+
min_range=[0, 0, 0, 0, 0],
|
|
156
|
+
max_range=[30, 20, 25, 15, 3000],
|
|
157
|
+
lower_is_better=['Miscontrol'], # invert specific params
|
|
158
|
+
num_rings=4,
|
|
159
|
+
ring_width=1,
|
|
160
|
+
center_circle_radius=2,
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
fig, ax = radar.setup_axis()
|
|
164
|
+
radar.draw_circles(ax=ax)
|
|
165
|
+
rings_inner, rings_outer, vertices = radar.draw_radar(
|
|
166
|
+
values, ax=ax, kwargs_radar={'facecolor': '#aa65b2', 'alpha': 0.6}
|
|
167
|
+
)
|
|
168
|
+
radar.draw_range_labels(ax=ax)
|
|
169
|
+
radar.draw_param_labels(ax=ax)
|
|
170
|
+
|
|
171
|
+
# Comparison radar (two players)
|
|
172
|
+
radar.draw_radar_compare(values1, values2, ax=ax)
|
|
173
|
+
|
|
174
|
+
# Multi-player (3+) -- stacked solid fills
|
|
175
|
+
radar.draw_radar_solid(values_list, ax=ax)
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
## Pizza Charts (Percentile Ranks)
|
|
179
|
+
|
|
180
|
+
```python
|
|
181
|
+
from mplsoccer import PyPizza
|
|
182
|
+
|
|
183
|
+
baker = PyPizza(
|
|
184
|
+
params=params,
|
|
185
|
+
straight_line_color="#000000",
|
|
186
|
+
straight_line_lw=1,
|
|
187
|
+
last_circle_lw=1,
|
|
188
|
+
other_circle_lw=1,
|
|
189
|
+
other_circle_ls="-.",
|
|
190
|
+
inner_circle_size=20,
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
fig, ax = baker.make_pizza(
|
|
194
|
+
values, figsize=(8, 8), param_location=110,
|
|
195
|
+
kwargs_slices=dict(facecolor='cornflowerblue', edgecolor='black'),
|
|
196
|
+
kwargs_params=dict(color='black', fontsize=12),
|
|
197
|
+
kwargs_values=dict(color='black', fontsize=12),
|
|
198
|
+
)
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
## Bumpy Charts (Rank Changes)
|
|
202
|
+
|
|
203
|
+
```python
|
|
204
|
+
from mplsoccer import Bumpy
|
|
205
|
+
|
|
206
|
+
bumpy = Bumpy(
|
|
207
|
+
scatter_color='#282828',
|
|
208
|
+
line_color='#252525',
|
|
209
|
+
rotate_xticks=90,
|
|
210
|
+
ticklabel_size=12,
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
fig, ax = bumpy.plot(
|
|
214
|
+
x_list=matchweeks, y_list=rankings, values=team_names,
|
|
215
|
+
highlight_dict={'Arsenal': 'red'},
|
|
216
|
+
figsize=(16, 10),
|
|
217
|
+
)
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
## Layout Helpers
|
|
221
|
+
|
|
222
|
+
### Grid (title + pitch + endnote)
|
|
223
|
+
|
|
224
|
+
```python
|
|
225
|
+
fig, axs = pitch.grid(figheight=10, title_height=0.06, endnote_height=0.03)
|
|
226
|
+
# axs is a dict: axs['pitch'], axs['title'], axs['endnote']
|
|
227
|
+
axs['title'].text(0.5, 0.5, 'My Title', ha='center', va='center')
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
### Joint Grid (pitch + marginal axes)
|
|
231
|
+
|
|
232
|
+
```python
|
|
233
|
+
fig, axs = pitch.jointgrid(ax_top=True, ax_right=True)
|
|
234
|
+
# axs['pitch'], axs['top'], axs['right'], axs['title'], axs['endnote']
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
## StatsBomb Data Loading
|
|
238
|
+
|
|
239
|
+
```python
|
|
240
|
+
from mplsoccer import Sbopen
|
|
241
|
+
|
|
242
|
+
parser = Sbopen()
|
|
243
|
+
|
|
244
|
+
# Browse available data
|
|
245
|
+
df_competition = parser.competition()
|
|
246
|
+
df_match = parser.match(competition_id=11, season_id=1)
|
|
247
|
+
|
|
248
|
+
# Load match data
|
|
249
|
+
df_lineup = parser.lineup(match_id)
|
|
250
|
+
df_event, df_related, df_freeze, df_tactics = parser.event(match_id)
|
|
251
|
+
|
|
252
|
+
# StatsBomb 360 data
|
|
253
|
+
frames, visible = parser.frame(match_id)
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
Also available: `Sbapi` (for API access with credentials) and `Sblocal` (for local files).
|
|
257
|
+
|
|
258
|
+
## Utility Functions
|
|
259
|
+
|
|
260
|
+
```python
|
|
261
|
+
from mplsoccer import FontManager, add_image
|
|
262
|
+
|
|
263
|
+
# Load Google Fonts
|
|
264
|
+
fm = FontManager('https://raw.githubusercontent.com/google/fonts/main/ofl/roboto/Roboto-Regular.ttf')
|
|
265
|
+
|
|
266
|
+
# Add image/logo to plot
|
|
267
|
+
add_image(image, fig, left=0.1, bottom=0.9, width=0.1)
|
|
268
|
+
|
|
269
|
+
# Inset axes on pitch (e.g., for mini charts at player positions)
|
|
270
|
+
inset_ax = pitch.inset_axes(x, y, width=10, height=10, ax=ax)
|
|
271
|
+
|
|
272
|
+
# Inset image on pitch
|
|
273
|
+
pitch.inset_image(x, y, image, width=10, ax=ax)
|
|
274
|
+
|
|
275
|
+
# Flip coordinates to other half
|
|
276
|
+
x_flip, y_flip = pitch.flip_side(x, y, flip=True)
|
|
277
|
+
|
|
278
|
+
# Calculate angle and distance
|
|
279
|
+
angle, distance = pitch.calculate_angle_and_distance(x1, y1, x2, y2, degrees=True)
|
|
280
|
+
```
|