# chickenstats library and utilities
from chickenstats.chicken_nhl import Scraper, Season
from chickenstats.chicken_nhl.info import NHL_COLORS
from chickenstats.chicken_nhl.helpers import charts_directory
# plotting library and utilities
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import seaborn as sns
# miscellaneous utilities
from pathlib import Path2025 after 15 games
nashville predators
Recap of the first 15 games of the Predators’ 2025 season
Housekeeping
Import dependencies and set options
# set pandas options so that there are no columns abbreviated
pd.set_option("display.max_columns", None)Create directory for charts
charts_directory()chickenstats matplotlib styles
plt.style.use("chickenstats") # this is available when you import chickenstats.utilitiesScraping and prepping data
Scraping schedule and game IDs
season = Season(2025)schedule = season.schedule()condition = schedule.game_state == "OFF"
game_ids = schedule.loc[condition].game_id.tolist()
latest_date = schedule.loc[condition].game_date.iloc[-1]Scraping play-by-play data
scraper = Scraper(game_ids)play_by_play = scraper.play_by_playPrepping the stats dataframes
game_stats = scraper.statsscraper.prep_stats(level="season")
season_stats = scraper.statsPlotting data
Filter conditions and data to plot
# Setting filter conditions and filtering data
team = "NSH"
toi_min = 5
strength_states = ["5v5"]
positions = ["F", "C", "L", "R", "L/R", "R/L", "C/R", "C/L"]
conds = np.logical_and.reduce(
[
season_stats.strength_state.isin(strength_states),
season_stats.position.isin(positions),
season_stats.toi >= toi_min,
]
)
plot_stats = season_stats.loc[conds].sort_values(by="xgf_percent", ascending=False).reset_index(drop=True)Setting plot colors based on chosen team
colors = NHL_COLORS[team]Plotting individual xGF and xGA values
# Setting overall figures
fig, ax = plt.subplots(dpi=650, figsize=(8, 5))
# Aesthetics, likes the tight layout and despining axes
fig.tight_layout()
sns.despine()
# Getting the averages and drawing the average lines
xga_mean = plot_stats.xga_p60.mean()
xgf_mean = plot_stats.xgf_p60.mean()
ax.axvline(x=xga_mean, zorder=-1, alpha=0.5)
ax.axhline(y=xgf_mean, zorder=-1, alpha=0.5)
# Setting the size norm so bubbles are consistent across figures
size_norm = (plot_stats.toi.min(), plot_stats.toi.max())
# Filtering data and plotting the non-selected teams first
conds = plot_stats.team != team
plot_data = plot_stats.loc[conds]
# They all get gray colors
facecolor = colors["MISS"]
edgecolor = colors["MISS"]
# Plotting the non-selected teams' data
sns.scatterplot(
data=plot_data,
x="xga_p60",
y="xgf_p60",
size="toi",
sizes=(20, 150),
size_norm=size_norm,
lw=1.5,
facecolor=facecolor,
edgecolor=edgecolor,
alpha=0.5,
legend=True,
)
# Filtering the data and plotting the selected team
conds = plot_stats.team == team
plot_data = plot_stats.loc[conds]
# Setting the colors
facecolor = colors["GOAL"]
edgecolor = colors["SHOT"]
# Plotting the selected teams' data
sns.scatterplot(
data=plot_data,
x="xga_p60",
y="xgf_p60",
size="toi",
sizes=(20, 150),
size_norm=size_norm,
lw=1.5,
facecolor=facecolor,
edgecolor=edgecolor,
alpha=0.8,
legend=False,
)
# Iterating through the dataframe to label the bubbles
for row, player in plot_data.iterrows():
# Setting x and y positions that are slightly offset from the data they point to
x_position = player.xga_p60 + 0.25
y_position = player.xgf_p60 - 0.25
# Annotation options
arrow_props = {"arrowstyle": "simple", "linewidth": 0.25, "color": "tab:gray"}
# Plotting the annotation
ax.annotate(
text=f"{player.player}",
xy=(player.xga_p60, player.xgf_p60),
xytext=(x_position, y_position),
fontsize=6,
bbox={"facecolor": "white", "alpha": 0.5, "edgecolor": "white", "pad": 0},
arrowprops=arrow_props,
)
# Setting axis lables
ax.axes.set_xlabel("xGA per 60 minutes")
ax.axes.set_ylabel("xGF per 60 minutes")
# Setting figure suptitle and subtitle
fig_suptitle = "Predators forwards are generating and allowing all kinds of 5v5 offense"
fig.suptitle(fig_suptitle, x=0.01, y=1.08, fontsize=11, fontweight="bold", horizontalalignment="left")
subtitle = f"NHL on-ice chances created and allowed | >{toi_min} min. TOI at 5v5 | 2024-25 season, as of {latest_date}"
fig.text(s=subtitle, x=0.01, y=1.02, fontsize=10, horizontalalignment="left")
# Attribution
attribution = "Data & xG model @chickenandstats | Viz @chickenandstats"
fig.text(s=attribution, x=0.99, y=-0.05, fontsize=8, horizontalalignment="right", style="italic")
# Save figure
savepath = Path(f"./charts/5v5_xgf_xga_{team}.png")
fig.savefig(savepath, transparent=False, bbox_inches="tight")xGF and GF
# Setting overall figures
fig, ax = plt.subplots(dpi=650, figsize=(8, 5))
# Aesthetics, likes the tight tight layout and despining axes
fig.tight_layout()
sns.despine()
# Getting the averages and drawing the average lines
gf_mean = plot_stats.gf_p60.mean()
xgf_mean = plot_stats.xgf_p60.mean()
ax.axhline(y=gf_mean, zorder=-1, alpha=0.5)
ax.axvline(x=xgf_mean, zorder=-1, alpha=0.5)
# Setting the size norm so bubbles are consistent across figures
size_norm = (plot_stats.toi.min(), plot_stats.toi.max())
# Getting plot colors based on team
colors = NHL_COLORS[team]
# Filtering data and plotting the non-selected teams first
conds = plot_stats.team != team
plot_data = plot_stats.loc[conds]
# They all get gray colors
facecolor = colors["MISS"]
edgecolor = colors["MISS"]
# Plotting the non-selected teams' data
sns.scatterplot(
data=plot_data,
x="xgf_p60",
y="gf_p60",
size="toi",
sizes=(20, 150),
size_norm=size_norm,
lw=1.5,
facecolor=facecolor,
edgecolor=edgecolor,
alpha=0.5,
legend=True,
)
# Filtering and plotting the selected teams' data
conds = plot_stats.team == team
plot_data = plot_stats.loc[conds]
# Setting the colors
facecolor = colors["GOAL"]
edgecolor = colors["SHOT"]
# Plotting the selected team's data
sns.scatterplot(
data=plot_data,
x="xgf_p60",
y="gf_p60",
size="toi",
sizes=(20, 150),
size_norm=size_norm,
lw=1.5,
facecolor=facecolor,
edgecolor=edgecolor,
alpha=0.8,
legend=False,
)
# Iterating through the dataframe to label the bubbles
for row, player in plot_data.iterrows():
# Setting x and y positions that are slightly offset from the data they point to
x_position = player.xgf_p60 + 0.25
y_position = player.gf_p60 - 0.25
# Annotation options
arrow_props = {"arrowstyle": "simple", "linewidth": 0.25, "color": "tab:gray"}
# Plotting the annotation
ax.annotate(
text=f"{player.player}",
xy=(player.xgf_p60, player.gf_p60),
xytext=(x_position, y_position),
fontsize=6,
bbox={"facecolor": "white", "alpha": 0.5, "edgecolor": "white", "pad": 0},
arrowprops=arrow_props,
)
# Setting x and y axes labels
ax.axes.set_xlabel("GF per 60 minutes")
ax.axes.set_ylabel("xGF per 60 minutes")
# Figure suptitle and subtitle
fig_suptitle = "Top Predators forwards are converting on limited 5v5 chances"
fig.suptitle(fig_suptitle, x=0.01, y=1.08, fontsize=11, fontweight="bold", horizontalalignment="left")
subtitle = f"NHL individual chances created and goals scored | >{toi_min} min. TOI at 5v5 | 2024-25 season, as of {latest_date}"
fig.text(s=subtitle, x=0.01, y=1.02, fontsize=10, horizontalalignment="left")
# Figure attribution
attribution = "Data & xG model @chickenandstats | Viz @chickenandstats"
fig.text(s=attribution, x=0.99, y=-0.05, fontsize=8, horizontalalignment="right", style="italic")
# Save figure
savepath = Path(f"./charts/5v5_xgf_gf_{team}.png")
fig.savefig(savepath, transparent=False, bbox_inches="tight")

