Hockey Hat Tricks

python
API
visualization
sports
hockey
Author

Dan MacGuigan

Published

February 22, 2024

In hockey (and some other sports), a hat trick is three goals scored by the same player in a single game. How common are hat tricks in the NHL?

We’ll again turn to the NHL’s stats database, with API documentation help from here and here.

First, let’s just look at the 2022-2023 season for one team. How about the Buffalo Sabres? We’ll need to loop through every Sabres game from this season and find games with hat tricks.

Let’s look at the data structure. Here’s the first Sabres game that season.

import requests
import json

team = "BUF"
season = 20222023
link = "https://api-web.nhle.com/v1/club-schedule-season/" + str(team) + "/" + str(season)
print(link)
sched = json.loads(requests.get(link).text) # pull request, get data (.text), and convert JSON to a Python dict
game1 = sched["games"][0] # look at the first game
print(json.dumps(game1, indent=4)) # print the new dictionary object
print("")
game1_date = game1["gameDate"]
print("The date of the Sabres first game this season is: " + str(game1_date))
game1_ID = game1["id"]
print("The ID of the Sabres first game this season is: " + str(game1_ID))
https://api-web.nhle.com/v1/club-schedule-season/BUF/20222023
{
    "id": 2022010006,
    "season": 20222023,
    "gameType": 1,
    "gameDate": "2022-09-25",
    "venue": {
        "default": "Capital One Arena"
    },
    "neutralSite": false,
    "startTimeUTC": "2022-09-25T18:00:00Z",
    "easternUTCOffset": "-04:00",
    "venueUTCOffset": "-04:00",
    "venueTimezone": "US/Eastern",
    "gameState": "FINAL",
    "gameScheduleState": "OK",
    "tvBroadcasts": [
        {
            "id": 320,
            "market": "H",
            "countryCode": "US",
            "network": "NBCSWA",
            "sequenceNumber": 1
        },
        {
            "id": 324,
            "market": "N",
            "countryCode": "US",
            "network": "NHLN",
            "sequenceNumber": 1
        }
    ],
    "awayTeam": {
        "id": 7,
        "placeName": {
            "default": "Buffalo"
        },
        "abbrev": "BUF",
        "logo": "https://assets.nhle.com/logos/nhl/svg/BUF_light.svg",
        "darkLogo": "https://assets.nhle.com/logos/nhl/svg/BUF_dark.svg",
        "awaySplitSquad": false,
        "score": 4
    },
    "homeTeam": {
        "id": 15,
        "placeName": {
            "default": "Washington"
        },
        "abbrev": "WSH",
        "logo": "https://assets.nhle.com/logos/nhl/svg/WSH_light.svg",
        "darkLogo": "https://assets.nhle.com/logos/nhl/svg/WSH_dark.svg",
        "homeSplitSquad": false,
        "score": 3
    },
    "periodDescriptor": {
        "periodType": "OT"
    },
    "gameOutcome": {
        "lastPeriodType": "OT"
    },
    "winningGoalie": {
        "playerId": 8480045,
        "firstInitial": {
            "default": "U."
        },
        "lastName": {
            "default": "Luukkonen"
        }
    },
    "winningGoalScorer": {
        "playerId": 8476994,
        "firstInitial": {
            "default": "V."
        },
        "lastName": {
            "default": "Hinostroza"
        }
    },
    "gameCenterLink": "/gamecenter/buf-vs-wsh/2022/09/25/2022010006"
}

The date of the Sabres first game this season is: 2022-09-25
The ID of the Sabres first game this season is: 2022010006


Unfortunately, this data structure doesn’t show us the goals scored by each player. To find that, we’ll need to cross reference a different database.

link = "https://api-web.nhle.com/v1/score/" + str(game1_date)
box_scores = json.loads(requests.get(link).text)["games"] # pull request, get data (.text), and convert JSON to a Python dict

game1_score = next(item for item in box_scores if item['id'] == game1_ID) # find the Sabres game using the game ID from our previous bit of code

