π The PWHL with sportsdataverse-py
Welcome to professional women's hockey! The Professional Women's Hockey League (PWHL) dropped its first puck in January 2024 with six clubs β Boston, Minnesota, MontrΓ©al, New York, Ottawa and Toronto β and it's been must-watch hockey ever since. π
sportsdataverse.pwhl gives you the whole league two ways:
- π¦
load_pwhl_*release loaders β fast, reliable parquet snapshots (schedules, boxscores, play-by-play, scoring & penalty summaries, rosters). Perfect for season-long analysis, and they work great offline. - π°οΈ
pwhl_*live wrappers + analytics β straight off the HockeyTech stats feed (standings, leaders, rosters, stats, single-game PBP) plus derived on-ice metrics (Corsi, time-on-ice, shifts).
And the best part: no API key needed β the public HockeyTech client key ships with the package. R companion: fastRhockey. Let's drop the puck! π₯
π§° The toolboxβ
Everything returns a tidy polars DataFrame by default β pass return_as_pandas=True for pandas. The π¦ loaders read pre-built release parquets (one season per call); the π°οΈ live wrappers hit the HockeyTech API in real time. Both are premium PWHL sources. Click any name for the full reference:
| Function | What it gives you | Source |
|---|---|---|
load_pwhl_schedule | Games + results, one row per game | π¦ loader |
load_pwhl_rosters | One row per player per team (skaters + goalies) | π¦ loader |
load_pwhl_skater_box | Skater boxscore, one row per player per game | π¦ loader |
load_pwhl_goalie_box | Goalie boxscore (saves, shots against, GAA inputs) | π¦ loader |
load_pwhl_team_box | Team boxscore (shots, PP, faceoffs) | π¦ loader |
load_pwhl_pbp | Event-level play-by-play (wide, with coordinates) | π¦ loader |
load_pwhl_scoring_summary | Tidy goal log (scorer + assists + situation flags) | π¦ loader |
load_pwhl_penalty_summary | Tidy penalty log (infraction, minutes, who took it) | π¦ loader |
load_pwhl_shots_by_period | Per-period shot & goal totals per game | π¦ loader |
load_pwhl_three_stars | Post-game three-star selections | π¦ loader |
pwhl_schedule | Live schedule, one row per game | π°οΈ live |
pwhl_standings | Live standings, one row per team | π°οΈ live |
pwhl_teams | Teams in a season (grab team_ids) | π°οΈ live |
pwhl_team_roster | A team's roster | π°οΈ live |
pwhl_leaders | Statistical leaders | π°οΈ live |
pwhl_stats | Aggregate skater/goalie stats | π°οΈ live |
pwhl_player_search | Find a player_id by name | π°οΈ live |
pwhl_player_stats | A player's season-by-season stat lines | π°οΈ live |
pwhl_pbp | Enriched single-game play-by-play | π°οΈ live |
pwhl_game_corsi | On-ice Corsi / Fenwick per player | π°οΈ live |
pwhl_player_toi | Time-on-ice per player | π°οΈ live |
pwhl_game_shifts | Raw shift stints | π°οΈ live |
most_recent_pwhl_season Β· pwhl_season_id | Season helpers | π°οΈ live |
π Setupβ
pip install sportsdataverse
No key, no config β just import and go.
import polars as pl
import sportsdataverse.pwhl as pwhl
# The inaugural season is 2024; this helper tracks the latest known season.
print("most recent PWHL season:", pwhl.most_recent_pwhl_season())
most recent PWHL season: 2027
The π°οΈ live HockeyTech feed is seasonal and occasionally rate-limited, so a tiny safe() helper runs those calls defensively β you get the frame when the feed is up, and a friendly one-liner when it isn't (never a scary traceback). The π¦ loaders read release parquets and are rock-solid, so they don't need the wrapper. π
def safe(label, thunk):
try:
out = thunk()
print(f"β
{label}")
return out
except Exception as e: # noqa: BLE001 -- demo resilience
print(f"βοΈ {label}: unavailable right now ({type(e).__name__})")
return None
π The schedule (loader)β
load_pwhl_schedule returns one row per game with the result and a set of flag/URL columns pointing at the per-game feeds. Pass seasons=[2024] (a list β you can stack multiple seasons). β οΈ Heads up: home_score/away_score come back as strings, so cast them before doing arithmetic.
schedule = pwhl.load_pwhl_schedule(seasons=[2024])
schedule.shape
(85, 29)
schedule.select([
'game_id', 'game_date', 'home_team', 'away_team',
'home_score', 'away_score', 'winner', 'game_type',
]).head()
shape: (5, 8)
βββββββββββ¬ββββββββββββββ¬ββββββββββββ¬ββββββββββββ¬βββββββββββββ¬βββββββββββββ¬ββββββββββββ¬ββββββββββββ
β game_id β game_date β home_team β away_team β home_score β away_score β winner β game_type β
β --- β --- β --- β --- β --- β --- β --- β --- β
β str β str β str β str β str β str β str β str β
βββββββββββͺββββββββββββββͺββββββββββββͺββββββββββββͺβββββββββββββͺβββββββββββββͺββββββββββββͺββββββββββββ‘
β 84 β Wed, May 8 β Toronto β Minnesota β 4 β 0 β Toronto β playoffs β
β 98 β Wed, May 29 β Boston β Minnesota β 0 β 3 β Minnesota β playoffs β
β 90 β Wed, May 15 β Minnesota β Toronto β 1 β 0 β Minnesota β playoffs β
β 63 β Wed, May 1 β Toronto β Minnesota β 4 β 1 β Toronto β regular β
β 45 β Wed, Mar 6 β Toronto β Boston β 3 β 1 β Toronto β regular β
βββββββββββ΄ββββββββββββββ΄ββββββββββββ΄ββββββββ ββββ΄βββββββββββββ΄βββββββββββββ΄ββββββββββββ΄ββββββββββββ
π₯ Rosters (loader)β
load_pwhl_rosters gives one row per player per team, split into skaters and goalies via the player_type column.
rosters = pwhl.load_pwhl_rosters(seasons=[2024])
rosters.select([
'team', 'team_abbr', 'player_type', 'first_name', 'last_name',
'jersey_number', 'position',
]).head()
shape: (5, 7)
ββββββββββββββββ¬ββββββββββββ¬ββββββββββββββ¬βββββββββββββ¬ββββββββββββ¬ββββββββββββββββ¬βββββββββββ
β team β team_abbr β player_type β first_name β last_name β jersey_number β position β
β --- β --- β --- β --- β --- β --- β --- β
β str β str β str β str β str β i32 β str β
ββββββββββββββββͺββββββββββββͺββββββββββββββͺβββββββββββββͺββββββββββββͺββββββββββββββββͺβββββββββββ‘
β PWHL Toronto β TOR β skater β Jocelyne β Larocque β 3 β LD β
β PWHL Toronto β TOR β skater β Lauriane β Rougeau β 5 β LD β
β PWHL Toronto β TOR β skater β Kali β Flanagan β 6 β RD β
β PWHL Toronto β TOR β skater β Olivia β Knowles β 7 β RD β
β PWHL Toronto β TOR β skater β Alexa β Vasko β 10 β C β
ββββββββββββββββ΄ββββββββββββ΄ββββββββββββββ΄βββββββββββββ΄ββββββββββββ΄ββββββββββββββββ΄βββββββββββ
π Boxscores (loader)β
Boxscores come in three flavours β team_box, skater_box, and goalie_box β each one row per team/player per game.
| Function | One row per⦠|
|---|---|
load_pwhl_team_box | team per game |
load_pwhl_skater_box | skater per game |
load_pwhl_goalie_box | goalie per game |
skater_box = pwhl.load_pwhl_skater_box(seasons=[2024])
skater_box.select([
'game_id', 'first_name', 'last_name', 'position',
'goals', 'assists', 'points', 'shots', 'plus_minus', 'time_on_ice',
]).head()
shape: (5, 10)
βββββββββββ¬βββββββββββββ¬ββββββββββββ¬βββββββββββ¬ββββ¬βββββββββ¬ββββββββ¬βββββββββββββ¬ββββββββββββββ
β game_id β first_name β last_name β position β β¦ β points β shots β plus_minus β time_on_ice β
β --- β --- β --- β --- β β --- β --- β --- β --- β
β i32 β str β str β str β β i32 β i32 β i32 β f64 β
βββββββββββͺβββββββββββββͺββββββββββββͺβββββββββββͺββββͺβββββββββͺββββββββͺβββββββββββββͺββββββββββββββ‘
β 2 β Jocelyne β Larocque β LD β β¦ β 0 β 2 β -2 β 26.7 β
β 2 β Lauriane β Rougeau β LD β β¦ β 0 β 0 β 0 β 12.1 β
β 2 β Kali β Flanagan β RD β β¦ β 0 β 1 β -1 β 21.6 β
β 2 β Olivia β Knowles β RD β β¦ β 0 β 0 β 0 β 9.7 β
β 2 β Alexa β Vasko β C β β¦ β 0 β 3 β 0 β 10.5 β
βββββββββββ΄βββββββββββββ΄ββββββββββββ΄βββββββββββ΄ββββ΄βββββββββ΄ββββββββ΄βββββββββββββ΄ββββββββββββββ
goalie_box = pwhl.load_pwhl_goalie_box(seasons=[2024])
goalie_box.select([
'game_id', 'first_name', 'last_name',
'saves', 'shots_against', 'goals_against', 'time_on_ice',
]).head()
shape: (5, 7)
βββββββββββ¬βββββββββββββ¬βββββββββββββ¬ββββββββ¬ββββββββββββββββ¬ββββββββββββββββ¬ββββββββββββββ
β game_id β first_name β last_name β saves β shots_against β goals_against β time_on_ice β
β --- β --- β --- β --- β --- β --- β --- β
β i32 β str β str β i32 β i32 β i32 β f64 β
βββββββββββͺβββββββββββββͺβββββββββββββͺββββββββͺββββββββββββββββͺββββββββββββββββͺββββββββββββββ‘
β 2 β Erica β Howe β 0 β 0 β 0 β null β
β 2 β Kristen β Campbell β 24 β 28 β 4 β 60.0 β
β 2 β Corinne β Schroeder β 29 β 29 β 0 β 60.0 β
β 2 β Abbey β Levy β 0 β 0 β 0 β null β
β 3 β Sandra β Abstreiter β 0 β 0 β 0 β null β
βββββββββββ΄βββββββββββββ΄βββββββββββββ΄ββββββββ΄ββββββββββββββββ΄ββββββββββββββββ΄ββββββββββββββ
π¬ Play-by-play (loader)β
load_pwhl_pbp returns a wide event log. The event column tags each row as faceoff, shot, goal, or penalty β and there are several coordinate systems (x_coord/y_coord plus rink-normalized *_fixed / *_right variants) for drawing rink plots.
pbp = pwhl.load_pwhl_pbp(seasons=[2024])
pbp.shape
(10456, 95)
(pbp
.group_by('event')
.agg(pl.len().alias('events'))
.sort('events', descending=True))
shape: (4, 2)
βββββββββββ¬βββββββββ
β event β events β
β --- β --- β
β str β u32 β
βββββββββββͺβββββββββ‘
β shot β 4922 β
β faceoff β 4631 β
β penalty β 518 β
β goal β 385 β
βββββββββββ΄βββββββββ