{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "0",
   "metadata": {},
   "source": [
    "# College hockey\n",
    "\n",
    "Nine charts and tables from the 2025-26 NCAA Division I men's and women's hockey seasons: records and\n",
    "opponent-adjusted ratings built from ESPN's scoreboard, conference strength, the USCHO poll week by week, the men's\n",
    "NCAA tournament and a women's ratings chart. ESPN publishes no college hockey standings, so everything starts from\n",
    "the game results that [sportsdataverse-py](https://py.sportsdataverse.org/)'s `espn_mch_*` (men) and `espn_wch_*`\n",
    "(women) wrappers return."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "1",
   "metadata": {},
   "outputs": [],
   "source": [
    "import warnings\n",
    "\n",
    "import matplotlib.pyplot as plt\n",
    "import polars as pl\n",
    "import sportsdataverse as sdv\n",
    "from sportsdataverse.hockey.college_hockey_ratings import college_hockey_game_results, college_hockey_ratings\n",
    "\n",
    "import sdvplot\n",
    "\n",
    "SEASON = 2026  # the 2025-26 season, named by the year it ends\n",
    "MONTHS = [\"202509\", \"202510\", \"202511\", \"202512\", \"202601\", \"202602\", \"202603\", \"202604\"]\n",
    "\n",
    "\n",
    "def season_events(league):\n",
    "    \"\"\"Every scoreboard event of the season: ESPN's college scoreboard takes a whole month (YYYYMM) as its date.\"\"\"\n",
    "    scoreboard = getattr(sdv, f\"espn_{league}_scoreboard\")\n",
    "    return [e for m in MONTHS for e in scoreboard(dates=m, return_parsed=False).get(\"events\", [])]\n",
    "\n",
    "\n",
    "men, women = season_events(\"mch\"), season_events(\"wch\")\n",
    "len(men), len(women)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "2",
   "metadata": {},
   "source": [
    "## 1. Records from the scoreboard\n",
    "\n",
    "`college_hockey_game_results` turns the events into one row per team and completed game; a record is a `group_by`\n",
    "away. College hockey keeps ties (a game still level after overtime), so the winning percentage counts a tie as half a\n",
    "win."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "3",
   "metadata": {},
   "outputs": [],
   "source": [
    "def records(events, league):\n",
    "    games = college_hockey_game_results(events, league=league)\n",
    "    names = {c[\"team\"][\"id\"]: c[\"team\"][\"displayName\"] for e in events for c in e[\"competitions\"][0][\"competitors\"]}\n",
    "    return (\n",
    "        games.group_by(\"team_id\", maintain_order=True)\n",
    "        .agg(\n",
    "            pl.len().alias(\"gp\"),\n",
    "            (pl.col(\"goals_for\") > pl.col(\"goals_against\")).sum().alias(\"w\"),\n",
    "            (pl.col(\"goals_for\") < pl.col(\"goals_against\")).sum().alias(\"l\"),\n",
    "            (pl.col(\"goals_for\") == pl.col(\"goals_against\")).sum().alias(\"t\"),\n",
    "            pl.col(\"goals_for\").sum().alias(\"gf\"),\n",
    "            pl.col(\"goals_against\").sum().alias(\"ga\"),\n",
    "        )\n",
    "        .with_columns(\n",
    "            team=pl.col(\"team_id\").replace_strict(names),\n",
    "            pct=(pl.col(\"w\") + pl.col(\"t\") / 2) / pl.col(\"gp\"),\n",
    "            record=pl.format(\"{}-{}-{}\", \"w\", \"l\", \"t\"),\n",
    "        )\n",
    "        .sort([\"pct\", \"w\", \"team\"], descending=[True, True, False])\n",
    "    )\n",
    "\n",
    "\n",
    "men_records = records(men, \"mch\")\n",
    "men_records.head(5)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "4",
   "metadata": {},
   "source": [
    "## 2. The top sixteen, with conferences\n",
    "\n",
    "ESPN's group endpoints list each conference's teams, which gives every team its conference. A great_tables table of\n",
    "the sixteen best records with `gt_sdv_logos` on ESPN's team ids and the NCAA-style `gt_theme_ncaa`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "5",
   "metadata": {},
   "outputs": [],
   "source": [
    "from great_tables import GT\n",
    "\n",
    "from sdvplot.great_tables import gt_sdv_logos, gt_theme_ncaa\n",
    "\n",
    "rows = []\n",
    "for item in sdv.espn_mch_season_groups(season=SEASON, season_type=2, return_parsed=False)[\"items\"]:\n",
    "    group_id = item[\"$ref\"].split(\"/groups/\")[1].split(\"?\")[0]\n",
    "    group = sdv.espn_mch_season_group(season=SEASON, season_type=2, group_id=group_id, return_parsed=False)\n",
    "    members = sdv.espn_mch_season_group_teams(SEASON, 2, group_id, return_parsed=False)[\"items\"]\n",
    "    rows += [\n",
    "        {\"team_id\": m[\"$ref\"].split(\"/teams/\")[1].split(\"?\")[0], \"conference\": group[\"abbreviation\"]} for m in members\n",
    "    ]\n",
    "conferences = pl.DataFrame(rows)\n",
    "assert conferences.schema[\"team_id\"] == men_records.schema[\"team_id\"] == pl.String\n",
    "men_records = men_records.join(conferences, on=\"team_id\", how=\"left\", maintain_order=\"left\")\n",
    "\n",
    "top = men_records.head(16).with_row_index(\"rank\", offset=1)\n",
    "gt = (\n",
    "    GT(top.select(\"rank\", pl.col(\"team_id\").alias(\"logo\"), \"team\", \"conference\", \"record\", \"pct\", \"gf\", \"ga\"))\n",
    "    .tab_header(\"Men's college hockey, 2025-26\", \"The sixteen best records, NCAA tournament included\")\n",
    "    .fmt_number(\"pct\", decimals=3)\n",
    "    .cols_label(rank=\"\", logo=\"\", team=\"Team\", conference=\"Conf.\", record=\"W-L-T\", pct=\"Pct.\", gf=\"GF\", ga=\"GA\")\n",
    "    .tab_source_note(\"Data: ESPN via sportsdataverse-py\")\n",
    ")\n",
    "gt_theme_ncaa(gt_sdv_logos(gt, \"logo\", league=\"ncaa_mhockey\", height=24))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "6",
   "metadata": {},
   "source": [
    "## 3. Opponent-adjusted ratings\n",
    "\n",
    "Raw goals flatter teams with easy schedules. `college_hockey_ratings` adjusts each team's goals for and against for\n",
    "its opponents (an iterative, KenPom-style fit), in goals per game against an average team. The scoreboard also holds\n",
    "a few exhibitions against teams outside Division I; keep teams with at least ten games."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "7",
   "metadata": {
    "sdvplot_gallery": {
     "alt": "Every Division I men's team's logo by opponent-adjusted goals for and against, 2025-26",
     "title": "College hockey adjusted ratings"
    },
    "tags": [
     "gallery"
    ]
   },
   "outputs": [],
   "source": [
    "men_ratings = college_hockey_ratings(men, league=\"mch\").filter(pl.col(\"games\") >= 10)  # drops one-off exhibition foes\n",
    "fig, ax = plt.subplots(figsize=(10, 6))\n",
    "ax.scatter(men_ratings[\"adj_off\"], men_ratings[\"adj_def\"], alpha=0)\n",
    "ax.axvline(men_ratings[\"adj_off\"].mean(), color=\"grey\", lw=0.8, ls=\"--\")\n",
    "ax.axhline(men_ratings[\"adj_def\"].mean(), color=\"grey\", lw=0.8, ls=\"--\")\n",
    "ax.invert_yaxis()\n",
    "ax.margins(0.06)\n",
    "sdvplot.add_logos(\n",
    "    ax,\n",
    "    men_ratings[\"adj_off\"],\n",
    "    men_ratings[\"adj_def\"],\n",
    "    men_ratings[\"team_id\"],\n",
    "    league=\"ncaa_mhockey\",\n",
    "    season=SEASON,\n",
    "    height=0.055,\n",
    ")\n",
    "ax.set(xlabel=\"Adjusted goals for per game\", ylabel=\"Adjusted goals against per game (reversed)\")\n",
    "ax.set_title(\"Men's college hockey, opponent-adjusted, 2025-26\", loc=\"left\", fontweight=\"bold\")\n",
    "fig.text(0.99, 0.01, \"Data: ESPN via sportsdataverse-py\", ha=\"right\", fontsize=8, color=\"grey\")\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "8",
   "metadata": {},
   "source": [
    "Michigan's attack, 4.6 adjusted goals a game, was the best in the country by more than half a goal; Michigan State\n",
    "allowed the fewest."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "9",
   "metadata": {},
   "source": [
    "## 4. Conference strength, in conference colors\n",
    "\n",
    "Each team's net rating (adjusted goals for minus against) as a dot colored by its conference, one row per conference\n",
    "ordered by its average; the best team in each conference carries its logo. (HE is Hockey East, AHA Atlantic Hockey\n",
    "America and IND the independents.) The index has no school colors for college hockey yet (`color_source` is\n",
    "`\"fallback\"`), and conference colors read better here anyway."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "10",
   "metadata": {},
   "outputs": [],
   "source": [
    "from plotnine import (\n",
    "    aes,\n",
    "    element_blank,\n",
    "    element_text,\n",
    "    geom_point,\n",
    "    geom_vline,\n",
    "    ggplot,\n",
    "    labs,\n",
    "    scale_color_manual,\n",
    "    theme,\n",
    "    theme_minimal,\n",
    ")\n",
    "\n",
    "from sdvplot.plotnine import geom_sdv_logos\n",
    "\n",
    "net = men_ratings.join(conferences, on=\"team_id\").sort(\"team_id\")\n",
    "order = (\n",
    "    net.group_by(\"conference\", maintain_order=True)\n",
    "    .agg(pl.col(\"adj_net\").mean())\n",
    "    .sort(\"adj_net\")[\"conference\"]\n",
    "    .to_list()\n",
    ")\n",
    "best = net.sort(\"adj_net\", descending=True).group_by(\"conference\", maintain_order=True).first()\n",
    "frame, best_frame = net.to_pandas(), best.to_pandas()\n",
    "for f in (frame, best_frame):\n",
    "    f[\"conference\"] = f[\"conference\"].astype(\"category\").cat.set_categories(order)\n",
    "palette = dict(\n",
    "    zip(\n",
    "        order,\n",
    "        [\n",
    "            \"#4e79a7\",\n",
    "            \"#f28e2b\",\n",
    "            \"#e15759\",\n",
    "            \"#76b7b2\",\n",
    "            \"#59a14f\",\n",
    "            \"#edc948\",\n",
    "            \"#b07aa1\",\n",
    "            \"#ff9da7\",\n",
    "            \"#9c755f\",\n",
    "            \"#bab0ac\",\n",
    "            \"#86bcb6\",\n",
    "            \"#d37295\",\n",
    "        ],\n",
    "        strict=False,\n",
    "    )\n",
    ")\n",
    "(\n",
    "    ggplot(frame, aes(\"adj_net\", \"conference\"))\n",
    "    + geom_vline(xintercept=0, color=\"#bbbbbb\")\n",
    "    + geom_point(aes(color=\"conference\"), size=3.5, alpha=0.8, show_legend=False)\n",
    "    + geom_sdv_logos(aes(team=\"team_id\"), data=best_frame, league=\"ncaa_mhockey\", season=SEASON, height=0.075)\n",
    "    + scale_color_manual(values=palette)\n",
    "    + labs(\n",
    "        x=\"Net rating (adjusted goals per game)\",\n",
    "        y=\"\",\n",
    "        title=\"Conference strength, men's 2025-26\",\n",
    "        caption=\"Data: ESPN via sportsdataverse-py\",\n",
    "    )\n",
    "    + theme_minimal()\n",
    "    + theme(figure_size=(9, 5.5), panel_grid_minor=element_blank(), plot_title=element_text(weight=\"bold\"))\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "11",
   "metadata": {},
   "source": [
    "The NCHC was the strongest conference on average; the Big Ten had the best team, Michigan."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "12",
   "metadata": {},
   "source": [
    "## 5. The USCHO poll, week by week\n",
    "\n",
    "Every competitor on ESPN's scoreboard carries its poll rank at game time (`curatedRank`, 99 when unranked), so the\n",
    "weekly USCHO poll falls out of the same events. Conference tournaments give byes in March, so take the top ten from\n",
    "the last week in which all ten ranked teams played, and follow them back through the season as a bump chart."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "13",
   "metadata": {},
   "outputs": [],
   "source": [
    "weekly = (\n",
    "    pl.DataFrame(\n",
    "        [\n",
    "            {\"date\": e[\"date\"][:10], \"team_id\": c[\"team\"][\"id\"], \"rank\": c.get(\"curatedRank\", {}).get(\"current\", 99)}\n",
    "            for e in men\n",
    "            if e[\"season\"][\"type\"] == 2\n",
    "            for c in e[\"competitions\"][0][\"competitors\"]\n",
    "        ]\n",
    "    )\n",
    "    .with_columns(week=pl.col(\"date\").str.to_date().dt.truncate(\"1w\"))\n",
    "    .group_by(\"team_id\", \"week\", maintain_order=True)\n",
    "    .agg(pl.col(\"rank\").min())\n",
    ")\n",
    "full = weekly.filter(pl.col(\"rank\") <= 10).group_by(\"week\", maintain_order=True).len().filter(pl.col(\"len\") == 10)\n",
    "last_week = full[\"week\"].max()  # the last week in which all ten ranked teams played\n",
    "polls = weekly.filter((pl.col(\"rank\") <= 20) & (pl.col(\"week\") <= last_week))\n",
    "final10 = polls.filter((pl.col(\"week\") == last_week) & (pl.col(\"rank\") <= 10)).sort(\"rank\", \"team_id\")[\"team_id\"]\n",
    "fig, ax = plt.subplots(figsize=(10, 6))\n",
    "for team_id in final10:\n",
    "    t = polls.filter(pl.col(\"team_id\") == team_id).sort(\"week\")\n",
    "    ax.plot(t[\"week\"], t[\"rank\"], marker=\"o\", ms=3, lw=1.6, alpha=0.75)\n",
    "ends = polls.filter((pl.col(\"week\") == last_week) & pl.col(\"team_id\").is_in(final10)).sort(\"rank\", \"team_id\")\n",
    "sdvplot.add_logos(ax, ends[\"week\"], ends[\"rank\"], ends[\"team_id\"], league=\"ncaa_mhockey\", season=SEASON, height=0.07)\n",
    "ax.invert_yaxis()\n",
    "ax.set_yticks([1, 5, 10, 15, 20])\n",
    "ax.set_ylabel(\"USCHO poll rank\")\n",
    "ax.spines[[\"top\", \"right\"]].set_visible(False)\n",
    "ax.set_title(f\"The USCHO top ten of {last_week:%B} {last_week.day}, through the season\", loc=\"left\", fontweight=\"bold\")\n",
    "fig.text(\n",
    "    0.99,\n",
    "    0.01,\n",
    "    \"Weeks when a team was outside the top 20 are left out. Data: ESPN via sportsdataverse-py\",\n",
    "    ha=\"right\",\n",
    "    fontsize=8,\n",
    "    color=\"grey\",\n",
    ")\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "14",
   "metadata": {},
   "source": [
    "## 6. The men's NCAA tournament\n",
    "\n",
    "The sixteen-team tournament is in the same events (season type 3), with the round in each game's notes. A results\n",
    "table with two logo columns, one for the winner and one for the loser."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "15",
   "metadata": {},
   "outputs": [],
   "source": [
    "from sdvplot.great_tables import gt_theme_scoreboard\n",
    "\n",
    "games = []\n",
    "for e in men:\n",
    "    if e[\"season\"][\"type\"] != 3:\n",
    "        continue\n",
    "    comp = e[\"competitions\"][0]\n",
    "    win, lose = sorted(comp[\"competitors\"], key=lambda c: not c[\"winner\"])\n",
    "    games.append(\n",
    "        {\n",
    "            \"date\": e[\"date\"][:10],\n",
    "            \"round\": comp[\"notes\"][0][\"headline\"].replace(\"NCAA Men's Hockey \", \"\").replace(\"Championship - \", \"\"),\n",
    "            \"winner\": win[\"team\"][\"id\"],\n",
    "            \"winner_name\": win[\"team\"][\"shortDisplayName\"],\n",
    "            \"score\": f\"{win['score']}-{lose['score']}\"\n",
    "            + (\" (OT)\" if \"OT\" in e[\"status\"][\"type\"][\"shortDetail\"] else \"\"),\n",
    "            \"loser\": lose[\"team\"][\"id\"],\n",
    "            \"loser_name\": lose[\"team\"][\"shortDisplayName\"],\n",
    "        }\n",
    "    )\n",
    "bracket = pl.DataFrame(games).sort(\"date\", maintain_order=True)\n",
    "gt = (\n",
    "    GT(bracket)\n",
    "    .tab_header(\"2026 NCAA men's hockey tournament\", \"Every game, regionals to the national championship\")\n",
    "    .cols_label(\n",
    "        date=\"Date\", round=\"Round\", winner=\"\", winner_name=\"Winner\", score=\"Score\", loser=\"\", loser_name=\"Loser\"\n",
    "    )\n",
    "    .tab_source_note(\"Data: ESPN via sportsdataverse-py\")\n",
    ")\n",
    "gt_theme_scoreboard(gt_sdv_logos(gt, [\"winner\", \"loser\"], league=\"ncaa_mhockey\", height=22))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "16",
   "metadata": {},
   "source": [
    "Denver won the title, beating Wisconsin 2-1 in the final after a double-overtime semifinal against Michigan."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "17",
   "metadata": {},
   "source": [
    "## 7. Tiers of the top thirty-two\n",
    "\n",
    "The thirty-two best net ratings as a tier list, with matplotlib's `team_tiers`. Tiers are rating ranks, so the\n",
    "labels say which."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "18",
   "metadata": {},
   "outputs": [],
   "source": [
    "from sdvplot.matplotlib import team_tiers\n",
    "\n",
    "ranked = men_ratings.sort(\"adj_net\", descending=True).head(32).with_row_index(\"rank\", offset=1)\n",
    "ranked = ranked.with_columns(\n",
    "    tier_no=pl.when(pl.col(\"rank\") <= 4)\n",
    "    .then(1)\n",
    "    .when(pl.col(\"rank\") <= 10)\n",
    "    .then(2)\n",
    "    .when(pl.col(\"rank\") <= 16)\n",
    "    .then(3)\n",
    "    .when(pl.col(\"rank\") <= 24)\n",
    "    .then(4)\n",
    "    .otherwise(5)\n",
    ")\n",
    "fig = team_tiers(\n",
    "    ranked.select(pl.col(\"team_id\").alias(\"team\"), \"tier_no\"),\n",
    "    \"ncaa_mhockey\",\n",
    "    title=\"Men's college hockey by net rating, 2025-26\",\n",
    "    subtitle=\"Opponent-adjusted goals per game, from college_hockey_ratings\",\n",
    "    caption=\"Data: ESPN via sportsdataverse-py\",\n",
    "    tier_desc={1: \"1-4\", 2: \"5-10\", 3: \"11-16\", 4: \"17-24\", 5: \"25-32\"},\n",
    "    height=0.085,\n",
    ")\n",
    "fig.set_size_inches(10, 6)\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "19",
   "metadata": {},
   "source": [
    "## 8. Women's ratings: a team with no logo yet\n",
    "\n",
    "The same pipeline for the women. Every team id on ESPN's women's scoreboard resolves, the two its teams list lacks\n",
    "included: Minnesota State's women's id (24059), which the index keeps as the school's second ESPN id, and Delaware\n",
    "(48). The logo archive has no Delaware mark yet, so `logo_url` gives `None` with one warning and the logo charts\n",
    "leave Delaware out instead of drawing a stand-in."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "20",
   "metadata": {},
   "outputs": [],
   "source": [
    "names = {c[\"team\"][\"id\"]: c[\"team\"][\"displayName\"] for e in women for c in e[\"competitions\"][0][\"competitors\"]}\n",
    "keys = pl.DataFrame({\"team_id\": list(names), \"key\": sdvplot.resolve(list(names), \"ncaa_whockey\")})\n",
    "assert keys[\"key\"].null_count() == 0  # every scoreboard id resolves\n",
    "with warnings.catch_warnings(record=True) as caught:\n",
    "    warnings.simplefilter(\"always\")\n",
    "    print(sdvplot.logo_url(\"48\", \"ncaa_whockey\", season=SEASON))\n",
    "print(caught[0].message)\n",
    "women_ratings = college_hockey_ratings(women, league=\"wch\").filter(pl.col(\"games\") >= 10).join(keys, on=\"team_id\")\n",
    "women_ratings.filter(pl.col(\"team_id\").is_in([\"24059\", \"48\"])).select(\"team_id\", \"key\", \"adj_net\", \"games\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "21",
   "metadata": {
    "sdvplot_gallery": {
     "alt": "The top fifteen Division I women's teams by net rating, 2025-26, logos on the axis",
     "title": "Women's college hockey ratings"
    },
    "tags": [
     "gallery"
    ]
   },
   "outputs": [],
   "source": [
    "from plotnine import coord_flip, geom_col\n",
    "\n",
    "top15 = women_ratings.sort(\"adj_net\", descending=True).head(15)\n",
    "frame = top15.to_pandas()\n",
    "frame[\"key\"] = frame[\"key\"].astype(\"category\").cat.set_categories(top15[\"key\"].reverse().to_list())\n",
    "p = (\n",
    "    ggplot(frame, aes(\"key\", \"adj_net\"))\n",
    "    + geom_col(fill=\"#7a1c3c\", width=0.7)\n",
    "    + coord_flip()\n",
    "    + labs(\n",
    "        x=\"\",\n",
    "        y=\"Net rating (adjusted goals per game)\",\n",
    "        title=\"Women's college hockey, top fifteen, 2025-26\",\n",
    "        caption=\"Data: ESPN via sportsdataverse-py\",\n",
    "    )\n",
    "    + theme_minimal()\n",
    "    + theme(figure_size=(8, 6), plot_title=element_text(weight=\"bold\"))\n",
    ")\n",
    "sdvplot.axis_logos(p, \"y\", league=\"ncaa_whockey\", season=SEASON, height=0.055)  # \"y\": the axis as drawn, after the flip"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "22",
   "metadata": {},
   "source": [
    "## 9. Women's scoring, interactive\n",
    "\n",
    "Goals for and against per game for every women's team in Plotly, logos as the points; hover for the record."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "23",
   "metadata": {},
   "outputs": [],
   "source": [
    "import plotly.graph_objects as go\n",
    "\n",
    "w = records(women, \"wch\").join(keys.rename({\"key\": \"logo\"}), on=\"team_id\", maintain_order=\"left\")\n",
    "w = w.with_columns(gf_pg=pl.col(\"gf\") / pl.col(\"gp\"), ga_pg=pl.col(\"ga\") / pl.col(\"gp\"))\n",
    "fig = go.Figure(\n",
    "    go.Scatter(\n",
    "        x=w[\"gf_pg\"],\n",
    "        y=w[\"ga_pg\"],\n",
    "        mode=\"markers\",\n",
    "        marker={\"opacity\": 0},\n",
    "        text=w[\"team\"],\n",
    "        customdata=w[\"record\"],\n",
    "        hovertemplate=\"%{text}<br>%{customdata}<br>%{x:.2f} for, %{y:.2f} against<extra></extra>\",\n",
    "    )\n",
    ")\n",
    "fig = sdvplot.add_logos(fig, w[\"gf_pg\"], w[\"ga_pg\"], w[\"logo\"], league=\"ncaa_whockey\", season=SEASON, height=0.07)\n",
    "fig.update_layout(\n",
    "    title=\"Women's college hockey: goals for and against per game, 2025-26\",\n",
    "    template=\"plotly_white\",\n",
    "    xaxis_title=\"Goals for per game\",\n",
    "    yaxis={\"title\": \"Goals against per game (reversed)\", \"autorange\": \"reversed\"},\n",
    "    width=800,\n",
    "    height=560,\n",
    ")\n",
    "fig"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "24",
   "metadata": {},
   "source": [
    "Wisconsin, the women's national champion, beat Ohio State 3-2 in the final."
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "sdvplot": {
   "description": "NCAA men's and women's hockey from ESPN's scoreboard: records, opponent-adjusted ratings, conference strength, the USCHO poll week by week, the men's tournament and women's ratings.",
   "label": "College hockey",
   "position": 43
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