print(json.dumps(game1_score, indent=4)) # print the new dictionary object
{
    "id": 2022010006,
    "season": 20222023,
    "gameType": 1,
    "gameDate": "2022-09-25",
    "venue": {
        "default": "Capital One Arena"
    },
    "startTimeUTC": "2022-09-25T18:00:00Z",
    "easternUTCOffset": "-04:00",
    "venueUTCOffset": "-04:00",
    "tvBroadcasts": [
        {
            "id": 320,
            "market": "H",
            "countryCode": "US",
            "network": "NBCSWA",
            "sequenceNumber": 1
        },
        {
            "id": 324,
            "market": "N",
            "countryCode": "US",
            "network": "NHLN",
            "sequenceNumber": 1
        }
    ],
    "gameState": "FINAL",
    "gameScheduleState": "OK",
    "awayTeam": {
        "id": 7,
        "name": {
            "default": "Sabres"
        },
        "abbrev": "BUF",
        "score": 4,
        "sog": 28,
        "logo": "https://assets.nhle.com/logos/nhl/svg/BUF_light.svg"
    },
    "homeTeam": {
        "id": 15,
        "name": {
            "default": "Capitals"
        },
        "abbrev": "WSH",
        "score": 3,
        "sog": 27,
        "logo": "https://assets.nhle.com/logos/nhl/svg/WSH_light.svg"
    },
    "gameCenterLink": "/gamecenter/buf-vs-wsh/2022/09/25/2022010006",
    "clock": {
        "timeRemaining": "03:45",
        "secondsRemaining": 225,
        "running": false,
        "inIntermission": false
    },
    "neutralSite": false,
    "venueTimezone": "US/Eastern",
    "period": 4,
    "periodDescriptor": {
        "number": 4,
        "periodType": "OT"
    },
    "gameOutcome": {
        "lastPeriodType": "OT",
        "otPeriods": 1
    },
    "goals": [
        {
            "period": 1,
            "periodDescriptor": {
                "number": 1,
                "periodType": "REG"
            },
            "timeInPeriod": "04:05",
            "playerId": 8477511,
            "name": {
                "default": "A. Mantha"
            },
            "mugshot": "https://assets.nhle.com/mugs/nhl/20222023/WSH/8477511.png",
            "teamAbbrev": "WSH",
            "goalsToDate": 1,
            "awayScore": 0,
            "homeScore": 1,
            "strength": "PP",
            "highlightClip": 6335818566112
        },
        {
            "period": 2,
            "periodDescriptor": {
                "number": 2,
                "periodType": "REG"
            },
            "timeInPeriod": "04:25",
            "playerId": 8481528,
            "name": {
                "default": "D. Cozens"
            },
            "mugshot": "https://assets.nhle.com/mugs/nhl/20222023/BUF/8481528.png",
            "teamAbbrev": "BUF",
            "goalsToDate": 1,
            "awayScore": 1,
            "homeScore": 1,
            "strength": "PP",
            "highlightClip": 6335819727112
        },
        {
            "period": 2,
            "periodDescriptor": {
                "number": 2,
                "periodType": "REG"
            },
            "timeInPeriod": "09:16",
            "playerId": 8482896,
            "name": {
                "default": "T. Kozak"
            },
            "mugshot": "https://assets.nhle.com/mugs/nhl/20222023/BUF/8482896.png",
            "teamAbbrev": "BUF",
            "goalsToDate": 1,
            "awayScore": 2,
            "homeScore": 1,
            "strength": "EV",
            "highlightClip": 6335819725112
        },
        {
            "period": 2,
            "periodDescriptor": {
                "number": 2,
                "periodType": "REG"
            },
            "timeInPeriod": "09:42",
            "playerId": 8477839,
            "name": {
                "default": "C. Sheary"
            },
            "mugshot": "https://assets.nhle.com/mugs/nhl/20222023/WSH/8477839.png",
            "teamAbbrev": "WSH",
            "goalsToDate": 1,
            "awayScore": 2,
            "homeScore": 2,
            "strength": "EV",
            "highlightClip": 6335818367112
        },
        {
            "period": 3,
            "periodDescriptor": {
                "number": 3,
                "periodType": "REG"
            },
            "timeInPeriod": "05:43",
            "playerId": 8481441,
            "name": {
                "default": "J. Snively"
            },
            "mugshot": "https://assets.nhle.com/mugs/nhl/20222023/WSH/8481441.png",
            "teamAbbrev": "WSH",
            "goalsToDate": 1,
            "awayScore": 2,
            "homeScore": 3,
            "strength": "EV",
            "highlightClip": 6335819254112
        },
        {
            "period": 3,
            "periodDescriptor": {
                "number": 3,
                "periodType": "REG"
            },
            "timeInPeriod": "18:55",
            "playerId": 8482097,
            "name": {
                "default": "J. Quinn"
            },
            "mugshot": "https://assets.nhle.com/mugs/nhl/20222023/BUF/8482097.png",
            "teamAbbrev": "BUF",
            "goalsToDate": 1,
            "awayScore": 3,
            "homeScore": 3,
            "strength": "PP",
            "highlightClip": 6335820218112
        },
        {
            "period": 4,
            "periodDescriptor": {
                "number": 4,
                "periodType": "OT"
            },
            "timeInPeriod": "01:15",
            "playerId": 8476994,
            "name": {
                "default": "V. Hinostroza"
            },
            "mugshot": "https://assets.nhle.com/mugs/nhl/20222023/BUF/8476994.png",
            "teamAbbrev": "BUF",
            "goalsToDate": 1,
            "awayScore": 4,
            "homeScore": 3,
            "strength": "EV",
            "highlightClip": 6335820905112
        }
    ]
}


