Skip to content
1 hidden cell
Identifying NFL Defenses' Strong and Weak Zones in 2023
Project
Identifying NFL Defenses' Strong and Weak Zones in 2023
Source
NFLSavant's "2023 Play By Play Data" dataset
Objective
Discover every NFL team's strong and weak zones in their run and pass defense. This will be done by analyzing every NFL team's mean and median yards allowed per rush attempt for each run direction, and analyzing every team's allowed completion % and mean yards allowed for each pass direction.
1. Load CSV file
1 hidden cell
2. Filter data for run plays and create Pandas crosstabs that return mean and median yards allowed for each team and rush direction
Why the mean and median?
- Specifically for running plays, average yards per play is extremely susceptible to outliers. Most run plays result in a 0-5 yard gain, but an occasional breakout run can be 20-99 yards. While these breakout runs are crucial to account for in our analysis, they can heavily influence the mean and not allow it to tell the full story of how many yards a defense truly gives up on a typical
play.
#import packages
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
#filter dataset for run plays
pbp_rush = pbp[pbp['PlayType'] == 'RUSH']
#crosstab that shows yards allowed per play by rush direction for each team
rush_def_yards = pd.crosstab(pbp_rush['DefenseTeam'], pbp_rush['RushDirection'], values=pbp_rush['Yards'], aggfunc='mean')
rush_def_yards = rush_def_yards[['LEFT END','LEFT TACKLE','LEFT GUARD', 'CENTER','RIGHT GUARD','RIGHT TACKLE','RIGHT END']]
#same crosstab but median to layer over heatmap
rush_def_yards_med = pd.crosstab(pbp_rush['DefenseTeam'], pbp_rush['RushDirection'], values=pbp_rush['Yards'], aggfunc='median')
rush_def_yards_med = rush_def_yards_med[['LEFT END','LEFT TACKLE','LEFT GUARD', 'CENTER','RIGHT GUARD','RIGHT TACKLE','RIGHT END']]
3. Create Seaborn heatmap that visualizes mean yards allowed per rush attempt
#heatmap function (written as function to use on fractions of teams for better looking graphics)
def heatmap_r(my_crosstab):
#set sns theme and size for heatmap
sns.set_theme(rc={'figure.figsize':(6,24)})
sns.set_style('white')
#YARDS ALLOWED BY RUN DIRECTION HEAT MAP
rundir_heatmap = sns.heatmap(my_crosstab, annot=True, cmap='YlOrRd', linewidths=1, cbar=False, square=True, fmt='.1f', vmin=2.5, vmax=7.5)
#set title, xlabel, ylable, tick font sizes
rundir_heatmap.set_title('NFL 2023: Average Yards Allowed per Rush Attempt by Direction', fontdict = {'weight':'bold', 'fontsize' : '16'}, y=1.075)
rundir_heatmap.set_xlabel('Run Direction', fontdict = {'weight':'bold', 'fontsize' : '16'})
rundir_heatmap.set_ylabel('')
rundir_heatmap.xaxis.set_tick_params(labelsize = 12)
rundir_heatmap.yaxis.set_tick_params(labelsize = 16)
plt.gca().tick_params(axis="x", labelbottom=True, labeltop=True)
plt.yticks(rotation=0)
plt.xticks(rotation=90)
plt.show()
heatmap_r(rush_def_yards)
4. Split heatmap, make slight adjustments, layer the median crosstab onto the heatmap, and add graphics/key for a more user-friendly visual
How can NFL teams use this run defense data?
- Self-Improvement
- For any given rush direction, if a team's mean and median yards allowed per rush are lower than other teams, they do a great job of filling these gaps on run plays and consistently stop runs coming in this direction. They should sustain the linemen and linebackers who defend this gap.
- If a team gives up high mean and median yards per rush compared to other teams, they are consistently giving up big runs in that direction. They should consider replacing linemen and linebackers who defend that gap or changing their approach to defending runs in that direction.
- In some cases, a team has low or average median yards allowed in a direction, but the mean is significantly higher. This means they don't typically give up big yardage in this direction, but they occasionally give up some really long runs causing the outlier influence. In this case, a team may want to improve how their secondary makes stops in this direction when the running back breaks through the defensive line.
- Strategy Against Opponents
- Teams should game plan to call a high volume of running plays in the directions where their opponent gives up high mean and median yards per rush. Running the ball in these directions should lead to consistent big gains in yardage.
- While not as reliable as directions with high mean and median, teams may also want to consider running the ball where the defense has a much higher mean than median yards allowed, due to the potential of a "breakout" run. This is especially true if they desperately need to change the momentum of the game.
- Teams should consider any players opponents have drafted, signed, or lost that could affect their defensive performance in specific rush directions. As the next season progresses, they should conduct a similar analysis to this one with the current season's data, for up-to-date insights on defensive weaknesses.
5. Filter data for pass plays and create Pandas crosstabs that return mean yards allowed and completion % allowed for each team and pass direction
#filter dataset for run plays
pbp_pass = pbp[pbp['PlayType'] == 'PASS']
#column that is 100 if if "incomplete" and "interception" columns are both zero, and zero if either column equals 1
pbp_pass['IsComplete100'] = (-1 * (pbp_pass['IsIncomplete'] + pbp_pass['IsInterception']) + 1) * 100
#crosstab that shows yards allowed per play by pass direction for each team
pass_def_yards = pd.crosstab(pbp_pass['DefenseTeam'], pbp_pass['PassType'], values=pbp_pass['Yards'], aggfunc='mean')
pass_def_yards = pass_def_yards[['SHORT LEFT','SHORT MIDDLE','SHORT RIGHT','DEEP LEFT','DEEP MIDDLE','DEEP RIGHT']]
#same crosstab but allowed completion % to layer over heatmap
pass_def_yards_pct = pd.crosstab(pbp_pass['DefenseTeam'], pbp_pass['PassType'], values=pbp_pass['IsComplete100'], aggfunc='mean')
pass_def_yards_pct = pass_def_yards_pct[['SHORT LEFT','SHORT MIDDLE','SHORT RIGHT','DEEP LEFT','DEEP MIDDLE','DEEP RIGHT']]
6. Create Seaborn heatmap that visualizes mean yards allowed per pass attempt
#heatmap function (written as function to use on fractions of teams for better looking graphics)
def heatmap_p(my_crosstab):
#set sns theme and size for heatmap
sns.set_theme(rc={'figure.figsize':(6,24)})
sns.set_style('white')
#YARDS ALLOWED BY RUN DIRECTION HEAT MAP
rundir_heatmap = sns.heatmap(my_crosstab, annot=True, cmap='gnuplot2_r', linewidths=1, cbar=False, square=True, fmt='.1f', vmin=5, vmax=20)
#set title, xlabel, ylable, tick font sizes
rundir_heatmap.set_title('NFL 2023: Average Yards Allowed per Pass Attempt by Direction', fontdict = {'weight':'bold', 'fontsize' : '16'}, y=1.075)
rundir_heatmap.set_xlabel('Pass Direction', fontdict = {'weight':'bold', 'fontsize' : '16'})
rundir_heatmap.set_ylabel('')
rundir_heatmap.xaxis.set_tick_params(labelsize = 12)
rundir_heatmap.yaxis.set_tick_params(labelsize = 16)
plt.gca().tick_params(axis="x", labelbottom=True, labeltop=True)
plt.yticks(rotation=0)
plt.xticks(rotation=90)
plt.show()
heatmap_p(pass_def_yards)