← All projects

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

  1. 01 Sources

    • StatsBomb Open Data — raw events
    • FBref 2024/25 — one-off manual export, kept separate
    • Transfermarkt — age, club, value (kept apart from performance)
  2. 02 Ingestion

    • statsbombpy + pandas
    • Minutes from line-ups, substitutions and red cards
    • Metrics per source; missing stays NULL
  3. 03 Storage

    • PostgreSQL with SQLAlchemy
    • Alembic migrations
    • Demo: SQLite snapshot
  4. 04 Analysis

    • Percentiles per competition and season (≥ 450 min)
    • Similarity with robust z-scores
    • Scouting profiles: matched / unmet / insufficient
  5. 05 Service

    • FastAPI REST API
    • React + TypeScript (Vite)
    • One Docker container on Render
How data flows through Goalkeeper Scouting, from sources to interface.

Technical decisions

5

Compare 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
WhatResultSourceChecked
Automated tests288 passed, 1 skippedpytest run locally against a synthetic event set (289 tests collected)
Goalkeepers480 players · 619 performance rowsDataset table in the README (410 StatsBomb + 209 FBref)
Contexts18 competition/seasons, 9 with strong coverageData Coverage page and README
DemoOnline (HTTP 200)Request to the public demo

Screenshots

4
Goalkeeper profile page with a performance snapshot, takeaways and a percentile benchmark.
A goalkeeper profile in one competition/season context.
List of similar goalkeepers with weight controls for shot stopping, distribution and proactivity.
Similarity with adjustable weights, labelled as model output.
Search results with match indicators against a scouting profile.
Discover filtered by a scouting profile.
Table of data coverage by competition and season, with the source of each context.
Data coverage: how many keepers exist and how many have a large enough sample.

Limitations 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