This data structure has info on each goal scored this game. Let’s figure out a way to tally the number of goals scored by each player. We’ll use the Counter function from the collections library.

import collections

scorers = [i["playerId"] for i in game1_score['goals']] # extract player IDs for each goal scored
scorers_table = dict(collections.Counter(scorers)) # count how many goals are scored by each player

print("goals scored by player ID")
print(scorers_table)
goals scored by player ID
{8477511: 1, 8481528: 1, 8482896: 1, 8477839: 1, 8481441: 1, 8482097: 1, 8476994: 1}


Now that we’ve got the basics, we can set up a loop to go through all Sabres games in the 2022-2023 season.

team = "BUF"
season = 20222023

link = "https://api-web.nhle.com/v1/club-schedule-season/" + str(team) + "/" + str(season)
sched = json.loads(requests.get(link).text) # pull request, get data (.text), and convert JSON to a Python dict

print(team + " games with hat tricks...")
print("date:{{player_ID: [number_of_goals, player_name]}}")
for game in sched["games"]: # loop through every focal team game in the schedule
  gameDATE = game["gameDate"]
  gameID = game["id"]

  link2 = "https://api-web.nhle.com/v1/score/" + str(gameDATE)
  box_scores = json.loads(requests.get(link2).text)["games"] # pull request, get data (.text), and convert JSON to a Python dict
  score = next(item for item in box_scores if item['id'] == gameID) # find the game using the game ID from our previous bit of code

  team_goals = [d for d in score['goals'] if d['teamAbbrev'] in team] # only get goals scored by the team

  scorers = [i["playerId"] for i in team_goals] # extract player IDs for each goal scored
  scorers_table = dict(collections.Counter(scorers))  

  hat_tricks_scorers = dict((k, v) for k, v in scorers_table.items() if v >= 3) # get scorers with more than 3 goals

  for key in hat_tricks_scorers: # loop through hat trick scorers
    player = next(item for item in team_goals if item['playerId']== key)['name']['default'] # get player name based on player ID
    hat_tricks_scorers[key] = [hat_tricks_scorers[key]] # convert dict value to list
    hat_tricks_scorers[key] += [player] # add player name as a value to player ID key

  if bool(hat_tricks_scorers): # only return results if there was a hat trick
    print(gameDATE + ":" + str(hat_tricks_scorers))
BUF games with hat tricks...
date:{{player_ID: [number_of_goals, player_name]}}
2022-10-20:{8477949: [3, 'A. Tuch']}
2022-10-31:{8479420: [3, 'T. Thompson']}
2022-12-07:{8479420: [5, 'T. Thompson']}
2022-12-29:{8473449: [3, 'K. Okposo']}
2023-01-03:{8479420: [3, 'T. Thompson']}
2023-02-23:{8479420: [3, 'T. Thompson']}
2023-02-26:{8481528: [3, 'D. Cozens']}
2023-04-01:{8477949: [3, 'A. Tuch']}


Tage Thompson had 4 hat tricks that season, impressive!

