๐๐ International basketball with sportsdataverse-py
This page is the executed notebook
16_international_basketball_intro.ipynb: download it to run it yourself.
The NBA is one league; the rest of the basketball world is dozens. sportsdataverse-py wraps the
feeds behind the biggest of them, and this notebook tours four:
| League | Module | Provider |
|---|---|---|
| EuroLeague | sportsdataverse.euroleague | EuroLeague Competition Engine |
| Super League Basketball (UK) | sportsdataverse.basketball.genius | Genius Sports LiveStats |
| NBL (Australia) | sportsdataverse.basketball.nbl_au | NBL Rosetta |
| Liga Endesa (Spain) | sportsdataverse.basketball.acb | ACB |
No API keys, polars by default (return_as_pandas=True for pandas), and at the end a EuroLeague
shot chart drawn on a FIBA court with sdvplot. ๐ฏ
๐งฐ The toolboxโ
| Function | What it gives you |
|---|---|
euroleague_seasons | Seasons of a competition (E = EuroLeague, U = EuroCup) |
euroleague_games | Every game of a season, played or not |
euroleague_standings | Standings after a round |
euroleague_game_points | Shot chart of one game (coord_x / coord_y in cm) |
slb_competitions | SLB competitions with their END-year season |
slb_schedule | Every match of an SLB season |
slb_player_box | Player box score of a finished SLB game |
slb_shots | Shot chart of a finished SLB game |
nbl_rosetta_standings | NBL regular-season ladder |
nbl_rosetta_matches | Matches of an NBL season |
acb_standings | Liga Endesa table after a round |
acb_calendar | Every round of a season with its games |
Genius LiveStats serves many leagues (wbbl_*, bal_*, nznbl_*, esake_*, lfb_*, ...) with the
same shape, so the SLB recipes below work for every one of them.
๐ Setupโ
pip install "sportsdataverse[sdvplot]"
The sdvplot extra brings in sdvplot and sportypy for the court drawing.
import matplotlib.pyplot as plt
import polars as pl
from sportsdataverse import euroleague as el
from sportsdataverse.basketball import acb, genius, nbl_au
pl.Config.set_tbl_rows(10)
polars.config.Config
Provider feeds are seasonal and occasionally rate-limited, so a tiny safe() helper runs each call
defensively: you get the frame when the feed is up, and a one-line skip when it isn't, naming whether
the answer was nothing there (NoDataError) or the fetch failed (AssetFetchError). ๐
from sportsdataverse.errors import AssetFetchError, NoDataError
def safe(label, thunk):
try:
out = thunk()
print(f"โ
{label}")
return out
except (NoDataError, AssetFetchError) as e:
print(f"\u23ed\ufe0f {label}: {type(e).__name__}: {e}")
return None
๐ช๐บ EuroLeagueโ
Season codes are the competition letter plus the START year (E2025 is 2025-26). We take the newest
season that has a played game, so the notebook works in the off-season too.
seasons = safe("EuroLeague seasons", lambda: el.euroleague_seasons("E"))
season_code, games = None, None
if seasons is not None:
for code in seasons.sort("year", descending=True)["code"].head(3):
g = safe(f"games {code}", lambda code=code: el.euroleague_games("E", code))
if g is not None and g.filter(pl.col("played") == True).height:
season_code, games = code, g
break
print("season:", season_code, "| games:", None if games is None else games.shape)
โ
EuroLeague seasons
โ
games E2026
season: E2026 | games: (380, 94)
played = games.filter(pl.col("played") == True).sort("utc_date") if games is not None else None
cols = ["game_code", "round", "utc_date", "local_club_name", "road_club_name"]
(played.select([c for c in cols if c in played.columns]).tail(5)
if played is not None and played.height else "no played games")
shape: (5, 5)
โโโโโโโโโโโโโฌโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ game_code โ round โ utc_date โ local_club_name โ road_club_name โ
โ --- โ --- โ --- โ --- โ --- โ
โ str โ i64 โ str โ str โ str โ
โโโโโโโโโโโโโชโโโโโโโโชโโโโโโโโโโโโโโโโโโโโโโโชโโโโโโโโโโโโโโโโโโโโโโโชโโโโโโโโโโโโโโโโโโโโโโโโโโโโโก
โ 63 โ 7 โ 2026-10-22T18:30:00Z โ Armani Olimpia Milan โ FC Barcelona โ
โ 66 โ 7 โ 2026-10-22T18:45:00Z โ Partizan Mozzart Bet โ Crvena Zvezda Meridianbet โ
โ โ โ โ Belgrade โ Belgโฆ โ
โ 68 โ 7 โ 2026-10-23T17:00:00Z โ Zalgiris Kaunas โ LDLC ASVEL Villeurbanne โ
โ 70 โ 7 โ 2026-10-23T18:15:00Z โ Olympiacos Piraeus โ Fenerbahce Tarfin Istanbul โ
โ 69 โ 7 โ 2026-10-23T18:45:00Z โ Paris Basketball โ Valencia Basket โ
โโโโโโโโโโโโโดโโโโโโโโดโโโโโโโโโโโโโโโโโโโโโโโดโโโโโโโโโโโโโโโโโโโโโโโดโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Standings after the latest round ๐โ
euroleague_standings takes the round as a string.
last_round = str(played["round"].max()) if played is not None and played.height else None
stand = safe(f"standings round {last_round}",
lambda: el.euroleague_standings("E", season_code, last_round)) if last_round else None
cols = ["position", "club_name", "games_played", "games_won", "games_lost", "points_difference"]
(stand.select([c for c in cols if c in stand.columns]).head(10)
if stand is not None and stand.height else "standings unavailable")
โ
standings round 7
shape: (10, 6)
โโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโฌโโโโโโโโโโโโฌโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโ
โ position โ club_name โ games_played โ games_won โ games_lost โ points_difference โ
โ --- โ --- โ --- โ --- โ --- โ --- โ
โ i64 โ str โ i64 โ i64 โ i64 โ str โ
โโโโโโโโโโโโชโโโโโโโโโโโโโโโโโโโโโโโโโโโโชโโโโโโโโโโโโโโโชโโโโโโโโโโโโชโโโโโโโโโโโโโชโโโโโโโโโโโโโโโโโโโโก
โ 1 โ Panathinaikos AKTOR โ 7 โ 6 โ 1 โ +96 โ
โ โ Athens โ โ โ โ โ
โ 2 โ Fenerbahce Tarfin โ 7 โ 6 โ 1 โ +57 โ
โ โ Istanbul โ โ โ โ โ
โ 3 โ Valencia Basket โ 7 โ 5 โ 2 โ +57 โ
โ 4 โ Olympiacos Piraeus โ 7 โ 5 โ 2 โ +37 โ
โ 5 โ Hapoel IBI Tel Aviv โ 7 โ 5 โ 2 โ +29 โ
โ 6 โ Dubai Basketball โ 7 โ 5 โ 2 โ +13 โ
โ 7 โ Real Madrid โ 7 โ 4 โ 3 โ +37 โ
โ 8 โ Crvena Zvezda Meridianbet โ 7 โ 4 โ 3 โ +28 โ
โ โ Belgโฆ โ โ โ โ โ
โ 9 โ FC Barcelona โ 7 โ 4 โ 3 โ +17 โ
โ 10 โ Virtus Bologna โ 7 โ 4 โ 3 โ -2 โ
โโโโโโโโโโโโดโโโโโโโโโโโโโโโโโโโโโโโโโโโโดโโโโโโโโโโโโโโโดโโโโโโโโโโโโดโโโโโโโโโโโโโดโโโโโโโโโโโโโโโโโโโโ
A shot chart on a FIBA court ๐ฏโ
euroleague_game_points returns one row
per field-goal attempt and made free throw, in centimeters with the hoop at the origin.
sdvplot.court_coords(..., provider="euroleague") converts that frame to meters on the FIBA court
that sdvplot.surface("fiba") draws (free throws, encoded -1, -1, become null). Both teams shoot at
one basket, so we draw the defensive half.
import sdvplot
gcode = str(played["game_code"][-1]) if played is not None and played.height else None
pts = safe(f"shots game {gcode}", lambda: el.euroleague_game_points(gcode, season_code)) if gcode else None
if pts is not None and pts.height:
shots = sdvplot.court_coords(pts, x="coord_x", y="coord_y", provider="euroleague")
shots = shots.filter(pl.col("court_x").is_not_null())
made = shots.filter(pl.col("id_action").is_in(["2FGM", "3FGM"]))
missed = shots.filter(pl.col("id_action").is_in(["2FGA", "3FGA"]))
ax = sdvplot.surface("fiba", display_range="defense")
ax.scatter(missed["court_x"], missed["court_y"], marker="x", s=30, c="#888888", zorder=20, label="missed")
ax.scatter(made["court_x"], made["court_y"], s=30, c="#d62728", zorder=21, label="made")
ax.legend(loc="upper right")
ax.set_title(f"EuroLeague {season_code} game {gcode}: {shots.height} field-goal attempts")
plt.show()
else:
print("no shot data for this game")
โ
shots game 69

๐ฌ๐ง Super League Basketball (Genius Sports)โ
slb_competitions lists the SLB competitions
Genius hosts; slb_schedule defaults to the current one.
comps = safe("SLB competitions", genius.slb_competitions)
comps.select([c for c in ["comp_id", "label", "season"] if c in comps.columns]).head() if comps is not None else None
โ
SLB competitions
shape: (5, 3)
โโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโ
โ comp_id โ label โ season โ
โ --- โ --- โ --- โ
โ i64 โ str โ i64 โ
โโโโโโโโโโโชโโโโโโโโโโโโโโโโโโโโโโโชโโโโโโโโโก
โ 49597 โ Championship 26-27 โ 2027 โ
โ 39625 โ Championship 2024-25 โ 2025 โ
โ 41897 โ Championship 2025-26 โ 2026 โ
โ 49599 โ Cup 26-27 โ 2027 โ
โ 39732 โ Cup 2024-25 โ 2025 โ
โโโโโโโโโโโดโโโโโโโโโโโโโโโโโโโโโโโดโโโโโโโโโ
sched = safe("SLB schedule", genius.slb_schedule)
done = sched.filter(pl.col("status") == "COMPLETE") if sched is not None else None
cols = ["match_id", "date_time", "home_team_name", "home_score", "away_team_name", "away_score"]
(done.select([c for c in cols if c in done.columns]).tail(5)
if done is not None and done.height else "no completed SLB games yet")
โ
SLB schedule
shape: (5, 6)
โโโโโโโโโโโโฌโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโ
โ match_id โ date_time โ home_team_name โ home_score โ away_team_name โ away_score โ
โ --- โ --- โ --- โ --- โ --- โ --- โ
โ i64 โ str โ str โ i64 โ str โ i64 โ
โโโโโโโโโโโโชโโโโโโโโโโโโโโโโชโโโโโโโโโโโโโโโโโโโโโโโชโโโโโโโโโโโโโชโโโโโโโโโโโโโโโโโโโชโโโโโโโโโโโโโก
โ 2882466 โ Oct 23, 2026, โ Leicester Riders โ 87 โ Manchester โ 89 โ
โ โ 7:30 PM โ โ โ Basketball โ โ
โ 2882469 โ Oct 23, 2026, โ Newcastle Eagles โ 82 โ Surrey 89ers โ 55 โ
โ โ 7:30 PM โ โ โ โ โ
โ 2882472 โ Oct 24, 2026, โ London Lions โ 82 โ Bristol Flyers โ 76 โ
โ โ 6:00 PM โ โ โ โ โ
โ 2882446 โ Oct 25, 2026, โ Liverpool Basketball โ 77 โ Newcastle Eagles โ 74 โ
โ โ 4:00 PM โ โ โ โ โ
โ 2882449 โ Oct 25, 2026, โ Cheshire Phoenix โ 91 โ Surrey 89ers โ 107 โ
โ โ 5:30 PM โ โ โ โ โ
โโโโโโโโโโโโดโโโโโโโโโโโโโโโโดโโโโโโโโโโโโโโโโโโโโโโโดโโโโโโโโโโโโโดโโโโโโโโโโโโโโโโโโโดโโโโโโโโโโโโโ
One game: box score and shots ๐โ
Take the latest completed match_id, then the player box score
(slb_player_box) and the shot chart
(slb_shots, x/y as a percent of the full court).
mid = int(done["match_id"][-1]) if done is not None and done.height else None
box = safe(f"SLB box {mid}", lambda: genius.slb_player_box(mid)) if mid else None
cols = ["name", "minutes", "points", "rebounds_total", "assists", "field_goals_made", "field_goals_attempted"]
(box.select([c for c in cols if c in box.columns]).sort("points", descending=True).head(8)
if box is not None and box.height and "points" in box.columns else "box score unavailable")
โ
SLB box 2882449
shape: (8, 7)
โโโโโโโโโโโโโโโโโโฌโโโโโโโโโโฌโโโโโโโโโฌโโโโโโโโโโโโโโโโโฌโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโ
โ name โ minutes โ points โ rebounds_total โ assists โ field_goals_mad โ field_goals_att โ
โ --- โ --- โ --- โ --- โ --- โ e โ empted โ
โ str โ str โ i64 โ i64 โ i64 โ --- โ --- โ
โ โ โ โ โ โ i64 โ i64 โ
โโโโโโโโโโโโโโโโโโชโโโโโโโโโโชโโโโโโโโโชโโโโโโโโโโโโโโโโโชโโโโโโโโโโชโโโโโโโโโโโโโโโโโโชโโโโโโโโโโโโโโโโโโก
โ A. Patterson โ 18:55 โ 20 โ 3 โ 1 โ 5 โ 10 โ
โ E. Reynolds Ii โ 29:22 โ 18 โ 3 โ 5 โ 5 โ 13 โ
โ J. Graham โ 26:37 โ 17 โ 4 โ 3 โ 7 โ 11 โ
โ C. Mcgill โ 31:13 โ 16 โ 3 โ 2 โ 6 โ 9 โ
โ J. Searles โ 25:48 โ 15 โ 4 โ 4 โ 5 โ 9 โ
โ J. Hinds โ 23:10 โ 14 โ 2 โ 0 โ 6 โ 11 โ
โ R. Cisarik โ 30:33 โ 13 โ 4 โ 2 โ 5 โ 7 โ
โ R. Stewart โ 11:45 โ 11 โ 2 โ 0 โ 4 โ 5 โ
โโโโโโโโโโโโโโโโโโดโโโโโโโโโโดโโโโโโโโโดโโโโโโโโโโโโโโโโโดโโโโโโโโโโดโโโโโโโโโโโโโโโโโโดโโโโโโโโโโโโโโโโโโ
sh = safe(f"SLB shots {mid}", lambda: genius.slb_shots(mid)) if mid else None
(sh.group_by("action_type", "result").len().sort("action_type")
if sh is not None and sh.height else "no shots for this game")
โ
SLB shots 2882449
shape: (4, 3)
โโโโโโโโโโโโโโโฌโโโโโโโโโฌโโโโโโ
โ action_type โ result โ len โ
โ --- โ --- โ --- โ
โ str โ i64 โ u32 โ
โโโโโโโโโโโโโโโชโโโโโโโโโชโโโโโโก
โ 2pt โ 0 โ 27 โ
โ 2pt โ 1 โ 43 โ
โ 3pt โ 0 โ 42 โ
โ 3pt โ 1 โ 26 โ
โโโโโโโโโโโโโโโดโโโโโโโโโดโโโโโโ
๐ฆ๐บ NBL (Australia)โ
The NBL's Rosetta API takes the season as its END year.
nbl_rosetta_standings gives the ladder;
nbl_rosetta_matches the match list.
nbl_season = 2026
ladder = safe(f"NBL ladder {nbl_season}", lambda: nbl_au.nbl_rosetta_standings(nbl_season))
cols = ["position", "team_name", "won", "lost", "win_percentage", "points_for", "points_against", "streak"]
(ladder.select([c for c in cols if c in ladder.columns]).sort("position")
if ladder is not None and ladder.height else "ladder unavailable")
โ
NBL ladder 2026
shape: (10, 8)
โโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโฌโโโโโโฌโโโโโโโฌโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโฌโโโโโโโโโ
โ position โ team_name โ won โ lost โ win_percentage โ points_for โ points_against โ streak โ
โ --- โ --- โ --- โ --- โ --- โ --- โ --- โ --- โ
โ i64 โ str โ i64 โ i64 โ f64 โ i64 โ i64 โ i64 โ
โโโโโโโโโโโโชโโโโโโโโโโโโโโโโโโโชโโโโโโชโโโโโโโชโโโโโโโโโโโโโโโโโชโโโโโโโโโโโโโชโโโโโโโโโโโโโโโโโชโโโโโโโโโก
โ 1 โ Sydney Kings โ 24 โ 9 โ 0.7273 โ 3276 โ 2879 โ 11 โ
โ 2 โ Adelaide 36ers โ 23 โ 10 โ 0.697 โ 3042 โ 2890 โ 0 โ
โ 3 โ South East โ 22 โ 11 โ 0.6667 โ 3324 โ 3061 โ 0 โ
โ โ Melbourne โ โ โ โ โ โ โ
โ โ Phoenix โ โ โ โ โ โ โ
โ 4 โ Perth Wildcats โ 21 โ 12 โ 0.6364 โ 2996 โ 2840 โ 1 โ
โ 5 โ Melbourne United โ 20 โ 13 โ 0.6061 โ 3041 โ 2905 โ 1 โ
โ 6 โ Tasmania โ 14 โ 19 โ 0.4242 โ 2873 โ 2884 โ 0 โ
โ โ JackJumpers โ โ โ โ โ โ โ
โ 7 โ NZ Breakers โ 13 โ 20 โ 0.3939 โ 3022 โ 3058 โ 1 โ
โ 8 โ Illawarra Hawks โ 13 โ 20 โ 0.3939 โ 3074 โ 3205 โ 2 โ
โ 9 โ Cairns Taipans โ 9 โ 24 โ 0.2727 โ 2754 โ 3194 โ 0 โ
โ 10 โ Brisbane Bullets โ 6 โ 27 โ 0.1818 โ 2710 โ 3196 โ 0 โ
โโโโโโโโโโโโดโโโโโโโโโโโโโโโโโโโดโโโโโโดโโโโโโโดโโโโโโโโโโโโโโโโโดโโโโโโโโโโโโโดโโโโโโโโโโโโโโโโโดโโโโโโโโโ
matches = safe(f"NBL matches {nbl_season}", lambda: nbl_au.nbl_rosetta_matches(nbl_season))
(matches.group_by("match_status").len() if matches is not None and "match_status" in matches.columns
else "matches unavailable")
โ
NBL matches 2026
shape: (2, 2)
โโโโโโโโโโโโโโโโฌโโโโโโ
โ match_status โ len โ
โ --- โ --- โ
โ str โ u32 โ
โโโโโโโโโโโโโโโโชโโโโโโก
โ draft โ 3 โ
โ complete โ 206 โ
โโโโโโโโโโโโโโโโดโโโโโโ
๐ช๐ธ Liga Endesa (ACB)โ
acb_standings defaults to the table after the current round;
acb_calendar lists every round of the season in one call.
A bar chart of the table makes the early-season race easy to read.
table = safe("ACB standings", acb.acb_standings)
if table is not None and table.height:
t = table.sort("win_pct", "plus_minus")
fig, ax = plt.subplots(figsize=(7, 6))
ax.barh(t["team"], t["win_pct"], color="#1f4e79")
ax.set_xlabel("win %")
end = int(t["season"][0]) # END year
ax.set_title(f"Liga Endesa {end - 1}-{str(end)[2:]} standings")
plt.tight_layout()
plt.show()
else:
print("ACB standings unavailable")
โ
ACB standings

cal = safe("ACB calendar", acb.acb_calendar)
(cal.group_by("round_number").agg(games=pl.len(), played=pl.col("home_score").is_not_null().sum())
.sort("round_number").head(8) if cal is not None and cal.height else "calendar unavailable")
โ
ACB calendar
shape: (8, 3)
โโโโโโโโโโโโโโโโฌโโโโโโโโฌโโโโโโโโโ
โ round_number โ games โ played โ
โ --- โ --- โ --- โ
โ i64 โ u32 โ u32 โ
โโโโโโโโโโโโโโโโชโโโโโโโโชโโโโโโโโโก
โ 1 โ 9 โ 9 โ
โ 2 โ 9 โ 9 โ
โ 3 โ 9 โ 9 โ
โ 4 โ 9 โ 9 โ
โ 5 โ 9 โ 9 โ
โ 6 โ 9 โ 9 โ
โ 7 โ 9 โ 9 โ
โ 8 โ 9 โ 9 โ
โโโโโโโโโโโโโโโโดโโโโโโโโดโโโโโโโโโ
๐ Where to nextโ
- Every Genius league (
wbbl_,bal_,nznbl_,esake_,lfb_,lnbm_,lub_) has the same_schedule/_player_box/_pbp/_shotsset as SLB. - More homes under
sportsdataverse.basketball:fiba_hub,atrium,feb,scb,lba,vtb,kbl,bleague,cebl,abaand others; each has a reference page in the sidebar. - Shot charts: sdvplot's
court_coordsalso converts NBA and WNBA stats-API shots (provider="nba"). - NBA, WNBA and college basketball have their own notebooks (
04,08,06,05).