Tabular Classification
English
xgboost
sports-analytics
soccer
football
vaep
action-valuation
statsbomb
wyscout
idsse
metrica
skillcorner
silly-kicks
File size: 13,834 Bytes
a13f796
 
 
 
 
 
 
 
 
 
 
 
 
a6ef619
 
 
a13f796
 
 
 
 
 
 
 
a6ef619
a13f796
a6ef619
a13f796
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a6ef619
a13f796
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8935b3e
 
 
 
 
 
 
 
 
 
 
 
a13f796
 
 
 
 
 
 
 
 
 
a6ef619
a13f796
 
a6ef619
a13f796
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a6ef619
61e88da
a13f796
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8935b3e
a13f796
8935b3e
a13f796
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
---
license: cc-by-nc-4.0
language: en
library_name: xgboost
tags:
  - sports-analytics
  - soccer
  - football
  - vaep
  - action-valuation
  - xgboost
  - statsbomb
  - wyscout
  - idsse
  - metrica
  - skillcorner
  - silly-kicks
datasets:
  - luxury-lakehouse/spadl-vaep-action-values
  - luxury-lakehouse/xg-shot-data
  - luxury-lakehouse/football2vec-player-embeddings
pipeline_tag: tabular-classification
---

# VAEP Model

Two XGBClassifier models that estimate **P(scoring)** and **P(conceding)** within the next 10 actions, enabling per-action valuation of every on-ball event in a soccer match. Trained on **~5,414 matches** across five open providers — [StatsBomb Open Data](https://github.com/statsbomb/open-data), [Wyscout](https://figshare.com/collections/Soccer_match_event_dataset/4415000), IDSSE (DFL Bundesliga), Metrica Sports, and SkillCorner — via [Hugging Face Jobs](https://huggingface.co/docs/hub/jobs) (CPU). Own goals are valued (≈ −1) as of silly-kicks 4.13.

Part of the (Right! Luxury!) Lakehouse soccer analytics platform.

## Model Description

**VAEP** (Valuing Actions by Estimating Probabilities) scores each on-ball action by its impact on the probability of scoring and conceding within the next 10 actions, as described in:

> Decroos, T., Bransen, L., Van Haaren, J., & Davis, J. (2019). **Actions Speak Louder than Goals: Valuing Player Actions in Soccer.** *Proceedings of the 25th ACM SIGKDD International Conference on Knowledge Discovery & Data Mining.*

This model implements the VAEP framework using two independent XGBClassifier models:

- **P(scores)**: Probability that the team in possession scores within the next 10 actions
- **P(concedes)**: Probability that the team in possession concedes within the next 10 actions

The net VAEP value of an action is the change in scoring probability minus the change in conceding probability before and after that action:

```
VAEP(action_i) = [P_scores(S_i) - P_scores(S_{i-1})] + [P_concedes(S_{i-1}) - P_concedes(S_i)]
```

where `S_i` is the game state after action `i`.

### Architecture

Both models are XGBClassifier instances with identical hyperparameters:

| Parameter | Value |
|-----------|-------|
| `n_estimators` | 100 |
| `max_depth` | 3 |
| `learning_rate` | 0.1 |
| `objective` | `binary:logistic` |
| `eval_metric` | `logloss` |
| `random_state` | 42 |

### Feature Extraction

Features are extracted using the [silly-kicks](https://github.com/karsten-s-nielsen/silly-kicks) library with 11 feature functions applied to game states composed of the current action and the previous `NB_PREV_ACTIONS = 3` actions:

| Feature Function | Description |
|-----------------|-------------|
| `actiontype_onehot` | One-hot encoding of the 23 SPADL action types |
| `result_onehot` | One-hot encoding of action outcomes (success, fail, etc.) |
| `bodypart_onehot` | One-hot encoding of body part (foot, head, other) |
| `time` | Period and time within the period |
| `startlocation` | Start x, y coordinates (SPADL 105×68m) |
| `endlocation` | End x, y coordinates |
| `startpolar` | Start location in polar coordinates (distance + angle to goal) |
| `endpolar` | End location in polar coordinates |
| `movement` | Displacement between start and end locations |
| `team` | Whether the team changed between consecutive actions |
| `time_delta` | Time elapsed between consecutive actions |

With `NB_PREV_ACTIONS = 3`, each feature function generates columns for the current action plus the 3 preceding actions, creating a rich game state representation.

### Serialization

Both models are serialized in a single **JSON envelope** — no pickle is used (banned by project security policy):

- Each model's XGBoost booster is saved via `save_raw("json")` and base64-encoded
- The envelope includes the number of input features and `nb_prev_actions` for reproducibility

```json
{
  "model_type": "vaep_xgboost_v1",
  "scores_booster_b64": "...",
  "concedes_booster_b64": "...",
  "n_features": 264,
  "nb_prev_actions": 3
}
```

This makes model weights fully inspectable, version-controllable, and safe to load without arbitrary code execution.

## Training Data

| Source | Matches | License |
|--------|---------|---------|
| [StatsBomb Open Data](https://github.com/statsbomb/open-data) | ~3,000 | CC-BY 4.0 |
| [Wyscout Public Dataset](https://figshare.com/collections/Soccer_match_event_dataset/4415000) | ~1,900 | CC-BY-NC 4.0 |
| **Total** | **~2,388** (deduplicated) | CC-BY-NC 4.0 (most restrictive applies) |

Coverage includes the Premier League, La Liga, Serie A, Bundesliga, Ligue 1, Champions League, World Cup, and more.

All event data is converted to the **SPADL** (Soccer Player Action Description Language) unified format with standardized coordinates (105×68 meters) and 23 canonical action types, enabling cross-source training without vendor-specific adapters.

Training is performed on [Hugging Face Jobs](https://huggingface.co/docs/hub/jobs) using the `cpu-basic` flavor.

### Training / Test Split

- **80/20 split** by game, stratified by `competition_id`
- Evaluation metrics computed on the held-out test set

## How to Use

### Quick Start

```bash
pip install huggingface_hub xgboost
```

```python
import json
import base64

from huggingface_hub import snapshot_download
from xgboost import XGBClassifier

# Download model
model_dir = snapshot_download("luxury-lakehouse/vaep-model")

# Load from JSON envelope
with open(f"{model_dir}/vaep_model.json") as f:
    envelope = json.load(f)

# Deserialize P(scores) model
model_scores = XGBClassifier()
scores_raw = base64.b64decode(envelope["scores_booster_b64"])
model_scores.load_model(bytearray(scores_raw))

# Deserialize P(concedes) model
model_concedes = XGBClassifier()
concedes_raw = base64.b64decode(envelope["concedes_booster_b64"])
model_concedes.load_model(bytearray(concedes_raw))

# Predict probabilities (requires silly-kicks feature extraction)
# p_scores = model_scores.predict_proba(X)[:, 1]
# p_concedes = model_concedes.predict_proba(X)[:, 1]
```

### Full Pipeline (with silly-kicks)

```python
import silly_kicks.spadl as spadl
import silly_kicks.vaep.features as fs
import silly_kicks.vaep.labels as labels

NB_PREV_ACTIONS = 3

FEATURE_FNS = [
    fs.actiontype_onehot, fs.result_onehot, fs.bodypart_onehot,
    fs.time, fs.startlocation, fs.endlocation,
    fs.startpolar, fs.endpolar, fs.movement, fs.team, fs.time_delta,
]

# actions: pandas DataFrame in SPADL format
gamestates = fs.gamestates(actions, nb_prev_actions=NB_PREV_ACTIONS)
X = pd.concat([fn(gamestates) for fn in FEATURE_FNS], axis=1)

p_scores = model_scores.predict_proba(X)[:, 1]
p_concedes = model_concedes.predict_proba(X)[:, 1]

# Compute VAEP values (change in probabilities)
vaep_offensive = p_scores[1:] - p_scores[:-1]   # delta P(scores)
vaep_defensive = p_concedes[:-1] - p_concedes[1:]  # delta P(concedes), note sign flip
vaep_value = vaep_offensive + vaep_defensive
```

## Intended Use

- **Player valuation**: Rank players by total VAEP contribution beyond goals and assists
- **Action analysis**: Identify the most impactful passes, carries, and defensive actions in a match
- **Tactical analysis**: Evaluate team playing styles by aggregating VAEP across action types
- **Scouting**: Compare players across leagues using a unified valuation framework
- **Research**: Reproducible VAEP implementation on open data

## Three-Axis Interpretation

VAEP's two probability deltas map naturally to three cognitive axes for interpretation, borrowing framing from García de Marina's xR framework (SOCCHUB 2026):

- **Survival** — the defensive component (`vaep_defensive`). How much did the action reduce the opponent's scoring probability? Positive = safer (opponent less likely to score). Negative = riskier (opponent more likely to score). Computed as `P(concedes)_before - P(concedes)_after` — the sign flip in the `vaep_defensive` computation ensures positive = good.

- **Progression** — the offensive component (`vaep_offensive`). How much did the action advance the team's own scoring probability? Positive = more threatening. Computed as `P(scores)_after - P(scores)_before`.

- **Decision Value** — the composite VAEP score (`vaep_value = vaep_offensive + vaep_defensive`). Was the implicit risk worth it? Positive = the action's Progression gain exceeded its Survival cost. Negative = the risk outweighed the benefit.

This is a labeling convention, not a new model. The underlying probabilities and computation are unchanged. The xR model itself (which introduces additional axes like xR-score) is not implemented here.

## EU AI Act — Intended Use and Non-Use

This model is published for **research and reproducibility** purposes on public, open-licensed match data. It is **not intended for, not validated for, and not supplied to** any use that would fall within Annex III §4 (Employment, workers management and access to self-employment) of Regulation (EU) 2024/1689 — including recruitment or selection of natural persons, decisions affecting work-related contractual relationships, promotion, termination, task allocation based on individual traits, or the monitoring and evaluation of performance and behaviour of workers for employment decisions.

Any deployer who wishes to use this model for such a purpose is responsible for performing their own conformity assessment under Article 43, for drawing up the technical documentation required by Article 11 and Annex IV, for implementing the human oversight measures required by Article 14, for declaring accuracy metrics under Article 15, and for ensuring the data governance obligations of Article 10 are met. Note specifically that the training data contains no protected attributes and therefore cannot support the group-fairness audits required by Article 10(2)(g) without ingesting additional personal data.

See the [`AI_GOVERNANCE.md`](https://github.com/karsten-s-nielsen/luxury-lakehouse/blob/main/AI_GOVERNANCE.md) gap analysis in the source repository for the project's full risk classification, re-classification triggers, and governance posture.

## Limitations

- **Open data only**: Trained on publicly available open data (StatsBomb, Wyscout, IDSSE/DFL, Metrica, SkillCorner). Commercial datasets with richer event annotations may yield different VAEP scores.
- **No tracking data**: VAEP is event-based. Off-ball positioning, pressing intensity, and space creation are not captured. See [OBSO](https://github.com/karsten-s-nielsen/luxury-lakehouse) for tracking-based approaches.
- **Competition-agnostic**: The models are trained across all competitions jointly. League-specific models may produce more calibrated probabilities for individual leagues.
- **Cross-source alignment**: The five providers use different event taxonomies. The SPADL adapter normalizes them, but subtle differences in event definitions (e.g., duel classification) remain.
- **No calibration**: Unlike the xG models, the VAEP classifiers do not include post-hoc isotonic calibration. Predicted probabilities may not be perfectly calibrated in absolute terms. For ranking players or actions, this is less consequential; for applications requiring absolute probability values, validate with a reliability diagram.
- **10-action horizon**: VAEP considers only the next 10 actions. Longer-range effects (e.g., a switch of play that leads to a goal 20 actions later) are not captured.

## Model Files

```
vaep_model.json    -- scores + concedes XGBoost boosters (JSON envelope, no pickle)
metrics.json       -- evaluation metrics and training configuration
```

## Citation

If you use this model, please cite the original VAEP paper:

```bibtex
@inproceedings{decroos2019actions,
  title={Actions Speak Louder than Goals: Valuing Player Actions in Soccer},
  author={Decroos, Tom and Bransen, Lotte and Van Haaren, Jan and Davis, Jesse},
  booktitle={Proceedings of the 25th ACM SIGKDD International Conference on Knowledge Discovery and Data Mining},
  pages={1851--1861},
  year={2019},
  publisher={ACM}
}
```

And the silly-kicks library:

```bibtex
@article{silly-kicks,
  title={silly-kicks: A Python library for valuing soccer actions},
  author={Decroos, Tom and Van Haaren, Jan and Davis, Jesse},
  year={2020},
  url={https://github.com/karsten-s-nielsen/silly-kicks}
}
```

```bibtex
@software{nielsen2026vaep,
  title={VAEP Model: Multi-Provider Action Valuation on Open Soccer Data},
  author={Nielsen, Karsten Skyt},
  year={2026},
  url={https://github.com/karsten-s-nielsen/luxury-lakehouse}
}
```

## Companion Resources

| Dataset | Description |
|---------|-------------|
| [SPADL/VAEP Action Values](https://huggingface.co/datasets/luxury-lakehouse/spadl-vaep-action-values) | Pre-computed per-action VAEP valuations (~9.5M actions) |
| [xG Shot Data](https://huggingface.co/datasets/luxury-lakehouse/xg-shot-data) | Tabular shot features for xG training |
| [Player Embeddings](https://huggingface.co/datasets/luxury-lakehouse/football2vec-player-embeddings) | Pre-computed behavioral + statistical vectors |

## Demo

Try the interactive [Soccer Analytics App](https://huggingface.co/spaces/luxury-lakehouse/soccer-analytics-app) — explore player impact rankings powered by VAEP valuations, and compare players across leagues.

> **Explore interactively:** [Soccer Analytics App](https://huggingface.co/spaces/luxury-lakehouse/soccer-analytics-app)

## More Information

- **License**: [CC-BY-NC 4.0](https://creativecommons.org/licenses/by-nc/4.0/) (inherited from Wyscout training data)
- **Training script**: `scripts/train_vaep_model_hf.py` (PEP 723 standalone)
- **Source module**: `src/ingestion/spadl_vaep.py`
- **Platform**: [Luxury Lakehouse Soccer Analytics](https://github.com/karsten-s-nielsen/luxury-lakehouse)