We can expand this to look at all teams in the 2022-2023 season. It will take a little while to run this code. We’ll use the datetime and dateutil libraries to loop through every day in that season. Also, let’s only look at regular season games ("gameType": 2). Lastly, we also need to ignore shootout goals (encoded by the periodDescriptor value), since these aren’t counted toward a hat trick.

This code will take a few minutes to run.

from datetime import date, timedelta
import dateutil.parser as dateparser

season = 20222023

# get start and end date of season
standings = json.loads(requests.get("https://api-web.nhle.com/v1/standings-season").text)["seasons"] # pull request, get data (.text), and convert JSON to a Python dict

start_date = dateparser.parse(next(item for item in standings if item["id"] == season)["standingsStart"])
end_date = dateparser.parse(next(item for item in standings if item["id"] == season)["standingsEnd"])

delta = timedelta(days=1)

all_hat_tricks = {}

while start_date.date() <= end_date.date():
    date = start_date.strftime("%Y-%m-%d")

    link2 = "https://api-web.nhle.com/v1/score/" + str(date)
    box_scores = json.loads(requests.get(link2).text)["games"] # pull request, get data (.text), and convert JSON to a Python dict

    hat_tricks_scorers_date = {} # empty dictionary to contain all hat trick scorers on this date

    if bool(box_scores): # only look at dates with games

      for game in box_scores: # loop through each game on each date
        if game['gameType'] == 2: # only look at regular season games
          goals = [d for d in game["goals"] if d['periodDescriptor']['periodType'] != "SO"] # ignore shootout goals
          scorers = [i["playerId"] for i in goals] # extract player IDs for each goal scored
          scorers_table = dict(collections.Counter(scorers))  
          hat_tricks_scorers = dict((k, v) for k, v in scorers_table.items() if v >= 3) # get scorers with more than 3 goals
          if bool(hat_tricks_scorers): # if there was a hat trick in this game
            for key in hat_tricks_scorers: # loop through hat trick scorers
              player = next(item for item in goals if item['playerId'] == key)['name']['default'] # get player name based on player ID
              team = next(item for item in goals if item['playerId'] == key)['teamAbbrev'] # get player name based on player ID
              hat_tricks_scorers[key] = [hat_tricks_scorers[key]] # convert dict value to list
              hat_tricks_scorers[key] += [player] # add player name as a value to player ID key
              hat_tricks_scorers[key] += [team] # add team abbreviation as a value to player ID key
              hat_tricks_scorers_date[key] = hat_tricks_scorers[key] # update the date dictionary
    if bool(hat_tricks_scorers_date): # if there was a hat trick on this date, update the main hat tricks dictionary 
      all_hat_tricks[date] = hat_tricks_scorers_date

    start_date += delta # increment date by 1

print("hat tricks were scored during " + str(len(all_hat_tricks)) + " games in the 2022-2023 NHL season")
hat tricks were scored during 66 games in the 2022-2023 NHL season

Let’s convert our messy all_hat_tricks dictionary to a cleaner data frame.

import pandas as pd

df_data = []
for i in all_hat_tricks: # loop through all dates
  for j in all_hat_tricks[i]: # loop through all hat tricks each date
    df_data.append([i, j, all_hat_tricks[i][j][1], all_hat_tricks[i][j][2], all_hat_tricks[i][j][0]]) # write data for each hat trick to list of list

all_hat_tricks_df = pd.DataFrame(df_data, columns=['date', 'player_ID', 'player_name', 'team', 'goals']) # convert list of lists to dataframe

print(all_hat_tricks_df.head(5)) # print first five rows
         date  player_ID    player_name team  goals
0  2022-10-12    8478402     C. McDavid  EDM      3
1  2022-10-20    8480830  A. Svechnikov  CAR      3
2  2022-10-20    8477949        A. Tuch  BUF      3
3  2022-10-22    8470794    J. Pavelski  DAL      3
4  2022-10-27    8478402     C. McDavid  EDM      3


Nice, that should be easier to work with. How many hat tricks were there in the 2022-2023 season?

print("Number of hat tricks in the 2022-2023 season: " + str(len(all_hat_tricks_df)))
Number of hat tricks in the 2022-2023 season: 96


How about a quick plot?

import plotly.express as px

