Kalen DeBoer Playcalling Analysis (2022-2023)¶
This notebook provides analysis of Washington's offensive playcalling under college football coach Kalen DeBoer. This document aims to provide insights on his particular habits, especially in key situations, such as 3rd down and long or 4th down conversions.
import pygskin
# Load the coach from the file and print basic information
coach = pygskin.Coach.unpickle("examples/coaches/Kalen_DeBoer.coach")
first_name = coach.coach_dict["first_name"]
last_name = coach.coach_dict["last_name"]
print(f"Loaded {first_name} {last_name}.")
print("Teams coached:")
for year in coach.coach_school_dict:
print(f"{coach.coach_school_dict[year]} ({year})")
# create a cybercoach for simpler analysis of plays
cybercoach = pygskin.CyberCoach(coach)
cybercoach.train(pygskin.ModelType.RANDOM_FOREST)
Loaded Kalen DeBoer. Teams coached: Washington (2022) Washington (2023)
Available Information¶
The CFBD database is not perfect, so stats like field goal attempts may not be completely accurate. Other features, like the game clock, were not always recorded properly. Free resources have limits. Here is some of the data we do have for analysis:
Plays¶
- Yards to go, field position, etc.
- Play call (run, pass, field goal, punt)
Game Information¶
- Quarter and game clock
- Timeouts remaining (both teams)
- Scores for both teams
Offensive Stats¶
- Passing yards per attempt (in this game only, up to this point)
- Rushing yards per attempt (in this game only, up to this point)
# print(cybercoach.original_play_df.columns)
print(cybercoach.original_play_df.columns)
Index(['id', 'drive_id', 'game_id', 'drive_number', 'play_number', 'offense', 'offense_conference', 'offense_score', 'defense', 'home', 'away', 'defense_conference', 'defense_score', 'period', 'clock', 'offense_timeouts', 'defense_timeouts', 'yard_line', 'yards_to_goal', 'down', 'distance', 'yards_gained', 'scoring', 'play_type', 'play_text', 'ppa', 'wallclock', 'week', 'season', 'seconds_remaining', 'score_diff', 'passing_yards_per_attempt', 'rushing_yards_per_attempt', 'play_call'], dtype='object')
General Information¶
import matplotlib
from matplotlib.figure import Figure
import matplotlib.patches as mpatches
import matplotlib.pyplot as plt
import numpy as np
# Pie chart of the play call distribution
coach_play_dist = plt.figure(figsize=(5, 5), dpi=100)
play_dist_axes = coach_play_dist.add_subplot(111)
play_dist = [value for value in cybercoach.play_distribution.values()]
play_types = [pygskin.PlayType(key).name for key in cybercoach.play_distribution.keys()]
colors = [pygskin.PLAY_TYPE_COLOR_DICT[pygskin.PlayType(key)] for key in cybercoach.play_distribution.keys()]
play_dist_axes.pie(play_dist, labels=play_types, autopct="%1.1f%%", colors=colors)
play_dist_axes.set_title("Actual Play Call Distribution")
# Bar graph of actual play call distribution by down
fourth_down_call_by_distance = plt.figure(figsize=(5, 5), dpi=100)
fourth_call_axes = fourth_down_call_by_distance.add_subplot(111)
play_dist_by_down = cybercoach.play_distribution_by_down
bar_data = {}
for dist in play_dist_by_down.keys():
bar_data[dist] = []
bottom = np.zeros(len(play_dist_by_down[dist].keys()))
for play_type in play_dist_by_down[dist].keys():
bar_data[dist].append(fourth_call_axes.bar(x=dist, height=play_dist_by_down[dist][play_type], width=1, bottom=bottom, label=play_type, color=pygskin.PLAY_TYPE_COLOR_DICT[play_type]))
bottom += play_dist_by_down[dist][play_type]
fourth_call_axes.set_title("Actual Play Call Distribution by Down")
patches = [mpatches.Patch(color=pygskin.PLAY_TYPE_COLOR_DICT[play_type], label=pygskin.PlayType(play_type).name) for play_type in pygskin.PlayType]
fourth_call_axes.legend(handles=patches)
fourth_call_axes.set_xlim(0.5, 4.5)
fourth_call_axes.set_xticks([1, 2, 3, 4])
# label axes
fourth_call_axes.set_xlabel("Down")
fourth_call_axes.set_ylabel("Play Call Distribution")
plt.show()
Specific Situations¶
This section breaks down Coach DeBoer's habits in key yardage situations.
4th Down¶
In most 4th down situations, the expected play call is considered very simple. A play is considered "4th and short" if the distance to the 1st down marker is less than or equal to than 3 yards. A play is considered "4th and long" if the distance to the 1st down marker is greater than 3 yards.
import math
fourth_down_plays = cybercoach.play_df[cybercoach.play_df["down"] == 4]
print(f"Between the years of {cybercoach.coach.first_year} and {cybercoach.coach.last_year}, Coach {last_name} faced {len(fourth_down_plays)} 4th down situations.")
fourth_and_short = fourth_down_plays[fourth_down_plays["distance"] <= 3]
fourth_and_long = fourth_down_plays[fourth_down_plays["distance"] > 3]
# Stacked bar graph of actual play call distribution by down
fourth_down_call_by_distance = plt.figure(figsize=(5, 5), dpi=100)
fourth_call_axes = fourth_down_call_by_distance.add_subplot(111)
# get the count of each play type by distance
play_type_by_distance = fourth_down_plays.groupby(["play_call", "distance"]).size().unstack().to_dict()
for dist in play_type_by_distance:
bottom = 0
for play_type in play_type_by_distance[dist].keys():
if not math.isnan(play_type_by_distance[dist][play_type]):
fourth_call_axes.bar(x=dist, height=play_type_by_distance[dist][play_type], width=1, bottom=bottom, label=play_type, color=pygskin.PLAY_TYPE_COLOR_DICT[play_type])
bottom += play_type_by_distance[dist][play_type]
fourth_call_axes.set_title("4th Down Play Call Distribution by Distance")
patches = [mpatches.Patch(color=pygskin.PLAY_TYPE_COLOR_DICT[play_type], label=pygskin.PlayType(play_type).name) for play_type in pygskin.PlayType]
fourth_call_axes.legend(handles=patches)
fourth_call_axes.set_xticks(range(0, max(play_type_by_distance.keys()) + 1, 5))
# label axes
fourth_call_axes.set_xlabel("Distance to 1st Down")
fourth_call_axes.set_ylabel("Play Call Distribution")
# divide the graph into short and long scenarios
fourth_call_axes.axvline(3.5, color="black", linestyle="--")
# print(f"Pass attempts: {fourth_down_plays[fourth_down_plays['play_call'] == pygskin.PlayType.PASS].shape[0]}")
# print(f"Run attempts: {fourth_down_plays[fourth_down_plays['play_call'] == pygskin.PlayType.RUN].shape[0]}")
# print(f"Field goal attempts: {fourth_down_plays[fourth_down_plays['play_call'] == pygskin.PlayType.FIELD_GOAL].shape[0]}")
# print(f"Punt attempts: {fourth_down_plays[fourth_down_plays['play_call'] == pygskin.PlayType.PUNT].shape[0]}")
plt.show()
Between the years of 2022 and 2023, Coach DeBoer faced 137 4th down situations.
4th and Short¶
In 4th and short situations, a coach may call a run or pass play if a turnover on downs would not give the opponent a very good field position. Otherwise, if the ball is within field goal range (no further than the opponent's ~40 yard line), it would probably result in a field goal. Otherwise, the team would likely punt the ball.
# 4th and short
print(f"{len(fourth_and_short)} of these {len(fourth_down_plays)} plays were 4th and short.")
# Pie chart of the play call distribution
fourth_and_short_play_dist = plt.figure(figsize=(5, 5), dpi=100)
fourth_short_axes = fourth_and_short_play_dist.add_subplot(111)
play_dist = [value for value in fourth_and_short["play_call"].value_counts()]
play_types = [pygskin.PlayType(key).name for key in fourth_and_short["play_call"].value_counts().keys()]
colors = [pygskin.PLAY_TYPE_COLOR_DICT[pygskin.PlayType(key)] for key in fourth_and_short["play_call"].value_counts().keys()]
fourth_short_axes.pie(play_dist, labels=play_types, autopct="%1.1f%%", colors=colors)
fourth_short_axes.set_title("4th and Short Play Call Distribution")
plt.show()
35 of these 137 plays were 4th and short.
4th and Long¶
Most spectators would typically expect a coach to punt on 4th and long if the ball was not within field goal range (unless it was the last chance to win the game).
# 4th and long
print(f"{len(fourth_and_long)} of these {len(fourth_down_plays)} plays were 4th and long.")
# Pie chart of the play call distribution
fourth_and_long_play_dist = plt.figure(figsize=(5, 5), dpi=100)
fourth_long_axes = fourth_and_long_play_dist.add_subplot(111)
play_dist = [value for value in fourth_and_long["play_call"].value_counts()]
play_types = [pygskin.PlayType(key).name for key in fourth_and_long["play_call"].value_counts().keys()]
colors = [pygskin.PLAY_TYPE_COLOR_DICT[pygskin.PlayType(key)] for key in fourth_and_long["play_call"].value_counts().keys()]
fourth_long_axes.pie(play_dist, labels=play_types, autopct="%1.1f%%", colors=colors)
fourth_long_axes.set_title("4th and Long Play Call Distribution")
# Analysis
print(f"As one might expect, Coach {last_name} called {fourth_and_long[fourth_and_long['play_call'] == pygskin.PlayType.RUN].shape[0]} run plays on 4th and long. Such a play (unless it were a trick play) would be considered an incredibly strange decision.")
102 of these 137 plays were 4th and long. As one might expect, Coach DeBoer called 0 run plays on 4th and long. Such a play (unless it were a trick play) would be considered an incredibly strange decision.
Field Goals¶
field_goals = cybercoach.play_df[cybercoach.play_df["play_call"] == pygskin.PlayType.FIELD_GOAL]
longest_field_goal = field_goals[field_goals["play_call"] == pygskin.PlayType.FIELD_GOAL]["yards_to_goal"].max()
shortest_field_goal = field_goals[field_goals["play_call"] == pygskin.PlayType.FIELD_GOAL]["yards_to_goal"].min()
print(f"Coach DeBoer called for field goals between {shortest_field_goal} and {longest_field_goal} yards.")
print(f"Coach DeBoer's team attempted {len(field_goals)} field goals in the analyzed years.")
# Histogram of field goal attempts by distance (5 yard bins)
field_goal_dist = plt.figure(figsize=(5, 5), dpi=100)
fg_axes = field_goal_dist.add_subplot(111)
fg_axes.hist(field_goals["yards_to_goal"], bins=range(0, 70, 5), color=pygskin.PLAY_TYPE_COLOR_DICT[pygskin.PlayType.FIELD_GOAL], edgecolor="black")
fg_axes.set_title("Field Goal Attempts by Distance")
fg_axes.set_xlabel("Distance to Goal")
fg_axes.set_ylabel("Number of Attempts")
# add labels to bars
for container in fg_axes.containers:
fg_axes.bar_label(container)
plt.show()
Coach DeBoer called for field goals between 3 and 32 yards. Coach DeBoer's team attempted 37 field goals in the analyzed years.