Analyzing a game¶
Let's look at one game: the 2025 CEBL final, where the Niagara River Lions beat the Calgary Surge 79–73 to win the championship.
Find the game¶
Every dataset is keyed on fiba_game_id, so the schedule is where you start.
import ceblpy
schedule = ceblpy.load_cebl_schedule(2025)
final = schedule[schedule["fiba_game_id"] == "2702972"]
print(
final[
[
"start_time_utc",
"venue_name",
"home_team_name",
"home_team_score",
"away_team_name",
"away_team_score",
]
].to_markdown(index=False)
)
| start_time_utc | venue_name | home_team_name | home_team_score | away_team_name | away_team_score |
|---|---|---|---|---|---|
| 2025-08-24 23:00:00+00:00 | Canada Life Centre | Calgary Surge | 73 | Niagara River Lions | 79 |
Team totals¶
The team box score has one row per team per game.
import ceblpy
boxscore = ceblpy.load_cebl_team_boxscore(2025)
final = boxscore[boxscore["fiba_game_id"] == "2702972"]
print(
final[
[
"team_name",
"points",
"field_goals_made",
"field_goals_attempted",
"total_rebounds",
"bench_points",
"points_in_paint",
]
].to_markdown(index=False)
)
| team_name | points | field_goals_made | field_goals_attempted | total_rebounds | bench_points | points_in_paint |
|---|---|---|---|---|---|---|
| Calgary Surge | 73 | 25 | 73 | 46 | 15 | 30 |
| Niagara River Lions | 79 | 29 | 74 | 50 | 26 | 20 |
The two teams combined for 54 made field goals on 147 attempts. Calgary scored more in the paint, 30–20, while Niagara had the edge on the glass and from the bench.
Individual lines¶
The player box score has one row per player per game, including players who
didn't get off the bench. Filtering on minutes > 0 drops them, which is
useful when you're computing per-player rates and don't want zero-minute rows
in the denominator.
import ceblpy
players = ceblpy.load_cebl_player_boxscore(2025)
final = players[(players["fiba_game_id"] == "2702972") & (players["minutes"] > 0)]
print(
final.sort_values("points", ascending=False)
.head(6)[
[
"first_name",
"family_name",
"minutes",
"points",
"total_rebounds",
"assists",
]
]
.to_markdown(index=False)
)
| first_name | family_name | minutes | points | total_rebounds | assists |
|---|---|---|---|---|---|
| Sean | Miller-Moore | 37.0167 | 19 | 4 | 1 |
| Khalil | Ahmad | 28.9667 | 16 | 9 | 1 |
| Nathan | Cayo | 34.6333 | 14 | 6 | 3 |
| Khyri | Thomas | 17 | 13 | 1 | 0 |
| Evan | Gilyard Ii | 30.9833 | 13 | 2 | 4 |
| Ahmed | Hill | 20.9167 | 13 | 6 | 2 |
Where the shots came from¶
Play-by-play has one row per event. Shots carry court coordinates in x and
y, so you can work with location as well as outcome.
Traceback (most recent call last):
File "/home/runner/work/ceblpy/ceblpy/.venv/lib/python3.13/site-packages/markdown_exec/_internal/formatters/python.py", line 71, in _run_python
exec_python(code, code_block_id, exec_globals)
~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/runner/work/ceblpy/ceblpy/.venv/lib/python3.13/site-packages/markdown_exec/_internal/formatters/_exec_python.py", line 7, in exec_python
compiled = compile(code, filename=filename, mode="exec")
File "<code block: n9>", line 5
pbp["fiba_game_id"] == "2702972") & (pbp["action_type"].isin(["2pt", "3pt"]))
^
SyntaxError: closing parenthesis ')' does not match opening parenthesis '[' on line 4
Plotting the shots¶
Lets create a plot of every shot in the game for the home team, Niagra. Shots show up at both ends because they switch sides at the half. So the first half is one side and the second is the other.
from io import StringIO
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
from mplbasketball import Court
import ceblpy
pbp = ceblpy.load_cebl_pbp(2025)
shots = pbp[
(pbp["fiba_game_id"] == "2702972")
& (pbp["action_type"].isin(["2pt", "3pt"]))
& (pbp["is_home"] == True)
].copy()
# x and y run 0-100 across the full court; scale to FIBA metres (28m x 15m)
shots["x_m"] = (shots["x"] / 100) * 28
shots["y_m"] = (shots["y"] / 100) * 15
# Drawn in a placeholder colour, swapped for currentColor at the end so the
# court and labels follow the page's light or dark theme.
INHERIT = "#ff00ff"
court = Court(court_type="fiba", origin="bottom-left", units="m")
fig, ax = court.draw(showaxis=False, line_color=INHERIT, line_alpha=0.45)
shot_styles = {
True: ("Made", {"color": "#e8452c"}),
False: ("Missed", {"facecolors": "none", "edgecolors": "#4c9be8"}),
}
for made, (label, style) in shot_styles.items():
subset = shots[shots["success"].eq(made)]
ax.scatter(subset["x_m"], subset["y_m"], s=55, alpha=0.9, label=label, **style)
ax.legend(loc="upper left", bbox_to_anchor=(1.02, 1), frameon=False, labelcolor=INHERIT)
ax.set_title("Niagara River Lions shots — 2025 CEBL final", pad=15, color=INHERIT)
buffer = StringIO()
plt.savefig(buffer, format="svg", bbox_inches="tight", transparent=True)
print(buffer.getvalue().replace(INHERIT, "currentColor"))
What else¶
load_cebl_officials gives you the crew for each game, and every loader takes
a list of seasons or nothing at all if you want the full history. See the
API reference for the details.