all_hat_tricks_df['hat_tricks'] = range(1, 1 + len(all_hat_tricks_df))

fig = px.line(all_hat_tricks_df, x='date', y="hat_tricks",
 title = "NHL 2022-2023 Season Hat Tricks",
 template="plotly_dark",
 line_shape='hv') # line_shape will plot lines as steps
fig.update_xaxes(title_text="")
fig.update_yaxes(title_text="number of hat tricks")
fig.update_traces(line_color='cyan', line_width=3)

# reduce margins for better viewing on mobile
fig.update_layout(margin=dict(l=20, r=20, b=20))

fig.show()


I wonder which NHL season had the most hat tricks per game played. To answer this question, we can loop over all NHL seasons and extract the hat trick data.

This code will take a few hours to run since we’re doing a lot of API queries.

from datetime import date, timedelta
import dateutil.parser as dateparser

# get start and end date of season
standings = json.loads(requests.get("https://api-web.nhle.com/v1/standings-season").text)["seasons"] # pull request, get data (.text), and convert JSON to a Python dict

# data structures to store results
every_hat_trick=[]
hat_tricks_by_season=[] 

for season in standings:
  season_id = season['id']

  if season_id != 20232024: # skip current season
    
    start_date = dateparser.parse(season["standingsStart"])
    end_date = dateparser.parse(season["standingsEnd"])

    delta = timedelta(days=1)

    season_games = 0 # counter for the number of games in the season
    season_hat_tricks = 0 # counter for the number of hat tricks

    while start_date.date() <= end_date.date():
        date = start_date.strftime("%Y-%m-%d")

        link2 = "https://api-web.nhle.com/v1/score/" + str(date)
        box_scores = json.loads(requests.get(link2).text)["games"] # pull request, get data (.text), and convert JSON to a Python dict

        hat_tricks_scorers_date = {} # empty dictionary to contain all hat trick scorers on this date

        if bool(box_scores): # only look at dates with games

          for game in box_scores: # loop through each game on each date
            if game['gameType'] == 2: # only look at regular season games
              season_games += 1 # increment the number of games for the season
              goals = [d for d in game["goals"] if d['periodDescriptor']['periodType'] != "SO"] # ignore shootout goals
              scorers = [i["playerId"] for i in goals] # extract player IDs for each goal scored
              scorers_table = dict(collections.Counter(scorers))  # count the number of goals per player
              hat_tricks_scorers = dict((k, v) for k, v in scorers_table.items() if v >= 3) # get scorers with more than 3 goals
              if bool(hat_tricks_scorers): # if there was a hat trick in this game
                for key in hat_tricks_scorers: # loop through hat trick scorers
                  player = next(item for item in goals if item['playerId'] == key)['name']['default'] # get player name based on player ID
                  team = next(item for item in goals if item['playerId'] == key)['teamAbbrev'] # get player name based on player ID
                  game_id = game['id']
                  temp = [season_id, game_id, team, key, player, hat_tricks_scorers[key]]
                  every_hat_trick.append(temp) # add hat trick data to list
                  season_hat_tricks += 1 # increment number of hat tricks for the season

        start_date += delta # increment date by 1
    
    hat_tricks_by_season.append([season_id, season_games, season_hat_tricks]) # add new season data to list

    print(str(season_id) + ", games = " + str(season_games) + ", hat tricks = " + str(season_hat_tricks))
