Understat adapter

Understat: The best narrow xG path.

Understat is a scraped dataset with per-shot xG, body part, and situation — ideal for xG-driven charts when you don't have a paid feed. The adapter handles their top-left origin, maps their result strings to the Campos outcome vocabulary, and drops own-goals the way the shot products expect.

Input shape
Schedule rows for matchSummary; shot rows for shots. Both are plain JS objects.
Status
Narrow (scrape-backed)
Raw feed
Understat
Provider-shaped. Their coordinates, their qualifier codes, their outcome strings.
Messy
Campos adapter
fromUnderstat.shots(shotRows)
Flips direction, standardises outcomes, filters own-goals and shootouts, resolves player labels.
One call
Typed data
ShotEvent[]
Drop into <ShotMap />, your own React view, a server-side report, or an analysis script.
Ready
Why this adapter matters

Understat is the only free public source with per-shot xG that Campos currently has an adapter for. If you don't have an Opta or StatsBomb licence, this is the path for xG-driven charts.

Get it running

One import, one call per surface.

import { fromUnderstat } from "@withqwerty/campos-adapters";

const summary = fromUnderstat.matchSummary(scheduleRow);
const shots = fromUnderstat.shots(shotRows);
What this adapter ships

The calls you can make today.

Shipped
shots() returns ShotEvent[]

Just the shots, ready to plot.

Understat notes: Includes xG, body part, and situation.

Shipped
matchSummary() returns MatchSummary

Scoreline, status, and team-level xG.

Understat notes: Scoreline, team xG, competition, kickoff.

What you can build with this

Charts that drop straight onto this adapter's output.

Supported cards plot from this adapter with no extra work. Partial cards plot, but read the scope note. Dimmed cards need a surface this provider doesn't ship.

Two entry points

Schedule rows in, shot rows in.

matchSummary() takes a schedule row and returns a clean MatchSummary with competition, kickoff, scoreline, and team xG. shots() takes an array of shot rows and returns ShotEvent[] with canonical coordinates (Understat stores y top-to-bottom from the broadcast camera — the adapter inverts to attacker-rightward), outcomes mapped to the Campos vocabulary, and own-goals filtered out.

Current scope

What's honest about this adapter today.

What it's great for

xG charts without a paid feed.

ShotMap with xG-weighted markers. XGTimeline for match xG accumulation. Team xG in match-header cards. Everything that needs per-shot or team-level xG and doesn't need the wider event stream.

What it doesn't give you

No passes, no events, no lineups.

Understat is shot-and-summary only. For pass maps, formations, or team sheets, reach for Opta, StatsBomb, or WhoScored instead.

How to get the data

Scrape it or use a Python client.

The adapter takes plain objects — it doesn't care how you got them. Most users pull from soccerdata's Python scraper and serialise to JSON, or hit Understat directly. The row shape is stable; see the raw example below.

Lineage

What this adapter borrows from whom.

Concrete credits for the projects that did the underlying research or laid the reference tables we read from.

soccerdata Python

Scrape-backed narrow-adapter pattern; the schemas we read for Understat and FBref schedule and shot rows.

Raw example — what the payload looks like before and after
Raw schedule row
{
  "game_id": 26761,
  "league": "Premier League",
  "season": "2024",
  "date": "2024-08-17T14:00:00Z",
  "home_team": "Liverpool",
  "away_team": "Bournemouth",
  "home_goals": 2,
  "away_goals": 0,
  "home_xg": 1.84,
  "away_xg": 0.91,
  "is_result": true
}
Raw shot row
{
  "shot_id": 548921,
  "game_id": 26761,
  "team_id": 87,
  "team": "Liverpool",
  "player_id": 1250,
  "player": "Diogo Jota",
  "xg": 0.12,
  "location_x": 0.82,
  "location_y": 0.54,
  "minute": 23,
  "body_part": "left foot",
  "situation": "open play",
  "result": "Saved Shot"
}
Canonical shot output
{
  "kind": "shot",
  "matchId": "26761",
  "playerName": "Diogo Jota",
  "minute": 23,
  "x": 82,
  "y": 46,
  "xg": 0.12,
  "outcome": "saved",
  "bodyPart": "left-foot",
  "context": "regular-play",
  "provider": "understat"
}
Compare providers