๐๐ European and college hockey with sportsdataverse-py
This page is the executed notebook
17_european_college_hockey_intro.ipynb: download it to run it yourself.
North America's minor and junior leagues have their own notebook (11_junior_hockey_intro). This one
crosses the Atlantic and drops into the NCAA:
| League | Module | Provider |
|---|---|---|
| SHL (Sweden) | sportsdataverse.hockey.shl | shl.se |
| Liiga (Finland) | sportsdataverse.hockey.liiga | liiga.fi |
| DEL (Germany) | sportsdataverse.hockey.del_ | DEL (eSports feed) |
| KHL | sportsdataverse.hockey.khl | khl.ru |
| NCAA men's / women's hockey | sportsdataverse.hockey.mch / .wch | ESPN |
DEL's package is del_, because del is a Python keyword. The European runtimes take the season as
its END year (2026 is 2025-26) and space requests at least a second apart, so be patient. โณ
๐งฐ The toolboxโ
| Function | What it gives you |
|---|---|
shl_schedule | Schedule and results of an SHL season |
shl_pbp | SHL play-by-play with shot locations (2023/24 on) |
liiga_standings | Liiga standings |
liiga_games | Every Liiga game of a season, with xG |
liiga_shotmap | One Liiga game's shots |
del_schedule | DEL schedule and results |
del_standings | DEL standings |
del_shots | One DEL game's shots with rink coordinates |
khl_standings | KHL regular-season standings |
espn_mch_standings | NCAA men's hockey standings |
espn_wch_scoreboard | NCAA women's hockey scoreboard |
๐ Setupโ
pip install "sportsdataverse[sdvplot]"
import matplotlib.pyplot as plt
import polars as pl
from sportsdataverse.hockey import del_, khl, liiga, mch, shl, wch
pl.Config.set_tbl_rows(10)
SEASON = 2026 # END year: the completed 2025-26 season
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
๐ธ๐ช SHLโ
shl_schedule returns every game of a season with its
game_uuid; shl_pbp takes that uuid.
shl_sched = safe(f"SHL schedule {SEASON}", lambda: shl.shl_schedule(SEASON))
cols = ["start_date_time", "home_team_code", "home_team_score", "away_team_code", "away_team_score", "state"]
shl_sched.select([c for c in cols if c in shl_sched.columns]).tail(5) if shl_sched is not None else None
โ
SHL schedule 2026
shape: (5, 6)
โโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโ
โ start_date_time โ home_team_code โ home_team_score โ away_team_code โ away_team_scor โ state โ
โ --- โ --- โ --- โ --- โ e โ --- โ
โ str โ str โ i64 โ str โ --- โ str โ
โ โ โ โ โ i64 โ โ
โโโโโโโโโโโโโโโโโโโชโโโโโโโโโโโโโโโโโชโโโโโโโโโโโโโโโโโโชโโโโโโโโโโโโโโโโโชโโโโโโโโโโโโโโโโโชโโโโโโโโโโโโก
โ 2026-03-14 โ TIK โ 1 โ VLH โ 3 โ post-game โ
โ 15:15:00 โ โ โ โ โ โ
โ 2026-03-14 โ LHC โ 3 โ OHK โ 2 โ post-game โ
โ 15:15:00 โ โ โ โ โ โ
โ 2026-03-14 โ DIF โ 2 โ SAIK โ 3 โ post-game โ
โ 15:15:00 โ โ โ โ โ โ
โ 2026-03-14 โ BIF โ 3 โ HV71 โ 4 โ post-game โ
โ 15:15:00 โ โ โ โ โ โ
โ 2026-03-14 โ FHC โ 4 โ MIF โ 1 โ post-game โ
โ 15:15:00 โ โ โ โ โ โ
โโโโโโโโโโโโโโโโโโโดโโโโโโโโโโโโโโโโโดโโโโโโโโโโโโโโโโโโดโโโโโโโโโโโโโโโโโดโโโโโโโโโโโโโโโโโดโโโโโโโโโโโโ
done = shl_sched.filter(pl.col("state") == "post-game") if shl_sched is not None else None
guid = done["game_uuid"][-1] if done is not None and done.height else None
shl_pbp = safe("SHL pbp", lambda: shl.shl_pbp(guid)) if guid else None
(shl_pbp["type"].value_counts(sort=True) if shl_pbp is not None and shl_pbp.height
else "no SHL play-by-play")
โ
SHL pbp
shape: (7, 2)
โโโโโโโโโโโโโโฌโโโโโโโโ
โ type โ count โ
โ --- โ --- โ
โ str โ u32 โ
โโโโโโโโโโโโโโชโโโโโโโโก
โ shot โ 84 โ
โ goalkeeper โ 6 โ
โ goal โ 5 โ
โ insight โ 4 โ
โ penalty โ 3 โ
โ period โ 3 โ
โ timeout โ 1 โ
โโโโโโโโโโโโโโดโโโโโโโโ
๐ซ๐ฎ Liigaโ
liiga_standings defaults to the current season.
liiga_games carries both teams' goals per game, and
liiga_shotmap one game's shots.
lst = safe("Liiga standings", liiga.liiga_standings)
cols = ["tournament", "ranking", "team_name", "games", "wins", "losses", "points", "goals", "goals_against"]
(lst.filter(pl.col("tournament") == lst["tournament"][0])
.select([c for c in cols if c in lst.columns]).sort("ranking").head(8)
if lst is not None and lst.height else "standings unavailable")
โ
Liiga standings
shape: (8, 9)
โโโโโโโโโโโโโโฌโโโโโโโโโโฌโโโโโโโโโโโโฌโโโโโโโโฌโโโโฌโโโโโโโโโฌโโโโโโโโโฌโโโโโโโโฌโโโโโโโโโโโโโโโโ
โ tournament โ ranking โ team_name โ games โ โฆ โ losses โ points โ goals โ goals_against โ
โ --- โ --- โ --- โ --- โ โ --- โ --- โ --- โ --- โ
โ str โ i64 โ str โ i64 โ โ i64 โ i64 โ i64 โ i64 โ
โโโโโโโโโโโโโโชโโโโโโโโโโชโโโโโโโโโโโโชโโโโโโโโชโโโโชโโโโโโโโโชโโโโโโโโโชโโโโโโโโชโโโโโโโโโโโโโโโโก
โ runkosarja โ 1 โ Lukko โ 17 โ โฆ โ 4 โ 35 โ 46 โ 32 โ
โ runkosarja โ 2 โ SaiPa โ 16 โ โฆ โ 3 โ 34 โ 49 โ 27 โ
โ runkosarja โ 3 โ Ilves โ 17 โ โฆ โ 5 โ 32 โ 48 โ 36 โ
โ runkosarja โ 4 โ Jukurit โ 18 โ โฆ โ 6 โ 32 โ 44 โ 35 โ
โ runkosarja โ 5 โ JYP โ 14 โ โฆ โ 3 โ 30 โ 44 โ 35 โ
โ runkosarja โ 6 โ Kรคrpรคt โ 18 โ โฆ โ 4 โ 30 โ 51 โ 50 โ
โ runkosarja โ 7 โ รssรคt โ 17 โ โฆ โ 6 โ 28 โ 38 โ 40 โ
โ runkosarja โ 8 โ TPS โ 17 โ โฆ โ 7 โ 27 โ 47 โ 42 โ
โโโโโโโโโโโโโโดโโโโโโโโโโดโโโโโโโโโโโโดโโโโโโโโดโโโโดโโโโโโโโโดโโโโโโโโโดโโโโโโโโดโโโโโโโโโโโโโโโโ
lg = safe("Liiga games", liiga.liiga_games)
fin = lg.filter(pl.col("ended") == True) if lg is not None else None
if fin is not None and fin.height:
g = fin.row(-1, named=True)
sm = safe(f"Liiga shotmap {g['game_id']}", lambda: liiga.liiga_shotmap(g["season"], g["game_id"]))
print(g["home_name"], g["home_goals"], "-", g["away_goals"], g["away_name"])
out = sm.group_by("event_type").len() if sm is not None and sm.height else "no shotmap"
else:
out = "no finished Liiga games"
out
โ
Liiga games
โ
Liiga shotmap 2701415
Sport 1 - 3 SaiPa
shape: (4, 2)
โโโโโโโโโโโโโโโโโโฌโโโโโโ
โ event_type โ len โ
โ --- โ --- โ
โ str โ u32 โ
โโโโโโโโโโโโโโโโโโชโโโโโโก
โ PLAYER_BLOCKED โ 25 โ
โ MISSED โ 27 โ
โ GOAL โ 4 โ
โ GOALIE_BLOCKED โ 41 โ
โโโโโโโโโโโโโโโโโโดโโโโโโ
๐ฉ๐ช DELโ
del_standings and
del_schedule take the END year;
del_shots a match_id.
dst = safe(f"DEL standings {SEASON}", lambda: del_.del_standings(SEASON))
cols = ["position", "name", "games", "wins", "wins_ot", "losses_ot", "losses", "points"]
dst.select([c for c in cols if c in dst.columns]).sort("position").head(8) if dst is not None else None
โ
DEL standings 2026
shape: (8, 8)
โโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโฌโโโโโโโโฌโโโโโโโฌโโโโโโโโโโฌโโโโโโโโโโโโฌโโโโโโโโโฌโโโโโโโโโ
โ position โ name โ games โ wins โ wins_ot โ losses_ot โ losses โ points โ
โ --- โ --- โ --- โ --- โ --- โ --- โ --- โ --- โ
โ i64 โ str โ i64 โ i64 โ i64 โ i64 โ i64 โ i64 โ
โโโโโโโโโโโโชโโโโโโโโโโโโโโโโโโโโโโโชโโโโโโโโชโโโโโโโชโโโโโโโโโโชโโโโโโโโโโโโชโโโโโโโโโชโโโโโโโโโก
โ 1 โ Kรถlner Haie โ 52 โ 36 โ 3 โ 2 โ 11 โ 116 โ
โ 2 โ Adler Mannheim โ 52 โ 29 โ 6 โ 4 โ 13 โ 103 โ
โ 3 โ Straubing Tigers โ 52 โ 32 โ 2 โ 1 โ 17 โ 101 โ
โ 4 โ EHC Red Bull Mรผnchen โ 52 โ 29 โ 5 โ 3 โ 15 โ 100 โ
โ 5 โ ERC Ingolstadt โ 52 โ 28 โ 5 โ 0 โ 19 โ 94 โ
โ 6 โ Eisbรคren Berlin โ 52 โ 23 โ 6 โ 4 โ 19 โ 85 โ
โ 7 โ Pinguins Bremerhaven โ 52 โ 24 โ 4 โ 3 โ 21 โ 83 โ
โ 8 โ Grizzlys Wolfsburg โ 52 โ 20 โ 3 โ 7 โ 22 โ 73 โ
โโโโโโโโโโโโดโโโโโโโโโโโโโโโโโโโโโโโดโโโโโโโโดโโโโโโโดโโโโโโโโโโดโโโโโโโโโโโโดโโโโโโโโโดโโโโโโโโโ
A DEL shot map ๐ฏโ
del_shots ships coordinate_x / coordinate_y in the provider's own frame (both axes run about
-100 to 100), plus team_side and is_goal. sdvplot has no mapping from this frame to a regulation
IIHF rink yet, so the chart below plots the raw frame rather than drawing a rink under it.
dsched = safe(f"DEL schedule {SEASON}", lambda: del_.del_schedule(SEASON))
mid = int(dsched.filter(pl.col("final_score_home").is_not_null())["match_id"][-1]) if dsched is not None else None
dshots = safe(f"DEL shots {mid}", lambda: del_.del_shots(mid)) if mid else None
if dshots is not None and dshots.height:
fig, ax = plt.subplots(figsize=(8, 5))
for side, color in (("home", "#1f77b4"), ("visitor", "#ff7f0e")):
s = dshots.filter(pl.col("team_side") == side)
name = s["home_name" if side == "home" else "visitor_name"][0] if s.height else side
ax.scatter(s["coordinate_x"], s["coordinate_y"], s=25, alpha=0.6, c=color, label=name)
goals = dshots.filter(pl.col("is_goal") == True)
ax.scatter(goals["coordinate_x"], goals["coordinate_y"], s=160, facecolors="none",
edgecolors="black", linewidths=1.5, label="goal")
ax.set_xlim(-105, 105)
ax.set_ylim(-105, 105)
ax.set_aspect("equal")
ax.legend(loc="upper center", ncol=3, fontsize=8)
ax.set_title(f"DEL match {mid}: {dshots.height} shots (provider frame)")
plt.show()
else:
print("no DEL shots for this match")
โ
DEL schedule 2026
โ
DEL shots 4304