19171918, games = 36, hat tricks = 37
19181919, games = 27, hat tricks = 16
19191920, games = 48, hat tricks = 35
19201921, games = 48, hat tricks = 20
19211922, games = 48, hat tricks = 25
19221923, games = 48, hat tricks = 17
19231924, games = 48, hat tricks = 5
19241925, games = 90, hat tricks = 18
19251926, games = 126, hat tricks = 18
19261927, games = 220, hat tricks = 16
19271928, games = 220, hat tricks = 24
19281929, games = 220, hat tricks = 4
19291930, games = 220, hat tricks = 35
19301931, games = 220, hat tricks = 20
19311932, games = 192, hat tricks = 18
19321933, games = 216, hat tricks = 17
19331934, games = 216, hat tricks = 19
19341935, games = 216, hat tricks = 13
19351936, games = 192, hat tricks = 11
19361937, games = 192, hat tricks = 16
19371938, games = 192, hat tricks = 11
19381939, games = 168, hat tricks = 14
19391940, games = 168, hat tricks = 7
19401941, games = 168, hat tricks = 6
19411942, games = 168, hat tricks = 12
19421943, games = 150, hat tricks = 23
19431944, games = 150, hat tricks = 43
19441945, games = 150, hat tricks = 22
19451946, games = 150, hat tricks = 19
19461947, games = 180, hat tricks = 26
19471948, games = 180, hat tricks = 15
19481949, games = 180, hat tricks = 7
19491950, games = 210, hat tricks = 10
19501951, games = 210, hat tricks = 13
19511952, games = 210, hat tricks = 14
19521953, games = 210, hat tricks = 10
19531954, games = 210, hat tricks = 11
19541955, games = 210, hat tricks = 15
19551956, games = 210, hat tricks = 10
19561957, games = 210, hat tricks = 14
19571958, games = 210, hat tricks = 16
19581959, games = 210, hat tricks = 21
19591960, games = 210, hat tricks = 15
19601961, games = 210, hat tricks = 21
19611962, games = 210, hat tricks = 14
19621963, games = 210, hat tricks = 14
19631964, games = 210, hat tricks = 13
19641965, games = 210, hat tricks = 12
19651966, games = 210, hat tricks = 22
19661967, games = 210, hat tricks = 18
19671968, games = 444, hat tricks = 41
19681969, games = 456, hat tricks = 41
19691970, games = 456, hat tricks = 36
19701971, games = 546, hat tricks = 52
19711972, games = 546, hat tricks = 53
19721973, games = 624, hat tricks = 66
19731974, games = 624, hat tricks = 66
19741975, games = 720, hat tricks = 85
19751976, games = 720, hat tricks = 89
19761977, games = 720, hat tricks = 59
19771978, games = 720, hat tricks = 69
19781979, games = 680, hat tricks = 73
19791980, games = 840, hat tricks = 81
19801981, games = 840, hat tricks = 133
19811982, games = 840, hat tricks = 139
19821983, games = 840, hat tricks = 108
19831984, games = 840, hat tricks = 113
19841985, games = 840, hat tricks = 113
19851986, games = 840, hat tricks = 114
19861987, games = 840, hat tricks = 97
19871988, games = 840, hat tricks = 113
19881989, games = 840, hat tricks = 119
19891990, games = 840, hat tricks = 88
19901991, games = 840, hat tricks = 71
19911992, games = 880, hat tricks = 101
19921993, games = 1008, hat tricks = 112
19931994, games = 1092, hat tricks = 91
19941995, games = 624, hat tricks = 48
19951996, games = 1066, hat tricks = 93
19961997, games = 1066, hat tricks = 74
19971998, games = 1066, hat tricks = 64
19981999, games = 1107, hat tricks = 56
19992000, games = 1148, hat tricks = 53
20002001, games = 1230, hat tricks = 93
20012002, games = 1230, hat tricks = 57
20022003, games = 1230, hat tricks = 75
20032004, games = 1230, hat tricks = 46
20052006, games = 1230, hat tricks = 79
20062007, games = 1230, hat tricks = 71
20072008, games = 1230, hat tricks = 73
20082009, games = 1230, hat tricks = 65
20092010, games = 1230, hat tricks = 67
20102011, games = 1230, hat tricks = 76
20112012, games = 1230, hat tricks = 55
20122013, games = 720, hat tricks = 32
20132014, games = 1230, hat tricks = 56
20142015, games = 1230, hat tricks = 49
20152016, games = 1230, hat tricks = 67
20162017, games = 1230, hat tricks = 59
20172018, games = 1271, hat tricks = 81
20182019, games = 1271, hat tricks = 97
20192020, games = 1082, hat tricks = 67
20202021, games = 868, hat tricks = 60
20212022, games = 1312, hat tricks = 102
20222023, games = 1312, hat tricks = 96


Phew, finally done. Let’s answer our question about which season has the highest rate of hat tricks.

hat_tricks_by_season_df = pd.DataFrame(hat_tricks_by_season, columns=['season_long', 'games', 'hat_tricks']) # convert list of lists to dataframe

