Personal project · 2026
Goalkeeper Scouting
A full-stack scouting platform for goalkeepers, built from raw match-event data. Every keeper is compared only with peers from the same competition and season, and the sample size is always shown next to the number.
The demo runs on Render's free tier, so the first request may take a few seconds while the service wakes up.
Problem
General football analytics tools tend to reduce goalkeepers to a handful of aggregated numbers with no context. A scout needs more than that: who the keeper is being compared with, over how many minutes, and how many actions sit behind each percentage.
Context and motivation
I wanted a project that follows a scout's actual workflow — find candidates, look at a keeper in context, benchmark against peers, find similar profiles, shortlist, and hand over a report — and that is honest about what the data cannot say. There is deliberately no single overall goalkeeper score.
What I built
Built end to end on my own: the ingestion pipeline for StatsBomb events, the data model and migrations, the metric and minutes calculations, the benchmarking and similarity engines, the REST API, the React interface, the tests and the deployment.
From events to report
01 Sources
- StatsBomb Open Data — raw events
- FBref 2024/25 — one-off manual export, kept separate
- Transfermarkt — age, club, value (kept apart from performance)
02 Ingestion
- statsbombpy + pandas
- Minutes from line-ups, substitutions and red cards
- Metrics per source; missing stays NULL
03 Storage
- PostgreSQL with SQLAlchemy
- Alembic migrations
- Demo: SQLite snapshot
04 Analysis
- Percentiles per competition and season (≥ 450 min)
- Similarity with robust z-scores
- Scouting profiles: matched / unmet / insufficient
05 Service
- FastAPI REST API
- React + TypeScript (Vite)
- One Docker container on Render
Technical decisions
5Compare only with real peers
The benchmark ranks a keeper only against others from the same competition and season, with at least 450 minutes by default, using percentiles with tie handling. Comparing Ligue 1 2015/16 with the 2022 World Cup would look precise and mean nothing.
Missing means missing, not zero
A metric with no underlying events is shown as "N/A — no recorded actions", never as 0. The FBref 2024/25 slice only covers shot stopping, so sweeping and distribution stay NULL instead of being estimated.
Minutes computed from events
Minutes come from each match's starting line-ups, substitutions and red cards, including extra time — not from an estimate. The tests use a small hand-built event set where every expected value can be checked by hand.
Sources kept apart
FBref rows have their own competitions, coverage records and peer groups and are never mixed with StatsBomb rows in the same comparison. The same metric name can have different definitions in each source, and the interface says so.
Scouting profiles instead of a rating
A profile (for example a high-line sweeper or a possession goalkeeper) is a set of weighted preferences with minimums and maximums. Each criterion returns matched, unmet or insufficient data; missing data never counts as a poor match.
What it does
- Discover: search by name or filter by competition, season, age, market value, minutes and performance
- Goalkeeper profile in one competition/season context, with plain-language takeaways
- Percentile benchmark against peers from the same competition and season
- Similar goalkeepers with adjustable weights (robust z-scores)
- Scouting profiles with an explainable match indicator
- Shortlist with priority, status and scout notes
- Print-ready scouting report
- Data coverage page per competition and season
Evidence
what was checked, and how| What | Result | Source | Checked |
|---|---|---|---|
| Automated tests | 288 passed, 1 skipped | pytest run locally against a synthetic event set (289 tests collected) | |
| Goalkeepers | 480 players · 619 performance rows | Dataset table in the README (410 StatsBomb + 209 FBref) | |
| Contexts | 18 competition/seasons, 9 with strong coverage | Data Coverage page and README | |
| Demo | Online (HTTP 200) | Request to the public demo |
Screenshots
4Limitations and next steps
Known limitations
- StatsBomb Open Data is historical: there is no current-season league coverage. The 2024/25 slice comes from a one-off manual FBref export and covers shot stopping only.
- Player identity uses name + competition + season. At least 20 players are spelled differently by StatsBomb and FBref and are not yet recognised as the same person.
- Custom scouting profiles and the shortlist live in the browser; there are no accounts or sync.
Next steps
- A stable player_id identity model (which also fixes the cross-source name mismatch)
- Custom profiles used in server-side matching
- More historical StatsBomb seasons