๐ท๐บ KHLโ
khl_standings returns one row per team per table
(conference and division tables are stacked, tagged by table).
kst = safe("KHL standings", khl.khl_standings)
cols = ["table", "rank", "name", "gp", "w", "l", "pts", "goals_for", "goals_against"]
(kst.filter(pl.col("table") == kst["table"][0]).select([c for c in cols if c in kst.columns]).head(8)
if kst is not None and kst.height else "standings unavailable")
โ
KHL standings
shape: (8, 9)
โโโโโโโโโโฌโโโโโโโฌโโโโโโโโโโโโโโโฌโโโโโโฌโโโโฌโโโโโโฌโโโโโโฌโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโ
โ table โ rank โ name โ gp โ โฆ โ l โ pts โ goals_for โ goals_against โ
โ --- โ --- โ --- โ --- โ โ --- โ --- โ --- โ --- โ
โ str โ i64 โ str โ i64 โ โ i64 โ i64 โ i64 โ i64 โ
โโโโโโโโโโชโโโโโโโชโโโโโโโโโโโโโโโชโโโโโโชโโโโชโโโโโโชโโโโโโชโโโโโโโโโโโโชโโโโโโโโโโโโโโโโก
โ league โ 1 โ ะะตัะฐะปะปััะณ ะะณ โ 20 โ โฆ โ 3 โ 32 โ 67 โ 39 โ
โ league โ 2 โ ะะพะบะพะผะพัะธะฒ โ 19 โ โฆ โ 5 โ 28 โ 49 โ 33 โ
โ league โ 3 โ ะฆะกะะ โ 19 โ โฆ โ 5 โ 28 โ 58 โ 36 โ
โ league โ 4 โ ะัะฐะบะพะฝั โ 18 โ โฆ โ 4 โ 26 โ 56 โ 44 โ
โ league โ 5 โ ะกะฟะฐััะฐะบ โ 19 โ โฆ โ 5 โ 25 โ 51 โ 35 โ
โ league โ 6 โ ะะตััะตั
ะธะผะธะบ โ 19 โ โฆ โ 6 โ 24 โ 55 โ 46 โ
โ league โ 7 โ ะกะตะฒะตัััะฐะปั โ 20 โ โฆ โ 7 โ 24 โ 46 โ 43 โ
โ league โ 8 โ ะขะพัะฟะตะดะพ โ 18 โ โฆ โ 5 โ 24 โ 49 โ 36 โ
โโโโโโโโโโดโโโโโโโดโโโโโโโโโโโโโโโดโโโโโโดโโโโดโโโโโโดโโโโโโดโโโโโโโโโโโโดโโโโโโโโโโโโโโโโ
๐ College hockey (ESPN)โ
The NCAA men's (mch) and women's (wch) leagues ride the cross-league ESPN wrappers. Team colors
come from sdvplot's team_colors, keyed by ESPN team id under the ncaa_mhockey league key.
import sdvplot
cst = safe("NCAA men's hockey standings", mch.espn_mch_standings)
if cst is not None and cst.height:
top = cst.sort("wins", "win_percent", descending=True).head(15).sort("wins", "win_percent")
colors = sdvplot.team_colors("ncaa_mhockey", [str(i) for i in top["team_id"]])
colors = [c or "#999999" for c in colors]
fig, ax = plt.subplots(figsize=(7, 6))
ax.barh(top["team_display_name"], top["wins"], color=colors)
ax.set_xlabel("wins")
ax.set_title("NCAA men's hockey: the 15 teams with the most wins so far")
plt.tight_layout()
plt.show()
else:
print("standings unavailable")
โ
NCAA men's hockey standings