hat_tricks_by_season_df['hat_tricks_per_game'] = round(hat_tricks_by_season_df['hat_tricks'] / hat_tricks_by_season_df['games'], 3) # calculate number of hat tricks per game

hat_tricks_by_season_df['temp'] = hat_tricks_by_season_df['season_long'].astype(str)

hat_tricks_by_season_df['temp1'] = hat_tricks_by_season_df['temp'].str[:-4]
hat_tricks_by_season_df['temp2'] = hat_tricks_by_season_df['temp'].str[-4:]

hat_tricks_by_season_df['season'] = hat_tricks_by_season_df[['temp1', 'temp2']].apply(lambda row: '-'.join(row.values.astype(str)), axis=1)

fig = px.bar(hat_tricks_by_season_df, y='hat_tricks_per_game', x=str('season'),
  template="plotly_dark",
  hover_name='season',
  title = "NHL Hat Trick Rate by Season")

fig.update_yaxes(title_text="hat tricks per game")
fig.update_traces(marker_color="cyan") 

# reduce margins for better viewing on mobile
fig.update_layout(margin=dict(l=20, r=20, b=20))

fig.show()


The NHL’s early seasons were the golden age of hat tricks. On average, there was actually more than one hat trick per game during the very first season! The rate of hat tricks declined drastically in the 1920s and 30s.

So what caused this decline? Probably many factors. First, improved goalie equipment coupled with rule changes led to a decline in scoring. According to hockey-reference.com, teams averaged 4.75 goals per game during the 1917-1918 NHL season. Compare that to only 3.18 goals per game in 2022-2023 season.

Second, and perhaps more importantly, team rosters have gotten larger. In the early years of the NHL, only 9 skaters per team were allowed to play during any given game. Todays teams have twice as many skaters per game. More players on the team means less ice time for any given player and fewer opportunities to rack up goals.

A complete analysis of hat trick rates would require consideration of these factors and more.

One last question. How many times has more than one hat trick been scored in an NHL game?

every_hat_trick_df = pd.DataFrame(every_hat_trick, columns=['season_id', "game_id", 'team', 'player_id', 'player', 'goals']) # convert list of lists to dataframe

print("there have been " + str(len(every_hat_trick_df.index)) + " hat tricks in NHL history")
print("")

games_hts = dict(collections.Counter(every_hat_trick_df['game_id'])) # count hat tricks for each game
multi_ht_games = dict((k, v) for k, v in games_hts.items() if v >= 2) # get only games with multiple hat tricks

print("there have been " + str(len(multi_ht_games)) + " games with multiple hat tricks")
print("")

three_ht_games = dict((k, v) for k, v in games_hts.items() if v >= 3) # get only games with more than 3 hat tricks

print("there have been " + str(len(three_ht_games)) + " games with more than 3 hat tricks")

print("")

# find the game with the most hat tricks
max_hts = max(games_hts.values())
n_games_max = sum(value == max_hts for value in games_hts.values()) # how many games have the max number of hat tricks?

max_ht_games = dict((k, v) for k, v in games_hts.items() if v >= max_hts) # get only games with multiple hat tricks

print("the maximum number of hat tricks scored in an NHL game is " + str(max_hts))
print("there have been " + str(n_games_max) + " games with " + str(max_hts) + " hat tricks")

c = 1
for i in max_ht_games:
  print("game " + str(c) + ":")
  max_hts_df = every_hat_trick_df.loc[every_hat_trick_df['game_id'] == i]
  print(max_hts_df)
  c += 1
there have been 5086 hat tricks in NHL history

there have been 239 games with multiple hat tricks

there have been 18 games with more than 3 hat tricks

the maximum number of hat tricks scored in an NHL game is 4
there have been 3 games with 4 hat tricks
game 1:
   season_id     game_id team  player_id      player  goals
4   19171918  1917020003  TAN    8447832  H. Meeking      3
5   19171918  1917020003  TAN    8445873  C. Denneny      3
6   19171918  1917020003  TAN    8448013    R. Noble      3
7   19171918  1917020003  SEN    8445874  C. Denneny      3
game 2:
    season_id     game_id team  player_id       player  goals
