Understat xG League & Team Stats Scraper avatar

Understat xG League & Team Stats Scraper

Pricing

from $0.90 / 1,000 record scrapeds

Go to Apify Store
Understat xG League & Team Stats Scraper

Understat xG League & Team Stats Scraper

Scrape expected goals (xG), xGA, NPxG, PPDA and full standings for all 6 understat.com leagues across any available season. One row per team per season, aggregated from match-level history.

Pricing

from $0.90 / 1,000 record scrapeds

Rating

0.0

(0)

Developer

BowTiedRaccoon

BowTiedRaccoon

Maintained by Community

Actor stats

0

Bookmarked

9

Total users

0

Monthly active users

4 days ago

Last modified

Features

Limited permissions

Share

Scrape expected goals (xG), non-penalty xG (NPxG), xGA, PPDA, deep completions, and full W/D/L standings from understat.com — the canonical open xG data source for European football.

Covers all 6 understat leagues (EPL, La Liga, Bundesliga, Serie A, Ligue 1, RFPL) across every available season (2014–present). One aggregated row per team per season.

Features

  • 6 leagues in a single run: EPL, La Liga, Bundesliga, Serie A, Ligue 1, RFPL
  • All seasons from 2014/2015 onwards — back-fill historical data or track the current season
  • Match-level aggregation — sums each team's per-match xG, xGA, NPxG, PPDA, deep completions into a clean season total
  • PPDA ratio computed correctly as passes_attempted / defensive_actions (lower = more pressing)

Use Cases

  • Build football analytics dashboards (xG vs actual goals by team)
  • Feed betting or fantasy football models with expected-goals standings
  • Compare xG performance across leagues or seasons for research / Substack writing
  • Backtest xG-based table predictions

Input

FieldTypeDefaultDescription
leaguesarray(all 6)Which leagues to scrape. Valid values: EPL, La_liga, Bundesliga, Serie_A, Ligue_1, RFPL
seasonsarray["2024"]Season start years to scrape. E.g. ["2024"] = 2024/2025, ["2023","2024"] = two seasons
outputTypestringteamSeasonteamSeason = one row per team per season. shots = one row per shot (see Shot-level mode)
matchIdsarray(none)Shots mode only: specific Understat match IDs (from understat.com/match/<id>). When set, leagues/seasons are ignored
maxItemsinteger10Maximum records to return, team rows or shots (0 = unlimited)

Example input — EPL last 3 seasons

{
"leagues": ["EPL"],
"seasons": ["2022", "2023", "2024"],
"maxItems": 0
}

Example input — All leagues, current season

{
"leagues": ["EPL", "La_liga", "Bundesliga", "Serie_A", "Ligue_1", "RFPL"],
"seasons": ["2024"],
"maxItems": 0
}

Output

One record per team per season:

{
"season": "2024/2025",
"league": "EPL",
"team_name": "Aston Villa",
"team_id": "71",
"games_played": 38,
"wins": 19,
"draws": 9,
"losses": 10,
"goals_for": 58,
"goals_against": 51,
"xg_for": 66.39,
"xg_against": 55.0,
"npxg": 61.71,
"npxg_against": 52.4,
"npxg_diff": 9.31,
"deep": 285,
"ppda": 12.39,
"oppda": 9.96,
"points": 66,
"xpoints": 63.2,
"xg_diff": -8.39,
"source": "https://understat.com/league/EPL"
}

Field reference

FieldDescription
seasonSeason label (e.g. 2024/2025)
leagueLeague name
team_nameTeam name as shown on understat.com
team_idUnderstat internal team ID
games_playedTotal matches played
wins / draws / lossesSeason record
goals_for / goals_againstGoals scored / conceded
xg_forTotal expected goals (xG)
xg_againstTotal expected goals against (xGA)
npxgNon-penalty xG (excludes spot-kick quality)
npxg_againstNon-penalty xG conceded
npxg_diffNon-penalty xG differential (npxg minus npxg_against)
deepDeep completions — passes into the opposition's danger zone
ppdaPasses per defensive action by opponent (lower = team presses more aggressively)
oppdaOpponent PPDA — how much the team is pressed
pointsTotal league points
xpointsExpected points — understat's xG-based points model
xg_diffGoals scored minus expected goals (finishing over/underperformance)
sourceSource URL

Shot-level mode

Set outputType to shots to get one row per shot for every finished match in the chosen leagues and seasons (or for the matches in matchIds). Matches are fetched concurrently, and the run stops fetching once maxItems shots are collected. A full league-season is thousands of shots across hundreds of matches, so set maxItems deliberately; shots mode makes one request per match and takes much longer than team totals.

{
"outputType": "shots",
"leagues": ["EPL"],
"seasons": ["2024"],
"maxItems": 500
}

Each row (numeric fields are real numbers, not strings):

{
"shot_id": "584630",
"match_id": "26602",
"league": "EPL",
"season": "2024/2025",
"date": "2024-08-16 19:00:00",
"minute": 19,
"team": "Manchester United",
"opponent": "Fulham",
"home_away": "h",
"home_team": "Manchester United",
"away_team": "Fulham",
"home_goals": 1,
"away_goals": 0,
"player": "Bruno Fernandes",
"player_id": "1228",
"player_assisted": "Lisandro Martínez",
"X": 0.971,
"Y": 0.673,
"xG": 0.0501,
"result": "MissedShots",
"situation": "OpenPlay",
"shot_type": "LeftFoot",
"last_action": "Chipped",
"source": "https://understat.com/match/26602"
}
FieldDescription
X / YShot position as 0-1 fractions of the pitch (X toward the goal being attacked, Y across the width)
xGExpected-goals value of the shot (0-1)
minuteMatch minute
situationUnderstat's own label: OpenPlay, FromCorner, SetPiece, DirectFreekick, Penalty. Understat has no counter-attack label, so counter-attack shots cannot be isolated
resultGoal, SavedShot, MissedShots, BlockedShot, ShotOnPost, OwnGoal
shot_type / last_actionBody part and the preceding action as Understat records them
team / opponent / home_team / away_team / home_goals / away_goalsMatch context
player / player_id / player_assistedShooter and assister

With matchIds, league is null because the match endpoint does not state it; season is still filled from the shot.

Notes

  • Data is sourced from understat.com's internal XHR API (/getLeagueData/{league}/{season}), which returns gzip-compressed JSON. All values are aggregated from per-match history.
  • PPDA and OPPDA are null if a team played 0 defensive actions (extremely rare, data gap seasons only).
  • The RFPL (Russian Premier League) data coverage may be incomplete for recent seasons.
  • maxItems counts across all requested league × season combinations. Set to 0 to scrape everything.