Understat xG League & Team Stats Scraper
Pricing
from $0.90 / 1,000 record scrapeds
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
Maintained by CommunityActor stats
0
Bookmarked
9
Total users
0
Monthly active users
4 days ago
Last modified
Categories
Features
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
| Field | Type | Default | Description |
|---|---|---|---|
leagues | array | (all 6) | Which leagues to scrape. Valid values: EPL, La_liga, Bundesliga, Serie_A, Ligue_1, RFPL |
seasons | array | ["2024"] | Season start years to scrape. E.g. ["2024"] = 2024/2025, ["2023","2024"] = two seasons |
outputType | string | teamSeason | teamSeason = one row per team per season. shots = one row per shot (see Shot-level mode) |
matchIds | array | (none) | Shots mode only: specific Understat match IDs (from understat.com/match/<id>). When set, leagues/seasons are ignored |
maxItems | integer | 10 | Maximum 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
| Field | Description |
|---|---|
season | Season label (e.g. 2024/2025) |
league | League name |
team_name | Team name as shown on understat.com |
team_id | Understat internal team ID |
games_played | Total matches played |
wins / draws / losses | Season record |
goals_for / goals_against | Goals scored / conceded |
xg_for | Total expected goals (xG) |
xg_against | Total expected goals against (xGA) |
npxg | Non-penalty xG (excludes spot-kick quality) |
npxg_against | Non-penalty xG conceded |
npxg_diff | Non-penalty xG differential (npxg minus npxg_against) |
deep | Deep completions — passes into the opposition's danger zone |
ppda | Passes per defensive action by opponent (lower = team presses more aggressively) |
oppda | Opponent PPDA — how much the team is pressed |
points | Total league points |
xpoints | Expected points — understat's xG-based points model |
xg_diff | Goals scored minus expected goals (finishing over/underperformance) |
source | Source 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"}
| Field | Description |
|---|---|
X / Y | Shot position as 0-1 fractions of the pitch (X toward the goal being attacked, Y across the width) |
xG | Expected-goals value of the shot (0-1) |
minute | Match minute |
situation | Understat's own label: OpenPlay, FromCorner, SetPiece, DirectFreekick, Penalty. Understat has no counter-attack label, so counter-attack shots cannot be isolated |
result | Goal, SavedShot, MissedShots, BlockedShot, ShotOnPost, OwnGoal |
shot_type / last_action | Body part and the preceding action as Understat records them |
team / opponent / home_team / away_team / home_goals / away_goals | Match context |
player / player_id / player_assisted | Shooter 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
nullif 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.
maxItemscounts across all requested league × season combinations. Set to0to scrape everything.