wsb = safe("NCAA women's hockey scoreboard", wch.espn_wch_scoreboard)
cols = ["date", "short_name", "status_type_detail", "home_score", "away_score"]
wsb.select([c for c in cols if c in wsb.columns]) if wsb is not None and wsb.height else "no games today"
โ
NCAA women's hockey scoreboard
shape: (1, 5)
โโโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโฌโโโโโโโโโโโโโ
โ date โ short_name โ status_type_detail โ home_score โ away_score โ
โ --- โ --- โ --- โ --- โ --- โ
โ str โ str โ str โ str โ str โ
โโโโโโโโโโโโโโโโโโโโโชโโโโโโโโโโโโโชโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโชโโโโโโโโโโโโโชโโโโโโโโโโโโโก
โ 2026-10-26T15:00Z โ MNST @ BC โ Mon, October 26th at 11:00 AM โฆ โ 0 โ 0 โ
โโโโโโโโโโโโโโโโโโโโโดโโโโโโโโโโโโโดโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโดโโโโโโโโโโโโโดโโโโโโโโโโโโโ
๐ Where to nextโ
- More European homes under
sportsdataverse.hockey:icehl,eihl,metal,elh,ha,nl,magnus,vhland the Swisssihf; each has a reference page in the sidebar. khl_webcaster_*is a second KHL source with player and team tables per stage.- North American minor and junior leagues:
11_junior_hockey_intro; the PWHL:10_pwhl_intro; the NHL:07_nhl_intro. - Rinks and team colors: sdvplot and sportypy.