59   19191920  1919020011  MTL    8448154     D. Pitre      3
60   19191920  1919020011  MTL    8447289   N. Lalonde      6
61   19191920  1919020011  MTL    8445496  O. Cleghorn      3
62   19191920  1919020011  TSP    8448013     R. Noble      3
game 3:
    season_id     game_id team  player_id       player  goals
76   19191920  1919020042  MTL    8448154     D. Pitre      3
77   19191920  1919020042  MTL    8445496  O. Cleghorn      3
78   19191920  1919020042  MTL    8447289   N. Lalonde      4
79   19191920  1919020042  MTL    8445314   H. Cameron      4


Wow, three games with four hat tricks each!

Let’s look at the first game with four hat tricks. We can look at the box score for this game where the Toronto Arenas (TAN) blew out the Ottawa Senators (SEN) by a score of 11-4. However, one Ottawa player still managed to score a hat trick.

Ottawa’s hat trick was scored by a player named “C. Denneny.” Weirdly, there was also a Toronto hat trick scorer named “C. Denneny.” As it turns out, Corb Denneny (Toronto) and Cy Denneny (Ottawa) were brothers! Both are now members of the Hockey Hall of Fame.

I wonder how many times brothers or relatives have scored hat tricks in the same game.

print("the following games have multiple hat tricks scored by players with the same last name:")
print("")

for i in multi_ht_games:
  temp = every_hat_trick_df[every_hat_trick_df['game_id'] == i] # get data for multi hat trick games
  temp2 = temp['player'].str[3:].tolist() # convert player column to list
  temp2_set = set(temp2) # create set
  if len(temp2) != len(temp2_set): # if lengths are not equal, list contains duplicate values
    brother_hts = every_hat_trick_df.loc[every_hat_trick_df['game_id'] == i]
    print(brother_hts)
the following games have multiple hat tricks scored by players with the same last name:

game 1:
   season_id     game_id team  player_id      player  goals
4   19171918  1917020003  TAN    8447832  H. Meeking      3
5   19171918  1917020003  TAN    8445873  C. Denneny      3
6   19171918  1917020003  TAN    8448013    R. Noble      3
7   19171918  1917020003  SEN    8445874  C. Denneny      3
game 2:
    season_id     game_id team  player_id      player  goals
17   19171918  1917020013  SEN    8445844  J. Darragh      3
18   19171918  1917020013  SEN    8445874  C. Denneny      3
19   19171918  1917020013  TAN    8445873  C. Denneny      3
game 3:
     season_id     game_id team  player_id       player  goals
112   19211922  1921020018  MTL    8445497  S. Cleghorn      4
113   19211922  1921020018  MTL    8445496  O. Cleghorn      4
game 4:
     season_id     game_id team  player_id      player  goals
127   19211922  1921020041  TSP    8445873  C. Denneny      3
128   19211922  1921020041  SEN    8445874  C. Denneny      4
game 5:
     season_id     game_id team  player_id      player  goals
557   19461947  1946020149  CHI    8445062  D. Bentley      4
558   19461947  1946020149  NYR    8449363  G. Warwick      3
559   19461947  1946020149  CHI    8445063  M. Bentley      3
game 6:
      season_id     game_id team  player_id      player  goals
1757   19801981  1980020621  QUE    8451689  P. Stastny      3
1758   19801981  1980020621  QUE    8451688  A. Stastny      3
game 7:
      season_id     game_id team  player_id      player  goals
1761   19801981  1980020636  QUE    8451689  P. Stastny      4
1762   19801981  1980020636  QUE    8451688  A. Stastny      3
1763   19801981  1980020636  QUE    8450815  J. Richard      3


There have been seven NHL games where two players with the same last name both scored hat tricks! Turns out our brothers from the previous code block, Corb Denneny and Cy Denneny, have actually scored hat tricks together three separate times.

Odie and Sprague Cleghorn are another pair of brothers who scored a hat trick in the same game, but this time they both played for the same team. In 1946, brother Dough and Max Bentley achieved a similar feat while both playing for the Chicago Blackhawks.

And lastly, brothers Peter and Anton Šťastný of the Quebec Nordiques scored hat tricks together twice in the 1980-1981 season. This was their first year in the NHL, having just defected to Canada from communist Czechoslovakia.

This is one of my favorite things about digging through sports databases. You can find all manner of strange statistical anomalies and reveal fascinating human stories that might have otherwise been lost to time.