Skip to content
1 hidden cell
Optimal Play-Calling in Short Yardage Situations in the NFL
Project
Optimal Play-Calling in Short Yardage Situations
Source
NFLSavant's "2023 Play By Play Data" dataset
Objective
Analyze 2023 NFL play-by-play data for short yardage situations:
- 3rd down and 3 yards or less
- 4th down and 3 yards or less
- 3 yards or less from a touchdown
- 2-point conversion
And identify:
- Is it more advantageous to pass or run in each type of short yardage situation?
- Is it more advantageous to pass or run for each team in short yardage situations?
- Which run direction is overall most advantageous in short yardage situations?
- How advantageous is each run direction in each type of short yardage situation?
- How advantageous is each run direction for each team in short yardage situations?
1. Load the CSV file from NFLsavant.com.
1 hidden cell
2. Prepare data for run vs. pass analysis.
#import packages
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
#create function that assigns each play as run, pass, or other
def run_or_pass(row):
if (row['PlayType'] in ['PASS','SACK','SCRAMBLE']) or (row['PlayType'] == 'TWO-POINT CONVERSION' and ('PASS' in row['Description'] or 'SACK' in row['Description'])):
type = 'Pass'
elif row['PlayType'] == 'RUSH' or (row['PlayType'] == 'TWO-POINT CONVERSION' and 'RUSH' in row['Description']):
type = 'Run'
else:
type = 'Other'
return type
#use function to create a modified play type column
pbp['Play'] = pbp.apply(run_or_pass, axis = 1)
#drop plays that aren't run or pass
pbp = pbp[pbp['Play'] != 'Other']
#define a short yardage threshold in case we want to adjust it
short = 3
#create function that assigns each play as 3rd and Short, 4th and Short, Goal Line, 2-Point Conversion, or Not Short Yardage
def situation(row):
if row['PlayType'] == 'TWO-POINT CONVERSION':
type = '2 Pt Conversion'
elif row['YardLineFixed'] <= short and row['YardLineDirection'] == 'OPP':
type = 'Goal Line'
elif row['Down'] == 3 and row['ToGo'] <= short:
type = '3rd and Short'
elif row['Down'] == 4 and row['ToGo'] <= short:
type = '4th and Short'
else:
type = 'NotShortYardage'
return type
#use function to create a situation column
pbp['Situation'] = pbp.apply(situation, axis = 1)
#drop plays that aren't short yardage
pbp = pbp[pbp['Situation'] != 'NotShortYardage']
#create function that determines if a play was a success
def success(row):
if (row['SeriesFirstDown'] == 1 and row['IsTwoPointConversion'] == 0) or row['IsTouchdown'] == 1 or row['IsTwoPointConversionSuccessful'] == 1:
type = 1
else:
type = 0
return type
#use function to create a situation column
pbp['Success'] = pbp.apply(success, axis = 1)
#create groupby tables to visualize short yardage situation success by situation type and by team
result_by_playtype = pbp.groupby(['Situation','Play'])['Success'].mean().reset_index()
result_by_playtype['SuccessPct'] = result_by_playtype['Success'].round(decimals = 2) * 100
result_by_team = pbp.groupby(['OffenseTeam','Play'])['Success'].mean().reset_index()
result_by_team['SuccessPct'] = result_by_team['Success'].round(decimals = 2) * 100
3. Visualize short yardage conversion rate by situation type when running vs. passing:
- 2-Point Conversion
- 3rd and Short (3 yards or less to 1st down)
- 4th and Short (3 yards or less to 1st down)
- Goal Line (3 yards or less to touchdown)
#BY-SITUATION BAR GRAPH
#set sns theme and size for by-situation chart
sns.set_theme(rc={'figure.figsize':(10,10)})
sns.set_style('white')
#establish by-situation chart
vis_situation = sns.barplot(data=result_by_playtype,
x="Situation",
y="SuccessPct",
hue="Play",
palette=['cornflowerblue', 'sandybrown'])
#set title, xlabel, ylable, tick font sizes
vis_situation.set_title('2023 Running vs. Passing Success Rates in Short Yardage Situations',
fontdict = {'weight':'bold', 'fontsize' : '24'},y=1.02)
vis_situation.set_xlabel('Situation',
fontdict = {'weight':'bold', 'fontsize' : '18'})
vis_situation.set_ylabel('Success Rate',
fontdict = {'weight':'bold', 'fontsize' : '18'})
vis_situation.xaxis.set_tick_params(labelsize = 16)
vis_situation.yaxis.set_tick_params(labelsize = 16)
#adding % symbol to yticks
y_value=['{:,.0f}'.format(x) + '%' for x in vis_situation.get_yticks()]
vis_situation.set_yticklabels(y_value)
plt.show()
Observations
- Each type of situation saw a 10%-20% higher success rate with running plays in the 2023 NFL season
- Running was generally more advantageous than passing in short yardage situations
4. Visualize short yardage conversion rate by NFL team when running vs. passing.
#BY-TEAM BAR GRAPH
#set sns theme and size for by-team chart
sns.set_theme(rc={'figure.figsize':(10,40)})
sns.set_style('white')
#establish by-team chart
vis_team = sns.barplot(data=result_by_team,
y="OffenseTeam",
x="SuccessPct",
hue="Play",
palette=['cornflowerblue', 'sandybrown'])
#set title, xlabel, ylable, tick font sizes
vis_team.set_title('2023 Short Yardage Situations: Conversion Rate by Team',
fontdict = {'weight':'bold', 'fontsize' : '24'},y=1.02)
vis_team.set_ylabel('')
vis_team.set_xlabel('Success Rate',
fontdict = {'weight':'bold', 'fontsize' : '18'})
vis_team.yaxis.set_tick_params(labelsize = 16)
vis_team.xaxis.set_tick_params(labelsize = 16)
#add annotations to each bar in bar graph
for container in vis_team.containers:
vis_team.bar_label(container)
#increase legend size
plt.legend(fontsize='x-large')
# add label to top and bottom of x axis
plt.gca().tick_params(axis="x", bottom=True, top=True, labelbottom=True, labeltop=True)
# add percent symbol to x ticks
def pct_sym(x, pos):
return '{:.0f}%'.format(x)
plt.gca().xaxis.set_major_formatter(plt.FuncFormatter(pct_sym))
plt.show()
Observations
- Teams that should pass in short yardage situations:
- Packers
- Buccaneers
- Teams that have similar chances of converting running or passing:
- Bengals
- Lions
- Chiefs
- Chargers
- Vikings
- Teams that should run in short yardage situations:
- Cardinals
- Falcons
- Ravens
- Bills
- Panthers
- Bears
- Browns
- Cowboys
- Broncos
- Texans
- Colts
- Jaguars
- Rams
- Raiders
- Dolphins
- Patriots
- Saints
- Giants
- Jets
- Eagles
- Steelers
- Seahawks
- 49ers
- Titans
- Commanders
5. Prepare data for run direction analysis.
#PREPARE DATA FOR FURTHER ANALYSIS (CONVERSION RATE BY RUN DIRECTION)
#define function that assigns a run direction regardless of right or left side, to include 2pt conversions
def run_dir(row):
if row['IsTwoPointConversion'] == 1 and row['Play'] == 'Run':
if 'END' in row['Description']:
type = 'END'
elif 'TACKLE' in row['Description']:
type = 'TACKLE'
elif 'GUARD' in row['Description']:
type = 'GUARD'
elif 'MIDDLE' in row['Description']:
type = 'CENTER'
else:
type = 'NOT_RUN'
elif row['PlayType'] == 'RUSH':
if 'END' in row['RushDirection']:
type = 'END'
elif 'TACKLE' in row['RushDirection']:
type = 'TACKLE'
elif 'GUARD' in row['RushDirection']:
type = 'GUARD'
elif 'CENTER' in row['RushDirection']:
type = 'CENTER'
else:
type = 'NOT_RUN'
else:
type = 'NOT_RUN'
return type
#use function to create a modified run direction column
pbp['RushDirectionMod'] = pbp.apply(run_dir, axis = 1)
#new table, drop plays that aren't short yardage
pbp_runs = pbp[pbp['RushDirectionMod'] != 'NOT_RUN']
#table that groups by rush direction and calculates success rate
result_by_rundir = pbp_runs.groupby(['RushDirectionMod'])['Success'].mean().reset_index()
result_by_rundir['SuccessPct'] = result_by_rundir['Success'].round(decimals = 2) * 100
#table that groups by rush direction and situation and calculates success rate
results_playtype_rundir = pbp_runs.groupby(['RushDirectionMod','Situation'])['Success'].mean().reset_index()
results_playtype_rundir['SuccessPct'] = results_playtype_rundir['Success'].round(decimals = 2) * 100
#crosstab that provides success rate for each team and run direction combination
rundir_team_crosstab = pd.crosstab(pbp_runs['OffenseTeam'], pbp_runs['RushDirectionMod'], values=pbp_runs['Success'], aggfunc='mean')
rundir_team_crosstab = rundir_team_crosstab[['CENTER','GUARD','TACKLE','END']]
#crosstab that provides sample size for each team and run direction combination
sample_size_crosstab = pd.crosstab(pbp_runs['OffenseTeam'], pbp_runs['RushDirectionMod'],values=pbp_runs['PlayType'], aggfunc='count')
sample_size_crosstab = sample_size_crosstab[['CENTER','GUARD','TACKLE','END']]
6. Visualize success rate by run direction in short yardage situations.
#RUN DIRECTION BAR GRAPH
#set sns theme and size for by-RUN DIRECTION chart
sns.set_theme(rc={'figure.figsize':(10,10)})
sns.set_style('white')
#establish by-RUN DIRECTION chart
vis_rundir = sns.barplot(data=result_by_rundir,
x="RushDirectionMod",
y="SuccessPct", palette='Dark2' , order=['CENTER','GUARD','TACKLE','END']
)
#set title, xlabel, ylable, tick font sizes
vis_rundir.set_title('2023 Success Rates in Short Yardage Situations by Run Direction',
fontdict = {'weight':'bold', 'fontsize' : '24'},y=1.02)
vis_rundir.set_xlabel('Run Direction',
fontdict = {'weight':'bold', 'fontsize' : '18'})
vis_rundir.set_ylabel('Success Rate',
fontdict = {'weight':'bold', 'fontsize' : '18'})
vis_rundir.xaxis.set_tick_params(labelsize = 16)
vis_rundir.yaxis.set_tick_params(labelsize = 16)
#adding % symbol to yticks
y_value=['{:,.0f}'.format(x) + '%' for x in vis_rundir.get_yticks()]
vis_rundir.set_yticklabels(y_value)
plt.show()