{ "cells": [ { "cell_type": "markdown", "id": "5ef15f5a", "metadata": {}, "source": [ "# 🏒🌍 European and college hockey with `sportsdataverse-py`\n", "\n", "North America's minor and junior leagues have their own notebook (`11_junior_hockey_intro`). This one\n", "crosses the Atlantic and drops into the NCAA:\n", "\n", "| League | Module | Provider |\n", "|---|---|---|\n", "| SHL (Sweden) | `sportsdataverse.hockey.shl` | shl.se |\n", "| Liiga (Finland) | `sportsdataverse.hockey.liiga` | liiga.fi |\n", "| DEL (Germany) | `sportsdataverse.hockey.del_` | DEL (eSports feed) |\n", "| KHL | `sportsdataverse.hockey.khl` | khl.ru |\n", "| NCAA men's / women's hockey | `sportsdataverse.hockey.mch` / `.wch` | ESPN |\n", "\n", "DEL's package is `del_`, because `del` is a Python keyword. The European runtimes take the season as\n", "its END year (2026 is 2025-26) and space requests at least a second apart, so be patient. ⏳" ] }, { "cell_type": "markdown", "id": "71b37f07", "metadata": {}, "source": [ "## 🧰 The toolbox\n", "\n", "| Function | What it gives you |\n", "|---|---|\n", "| [`shl_schedule`](../shl/reference/shl.md#shl_schedule) | Schedule and results of an SHL season |\n", "| [`shl_pbp`](../shl/reference/shl.md#shl_pbp) | SHL play-by-play with shot locations (2023/24 on) |\n", "| [`liiga_standings`](../liiga/reference/liiga.md#liiga_standings) | Liiga standings |\n", "| [`liiga_games`](../liiga/reference/liiga.md#liiga_games) | Every Liiga game of a season, with xG |\n", "| [`liiga_shotmap`](../liiga/reference/liiga.md#liiga_shotmap) | One Liiga game's shots |\n", "| [`del_schedule`](../del/reference/del_esports.md#del_schedule) | DEL schedule and results |\n", "| [`del_standings`](../del/reference/del_esports.md#del_standings) | DEL standings |\n", "| [`del_shots`](../del/reference/del_esports.md#del_shots) | One DEL game's shots with rink coordinates |\n", "| [`khl_standings`](../khl/reference/khl_rest.md#khl_standings) | KHL regular-season standings |\n", "| [`espn_mch_standings`](../mch/reference/site.md#espn_mch_standings) | NCAA men's hockey standings |\n", "| [`espn_wch_scoreboard`](../wch/reference/site.md#espn_wch_scoreboard) | NCAA women's hockey scoreboard |" ] }, { "cell_type": "markdown", "id": "0033ea77", "metadata": {}, "source": [ "## 🔌 Setup\n", "\n", "```sh\n", "pip install \"sportsdataverse[sdvplot]\"\n", "```" ] }, { "cell_type": "code", "execution_count": null, "id": "4e44d547", "metadata": {}, "outputs": [], "source": [ "import matplotlib.pyplot as plt\n", "import polars as pl\n", "\n", "from sportsdataverse.hockey import del_, khl, liiga, mch, shl, wch\n", "\n", "pl.Config.set_tbl_rows(10)\n", "SEASON = 2026 # END year: the completed 2025-26 season" ] }, { "cell_type": "markdown", "id": "8a8371c4", "metadata": {}, "source": [ "Provider feeds are seasonal and occasionally rate-limited, so a tiny `safe()` helper runs each call\n", "defensively: you get the frame when the feed is up, and a one-line skip when it isn't, naming whether\n", "the answer was *nothing there* (`NoDataError`) or *the fetch failed* (`AssetFetchError`). 🛟" ] }, { "cell_type": "code", "execution_count": null, "id": "2cc88d46", "metadata": {}, "outputs": [], "source": [ "from sportsdataverse.errors import AssetFetchError, NoDataError\n", "\n", "def safe(label, thunk):\n", " try:\n", " out = thunk()\n", " print(f\"✅ {label}\")\n", " return out\n", " except (NoDataError, AssetFetchError) as e:\n", " print(f\"\\u23ed\\ufe0f {label}: {type(e).__name__}: {e}\")\n", " return None" ] }, { "cell_type": "markdown", "id": "b10cd343", "metadata": {}, "source": [ "## 🇸🇪 SHL\n", "\n", "[`shl_schedule`](../shl/reference/shl.md#shl_schedule) returns every game of a season with its\n", "`game_uuid`; [`shl_pbp`](../shl/reference/shl.md#shl_pbp) takes that uuid." ] }, { "cell_type": "code", "execution_count": null, "id": "39d6f451", "metadata": {}, "outputs": [], "source": [ "shl_sched = safe(f\"SHL schedule {SEASON}\", lambda: shl.shl_schedule(SEASON))\n", "cols = [\"start_date_time\", \"home_team_code\", \"home_team_score\", \"away_team_code\", \"away_team_score\", \"state\"]\n", "shl_sched.select([c for c in cols if c in shl_sched.columns]).tail(5) if shl_sched is not None else None" ] }, { "cell_type": "code", "execution_count": null, "id": "f04b477f", "metadata": {}, "outputs": [], "source": [ "done = shl_sched.filter(pl.col(\"state\") == \"post-game\") if shl_sched is not None else None\n", "guid = done[\"game_uuid\"][-1] if done is not None and done.height else None\n", "shl_pbp = safe(\"SHL pbp\", lambda: shl.shl_pbp(guid)) if guid else None\n", "(shl_pbp[\"type\"].value_counts(sort=True) if shl_pbp is not None and shl_pbp.height\n", " else \"no SHL play-by-play\")" ] }, { "cell_type": "markdown", "id": "1ddee312", "metadata": {}, "source": [ "## 🇫🇮 Liiga\n", "\n", "[`liiga_standings`](../liiga/reference/liiga.md#liiga_standings) defaults to the current season.\n", "[`liiga_games`](../liiga/reference/liiga.md#liiga_games) carries both teams' goals per game, and\n", "[`liiga_shotmap`](../liiga/reference/liiga.md#liiga_shotmap) one game's shots." ] }, { "cell_type": "code", "execution_count": null, "id": "20295eb8", "metadata": {}, "outputs": [], "source": [ "lst = safe(\"Liiga standings\", liiga.liiga_standings)\n", "cols = [\"tournament\", \"ranking\", \"team_name\", \"games\", \"wins\", \"losses\", \"points\", \"goals\", \"goals_against\"]\n", "(lst.filter(pl.col(\"tournament\") == lst[\"tournament\"][0])\n", " .select([c for c in cols if c in lst.columns]).sort(\"ranking\").head(8)\n", " if lst is not None and lst.height else \"standings unavailable\")" ] }, { "cell_type": "code", "execution_count": null, "id": "3c661e78", "metadata": {}, "outputs": [], "source": [ "lg = safe(\"Liiga games\", liiga.liiga_games)\n", "fin = lg.filter(pl.col(\"ended\") == True) if lg is not None else None\n", "if fin is not None and fin.height:\n", " g = fin.row(-1, named=True)\n", " sm = safe(f\"Liiga shotmap {g['game_id']}\", lambda: liiga.liiga_shotmap(g[\"season\"], g[\"game_id\"]))\n", " print(g[\"home_name\"], g[\"home_goals\"], \"-\", g[\"away_goals\"], g[\"away_name\"])\n", " out = sm.group_by(\"event_type\").len() if sm is not None and sm.height else \"no shotmap\"\n", "else:\n", " out = \"no finished Liiga games\"\n", "out" ] }, { "cell_type": "markdown", "id": "b63304de", "metadata": {}, "source": [ "## 🇩🇪 DEL\n", "\n", "[`del_standings`](../del/reference/del_esports.md#del_standings) and\n", "[`del_schedule`](../del/reference/del_esports.md#del_schedule) take the END year;\n", "[`del_shots`](../del/reference/del_esports.md#del_shots) a `match_id`." ] }, { "cell_type": "code", "execution_count": null, "id": "d83525eb", "metadata": {}, "outputs": [], "source": [ "dst = safe(f\"DEL standings {SEASON}\", lambda: del_.del_standings(SEASON))\n", "cols = [\"position\", \"name\", \"games\", \"wins\", \"wins_ot\", \"losses_ot\", \"losses\", \"points\"]\n", "dst.select([c for c in cols if c in dst.columns]).sort(\"position\").head(8) if dst is not None else None" ] }, { "cell_type": "markdown", "id": "bc82b2d5", "metadata": {}, "source": [ "### A DEL shot map 🎯\n", "\n", "`del_shots` ships `coordinate_x` / `coordinate_y` in the provider's own frame (both axes run about\n", "-100 to 100), plus `team_side` and `is_goal`. sdvplot has no mapping from this frame to a regulation\n", "IIHF rink yet, so the chart below plots the raw frame rather than drawing a rink under it." ] }, { "cell_type": "code", "execution_count": null, "id": "1915133d", "metadata": {}, "outputs": [], "source": [ "dsched = safe(f\"DEL schedule {SEASON}\", lambda: del_.del_schedule(SEASON))\n", "mid = int(dsched.filter(pl.col(\"final_score_home\").is_not_null())[\"match_id\"][-1]) if dsched is not None else None\n", "dshots = safe(f\"DEL shots {mid}\", lambda: del_.del_shots(mid)) if mid else None\n", "if dshots is not None and dshots.height:\n", " fig, ax = plt.subplots(figsize=(8, 5))\n", " for side, color in ((\"home\", \"#1f77b4\"), (\"visitor\", \"#ff7f0e\")):\n", " s = dshots.filter(pl.col(\"team_side\") == side)\n", " name = s[\"home_name\" if side == \"home\" else \"visitor_name\"][0] if s.height else side\n", " ax.scatter(s[\"coordinate_x\"], s[\"coordinate_y\"], s=25, alpha=0.6, c=color, label=name)\n", " goals = dshots.filter(pl.col(\"is_goal\") == True)\n", " ax.scatter(goals[\"coordinate_x\"], goals[\"coordinate_y\"], s=160, facecolors=\"none\",\n", " edgecolors=\"black\", linewidths=1.5, label=\"goal\")\n", " ax.set_xlim(-105, 105)\n", " ax.set_ylim(-105, 105)\n", " ax.set_aspect(\"equal\")\n", " ax.legend(loc=\"upper center\", ncol=3, fontsize=8)\n", " ax.set_title(f\"DEL match {mid}: {dshots.height} shots (provider frame)\")\n", " plt.show()\n", "else:\n", " print(\"no DEL shots for this match\")" ] }, { "cell_type": "markdown", "id": "201da2c4", "metadata": {}, "source": [ "## 🇷🇺 KHL\n", "\n", "[`khl_standings`](../khl/reference/khl_rest.md#khl_standings) returns one row per team per table\n", "(conference and division tables are stacked, tagged by `table`)." ] }, { "cell_type": "code", "execution_count": null, "id": "945d9a21", "metadata": {}, "outputs": [], "source": [ "kst = safe(\"KHL standings\", khl.khl_standings)\n", "cols = [\"table\", \"rank\", \"name\", \"gp\", \"w\", \"l\", \"pts\", \"goals_for\", \"goals_against\"]\n", "(kst.filter(pl.col(\"table\") == kst[\"table\"][0]).select([c for c in cols if c in kst.columns]).head(8)\n", " if kst is not None and kst.height else \"standings unavailable\")" ] }, { "cell_type": "markdown", "id": "b4bb3d4b", "metadata": {}, "source": [ "## 🎓 College hockey (ESPN)\n", "\n", "The NCAA men's (`mch`) and women's (`wch`) leagues ride the cross-league ESPN wrappers. Team colors\n", "come from sdvplot's `team_colors`, keyed by ESPN team id under the `ncaa_mhockey` league key." ] }, { "cell_type": "code", "execution_count": null, "id": "0bf8c827", "metadata": {}, "outputs": [], "source": [ "import sdvplot\n", "\n", "cst = safe(\"NCAA men's hockey standings\", mch.espn_mch_standings)\n", "if cst is not None and cst.height:\n", " top = cst.sort(\"wins\", \"win_percent\", descending=True).head(15).sort(\"wins\", \"win_percent\")\n", " colors = sdvplot.team_colors(\"ncaa_mhockey\", [str(i) for i in top[\"team_id\"]])\n", " colors = [c or \"#999999\" for c in colors]\n", " fig, ax = plt.subplots(figsize=(7, 6))\n", " ax.barh(top[\"team_display_name\"], top[\"wins\"], color=colors)\n", " ax.set_xlabel(\"wins\")\n", " ax.set_title(\"NCAA men's hockey: the 15 teams with the most wins so far\")\n", " plt.tight_layout()\n", " plt.show()\n", "else:\n", " print(\"standings unavailable\")" ] }, { "cell_type": "code", "execution_count": null, "id": "48921a00", "metadata": {}, "outputs": [], "source": [ "wsb = safe(\"NCAA women's hockey scoreboard\", wch.espn_wch_scoreboard)\n", "cols = [\"date\", \"short_name\", \"status_type_detail\", \"home_score\", \"away_score\"]\n", "wsb.select([c for c in cols if c in wsb.columns]) if wsb is not None and wsb.height else \"no games today\"" ] }, { "cell_type": "markdown", "id": "91e0abe0", "metadata": {}, "source": [ "## 🎉 Where to next\n", "\n", "- More European homes under `sportsdataverse.hockey`: `icehl`, `eihl`, `metal`, `elh`, `ha`, `nl`,\n", " `magnus`, `vhl` and the Swiss `sihf`; each has a reference page in the sidebar.\n", "- `khl_webcaster_*` is a second KHL source with player and team tables per stage.\n", "- North American minor and junior leagues: `11_junior_hockey_intro`; the PWHL: `10_pwhl_intro`; the\n", " NHL: `07_nhl_intro`.\n", "- Rinks and team colors: [sdvplot](https://sdvplot.sportsdataverse.org/) and\n", " [sportypy](https://sportypy.sportsdataverse.org/)." ] } ], "metadata": { "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "name": "python" } }, "nbformat": 4, "nbformat_minor": 5 }