An AI football scouting platform — the models do the maths, the LLM explains them.
Quick start · What it does · Architecture · Roadmap · Contributing
FootyVision helps sporting directors, scouts and analysts answer questions like "who are the best stylistic replacements for this player?", "which players fit this role?", "is this signing good value?" and "find me a ball-winning midfielder".
Guiding principle: traditional ML models do the maths; the LLM explains and interprets (scouting reports, comparisons, natural-language search, a RAG assistant) — it never invents numbers.
Built end-to-end: StatsBomb event ETL → Postgres → similarity / scoring / value ML → local LLM + embeddings → FastAPI + a web dashboard. Runs fully locally, with no paid APIs.
| Capability | How it works | Try it |
|---|---|---|
| Similarity engine | Per-90 features, z-scored within position group, cosine similarity | GET /players/{id}/similar |
| Scouting radars | Percentile-vs-peers on each metric | GET /players/{id}/radar |
| Performance Score | Transparent position-weighted percentile composite (0–100) | GET /players/{id}/score, GET /rankings |
| Role classifier | XGBoost predicts position from style (~85%), SHAP explains, flags role-mismatches | footyvision talent-report |
| LLM scouting reports | Report grounded in computed stats; the model can't invent numbers | POST /players/{id}/report |
| Natural-language search | LLM → validated Pydantic PlayerQuery (never raw SQL) → safe query |
POST /search |
| Market value model | LightGBM + age + SHAP on real Transfermarkt values | footyvision value-report |
| RAG assistant | Player profiles embedded locally → retrieve + grounded answer, cites names | POST /assistant |
Example — "La Liga forwards with xG per 90 over 0.5" → Ronaldo, Benzema, Suárez, Messi. "médio defensivo que ganhe bolas e intercete" → retrieves Busquets/Camacho and recommends Camacho, citing his real intercepting/tackling profile.
StatsBomb Open Data ──► ETL (extract ▸ aggregate ▸ load) ──► PostgreSQL
│
┌──────────────┬──────────────────┬─────────────────────┼──────────────────┐
▼ ▼ ▼ ▼ ▼
Similarity Performance Score XGBoost role clf Value predictor RAG vector
(z + cosine) (weighted pctl) + SHAP (LightGBM + SHAP) store (embeds)
└──────────────┴────────┬─────────┴──────────────────────┴──────────────────┘
▼
Local LLM (LM Studio / Ollama — chat + embeddings)
reports ▸ NL → safe query ▸ conversational RAG assistant
▼
FastAPI ──► web dashboard (radar / search / assistant)
Layer-by-layer detail and the reasoning behind each design decision:
docs/ARCHITECTURE.md.
- Backend: FastAPI · SQLAlchemy 2 · Alembic · PostgreSQL
- Data/ML: pandas · scikit-learn · XGBoost · LightGBM · SHAP · rapidfuzz
- LLM (local): OpenAI-compatible endpoint — chat (Gemma/Qwen/Llama) + embeddings (
EmbeddingGemma-300M) - Data sources: StatsBomb Open Data (
statsbombpy) · Transfermarkt values (Kaggle) · FBref/SoFIFA (soccerdata) - Frontend: Next.js dashboard (
frontend/web) · self-contained Plotly radar demo (frontend/radar_demo.html) - Infra: Docker Compose · GitHub Actions
Prerequisites: Docker (for Postgres), Python 3.11+, and a local LLM server (LM Studio or Ollama) for the LLM/RAG features.
# 1. Configuration
cp .env.example .env # the defaults work out of the box
# 2. Postgres
docker compose up -d db
# 3. Python env + package
python -m venv .venv
.venv\Scripts\Activate.ps1 # Windows; macOS/Linux: source .venv/bin/activate
pip install -e ".[dev]"
# 4. Schema + data (StatsBomb Open Data)
footyvision init-db
footyvision competitions # list available competitions
footyvision load -c 11 -s 27 # La Liga 2015/16 (the one full season; ~380 matches)
# 5. API
uvicorn footyvision.api.main:app --reload # -> http://localhost:8000/docsLLM features (reports, NL search, assistant): start LM Studio, load a chat model + the
embeddinggemma-300m embedding model, Start Server (port 1234), set LLM_MODEL in .env, then:
footyvision index # embed player profiles into the RAG vector storeValue model (optional): needs Kaggle access to davidcariboo/player-scores; drop
players.csv + player_valuations.csv into data/, then footyvision value-report.
Everything at once:
- Docker:
docker compose up --build(the API reaches the host LLM viahost.docker.internal). - Windows:
.\scripts\start.ps1boots Postgres, LM Studio, the API and the dashboard. - Dashboard only:
cd frontend/web && npm install && npm run dev.
/health · /llm/health · /players · /players/{id} · /players/{id}/seasons ·
/players/{id}/similar · /players/{id}/radar · /players/{id}/score · /rankings ·
/talent/model-info · POST /players/{id}/report · /players/{id}/report/context ·
POST /search · POST /search/structured · POST /assistant.
Interactive OpenAPI docs at /docs.
src/footyvision/ # the Python package
config.py # env-driven settings
db/ # base.py, models.py — SQLAlchemy schema
etl/ # statsbomb, aggregate, load, transfermarkt, sofifa
ml/ # features, similarity, scoring, talent, value
llm/ # client.py (chat + embeddings), scouting.py
search/ # query.py (safe PlayerQuery), nl.py (NL → query)
rag/ # profiles, store, assistant, service
api/ # main.py, routers/, schemas.py
cli.py # init-db · load · aggregate · talent-report · value-report · index
frontend/ # radar_demo.html · web/ (Next.js dashboard)
migrations/ # Alembic
scripts/ # start.ps1 · eval_embeddings.py (retrieval benchmark)
tests/ # 52 tests (unit + API), DB/network/LLM-free
docs/ # ARCHITECTURE.md, ROADMAP.md
This project deliberately reports what public/free data can't do, not just what it can:
- StatsBomb Open Data has no Portuguese league and only one complete domestic season (La Liga 2015/16) — so the pool is that season plus a partial Bundesliga.
- FBref only exposes advanced stats (xG, progression) for the Big-5 leagues, so a rich Primeira Liga engine isn't feasible from free sources.
- The embedding model was chosen by measurement, not by leaderboard
(
scripts/eval_embeddings.pyscores retrieval on these 411 profiles). The original setup — nomic-embed-text with no task prefixes — retrieved the right position for only 30% of Portuguese queries against 90% of English ones. EmbeddingGemma-300M with its proper prefixes reaches 73% / 97%. A cross-lingual gap remains, and the script reports it rather than hiding it. - The value model is trained on real Transfermarkt values but scores a low held-out R² (≈0.05): one season of public per-90 stats plus age barely predicts market value (SHAP correctly ranks age #1). Cross-source name matching (Spanish multi-surnames ↔ short TM names) adds label noise. A truthful evaluation beats an inflated one.
pytest # 52 tests, no Postgres/network/LLM required
ruff check src tests
ruff format --check src testsCI runs the same checks on Python 3.11 and 3.12, plus a production build of the dashboard. See CONTRIBUTING.md for the ground rules (tests stay offline, the LLM never invents numbers, no raw SQL from user input).
MIT © Ana Pinto.
Football data from StatsBomb Open Data, used under their terms; market values from the public Transfermarkt dataset on Kaggle. This project is not affiliated with StatsBomb or Transfermarkt.