Data from FBref, Understat, and API-Football. Source on GitHub · API docs · RSS

← Back to fixtures

Data Dictionary

Every table that backs this site, where its data comes from, and its known quirks — the gaps and inconsistencies real data always has, stated plainly rather than smoothed over.

leagues

One row per competition. sport distinguishes soccer from the planned NFL/NBA expansion.

codetext

'EPL', 'SERIE_A', 'MLS', 'LA_LIGA', 'WC'

is_internationalboolean

true only for World Cup

sporttext

'soccer' | 'basketball' | 'football' — every league is 'soccer' today; the other two exist for the planned NFL/NBA build

seasons

Per-league season. Label format is NOT uniform across leagues — a real bug (fixed) once made these disagree between ingestion paths for the same real season.

labeltext

Hyphenated 'YYYY-YY' for cross-year leagues (EPL, SERIE_A, LA_LIGA — e.g. '2026-27'); bare 'YYYY' for single-calendar-year competitions (MLS, WC — e.g. '2026'). Both ingestion paths (loader.py, api_football.py) must agree on this per league or a season silently duplicates.

teams / players

Shared entity tables across every league and (eventually) every sport. External ids exist purely for de-duplicating the same real-world team/player across three independent data sources.

fbref_id / understat_id / api_football_idtext/text/int

Nullable — a team only has an id for the sources that actually cover it. Name-based aliasing (src/ingestion/entities.py) resolves the same team across sources when ids don't overlap.

players.positiontext

'GK'/'DF'/'MF'/'FW' — free text from the source, not an enum

matches

One row per fixture, shared across all leagues/sports. home_score/away_score were renamed from home_goals/away_goals (generalizing off soccer terminology) — check you're on a build past that rename.

statustext

'scheduled' | 'live' | 'final' | 'postponed'

home_score / away_scoreint

NULL until final

went_to_otboolean

Renamed from went_to_et — extra time/overtime, applies to any sport

went_to_pensboolean

Soccer/knockout-tournament specific (penalty shootout) — deliberately NOT generalized, NFL/NBA have no equivalent and this column will simply stay false for them

external_reftext

e.g. 'api-football:12345', 'understat:67890' — the source event id, prefixed by source

team_match_stats / player_match_stats

Soccer-specific box-score stats, one row per team/player per match. Deliberately NOT shared with other sports (see team_match_stats_nfl below) — per-sport tables chosen over a shared core + JSONB column for full type safety and easy indexing.

xg / xanumeric

Understat only — API-Football's tier doesn't include xG, so these are NULL for matches sourced only from API-Football (e.g. current-season EPL/SERIE_A fixtures pulled via the --primary stopgap path before FBref/Understat catch up)

ppdanumeric

Pressing intensity — Understat only

corners, shots, shots_on_target, possession_pct, fouls, yellows, reds, saves, deep_completionsvarious

FBref/API-Football; coverage varies by league — see the World Cup exception below

team_match_stats_nfl / player_match_stats_nfl

Empty as of this writing — schema prep only, ahead of the actual NFL ingestion adapter. Column list is a best-effort standard box-score set matching nflverse/nfl_data_py's shape; expect adjustment once real data starts flowing.

team_match_stats_nfltable

total/passing/rushing yards, turnovers, sacks, penalties, first downs, third-down conversions, time of possession

player_match_stats_nfltable

position, passing/rushing/receiving stat lines, defensive tackles/sacks (sacks is NUMERIC(3,1) — half-sacks are real)

shots

Event-level shot data (Understat only) — coordinates, situation, body part, Understat's own xG as a benchmark. Feeds a planned custom xG model (v3); also the reason MLS/API-Football-only leagues have no shot-level detail.

x, ynumeric

Understat's normalized pitch coordinates

source_xgnumeric

Understat's xG for this shot — the benchmark a future in-house model would be compared against

model_versions

One row per distinct (model, config), not per prediction run. This was broken for a while — version_tag used to be unique per fixture, which meant every slate-generation call minted a new throwaway row and the registry never actually deduplicated anything. Fixed: version_tag is now stable per league+code-version.

paramsjsonb

Real hyperparameters + feature lists as of the fix, not free-text notes

train_metricsjsonb

Real out-of-fold error metrics (e.g. MAE) where computed inline; not always populated for every model

predictions

The immutable ledger. INSERT-only (a trigger rejects UPDATE/DELETE outright); another trigger rejects any row locked at or after its match's kickoff.

markettext

CHECK-constrained: '1X2','BTTS','TOTAL_GOALS','CORNERS','SOT','PLAYER_GOALS','PLAYER_SAVES' (soccer, live) plus 'MONEYLINE','SPREAD','TOTAL_POINTS' (NFL/NBA, schema prep only — nothing writes these yet)

sidetext

Semantics depend on market: 'home'/'draw'/'away' for 1X2, 'yes'/'no' for BTTS, 'over'/'under' for line markets, 'home'/'away' for SPREAD/MONEYLINE (which team the prediction concerns)

linenumeric

NULL where not applicable (1X2, BTTS, MONEYLINE); the threshold for O/U markets; the signed spread for SPREAD

probabilitynumeric

CHECK-constrained to the open interval (0,1) — never exactly 0 or 1

prediction_grades

Separate, append-only. The prediction row itself is never touched when grading happens.

outcometext

'hit' | 'miss' | 'void' — void covers pushes (line lands exactly on the actual value) and postponed/abandoned matches

actual_valuenumeric

The observed number graded against — a signed margin for SPREAD, a count for line markets, goal difference for 1X2

ingest_review

Queue for entity-resolution ambiguities a human should check (e.g. a possible-duplicate player). Empty as of this writing — surfaced by scripts/review_queue.py, not shown anywhere in the UI.

pipeline_runs

Backing store for a future data-freshness indicator. The table exists but nothing writes to it yet — auto_slate.py/auto_grade.py/the ingestion scripts still need a small wrapper to record start/finish/status. Known gap, tracked in Todoist.

How it works →