For this assignment, we were given a dataframe with player and play data from the first week of the 2022 NFL season. We decided to examine the efficacy of motion plays against every coverage the defense could run. To do this, we created a logistic regression model examining the percentage of motion plays against a specific coverage that resulted in a first down. In theory, this model will allow NFL teams to exploit certain coverages on the offensive side of the ball, and avoid certain coverages on the defensive side of the ball. Additionally, we analyzed defensive coverages in particular game situations, including after a touchback and a two-minute drill. These, being situations that every team finds themselves in frequently, are imperative to consider to see if there is a discrepancy with running a specific coverage in that scenario.
From the wider dataframe, we took all the variables associated with a given play, including preSnapHomeScore, preSnapVisitorScore (and subsequently possessionTeam and homeTeamAbbr to have our pointDifferential variable always consider the team with the ball), penaltyYards, absoluteYardLineNumber, quarter, and pff_passCoverage. To aid our logistic regression, we transformed some of these into binary variables. For example, if the game was in the 1st quarter, we assigned a 1 to our is1Q variable, and a 0 for the rest. We did the same for variables examining field position and coverage as well. Then, we separated the data into a training and testing dataset to determine if our model fit the data well. After regularly getting predictive accuracies between 60% and 75%, we felt comfortable using our model for the rest of this project. Finally, using this model, we found the success rate of all of the given coverages, and we plotted those success rates for specific game scenarios to see which one held up the best.
setwd("/Users/smesaros/Desktop/IS-BDB-Data")
directory <- paste0(getwd())
source('https://raw.githubusercontent.com/ptallon/SportsAnalytics_Fall2024/main/SharedCode.R')
load_packages(c("data.table", "dplyr", "ggplot2", "ggalt", "gifski", "gganimate", "rcompanion", "tidyr"))
t1 <- fread("tracking_week_1.csv")
games <- fread("games.csv")
plays <- fread("plays.csv")
players <- fread("players.csv")
player_play <- fread("player_play.csv")
df <- left_join(t1, games, by = c("gameId"))
df <- left_join(df, plays, by = c("gameId", "playId"))
df <- left_join(df, players, by = c("nflId"))
df <- left_join(df, player_play, by = c("gameId", "playId", "nflId"))
motion_df <- df %>%
filter(motionSinceLineset == "TRUE") %>%
mutate(pointDifferential = ifelse(possessionTeam == homeTeamAbbr,
preSnapHomeScore - preSnapVisitorScore,
preSnapVisitorScore - preSnapHomeScore)) %>%
mutate(penaltyYards.x = ifelse(is.na(penaltyYards.x), 0, penaltyYards.x)) %>%
mutate(got1D = ifelse(yardsGained - yardsToGo > 0 & yardsGained - penaltyYards.x > yardsToGo, 1, 0)) %>%
mutate(fieldPosition = case_when(
absoluteYardlineNumber < 30 ~ 0,
absoluteYardlineNumber < 60 ~ 1,
absoluteYardlineNumber < 90 ~ 2,
absoluteYardlineNumber >= 90 ~ 3,
)) %>%
mutate(redzone = ifelse(fieldPosition == 0, 1, 0)) %>%
mutate(oppTerritory = ifelse(fieldPosition == 1, 1, 0)) %>%
mutate(ownTerritory = ifelse(fieldPosition == 2, 1, 0)) %>%
mutate(deadzone = ifelse(fieldPosition == 3, 1, 0)) %>%
mutate(is1Q = ifelse(quarter == 1, 1, 0)) %>%
mutate(is2Q = ifelse(quarter == 2, 1, 0)) %>%
mutate(is3Q = ifelse(quarter == 3, 1, 0)) %>%
mutate(is4Q = ifelse(quarter == 4, 1, 0)) %>%
mutate(isOT = ifelse(quarter == 5, 1, 0)) %>%
mutate(isCover0 = ifelse(pff_passCoverage == "Cover-0", 1, 0)) %>%
mutate(isCover1 = ifelse(pff_passCoverage == "Cover-1" | pff_passCoverage == "Cover-1 Double", 1, 0)) %>%
mutate(isCover2 = ifelse(pff_passCoverage == "Cover-2" | pff_passCoverage == "2-Man", 1, 0)) %>%
mutate(isCover3 = ifelse(pff_passCoverage == "Cover-3" | pff_passCoverage == "Cover-3 Cloud Left" | pff_passCoverage == "Cover-3 Cloud Right" | pff_passCoverage == "Cover-3 Seam", 1, 0)) %>%
mutate(isCover4 = ifelse(pff_passCoverage == "Quarters", 1, 0)) %>%
mutate(isCover6 = ifelse(pff_passCoverage == "Cover 6-Left" | pff_passCoverage == "Cover-6 Right", 1, 0)) %>%
mutate(isRZ_GL = ifelse(pff_passCoverage == "Goal Line" | pff_passCoverage == "Red Zone", 1, 0)) %>%
select(motionSinceLineset, playId, pff_passCoverage, down, yardsToGo, pointDifferential,
absoluteYardlineNumber, fieldPosition, redzone, oppTerritory, ownTerritory, deadzone,
yardsGained, penaltyYards.x, got1D, is1Q, is2Q, is3Q, is4Q, isOT, isCover0,
isCover1, isCover2, isCover3, isCover4, isCover6, isRZ_GL) %>%
group_by(pff_passCoverage) %>% distinct() %>%
data.frame()
idx_motion <- sample(seq(1, 2), size = nrow(motion_df), replace = TRUE, prob = c(.77, .23))
train_motion <- motion_df[idx_motion == 1,]
test_motion <- motion_df[idx_motion == 2,]
model_motion <- glm(got1D ~ down + yardsToGo + pointDifferential
+ redzone + oppTerritory + ownTerritory
+ deadzone + is1Q + is2Q
+ is3Q + is4Q + isOT
+ isCover0 + isCover1 + isCover2
+ isCover3 + isCover4 + isCover6
+ isRZ_GL, family = "binomial", data = train_motion)
predictiveModel <- predict(model_motion, newdata = test_motion, type = "response")
# Convert probabilities to binary classifications
test_motion$predicted_class <- ifelse(predictiveModel > 0.5, 1, 0)
# Confusion matrix
confusion_matrix <- table(test_motion$predicted_class, test_motion$got1D)
# Accuracy
accuracy <- sum(diag(confusion_matrix)) / sum(confusion_matrix)
# Print the results
print(confusion_matrix)
##
## 0 1
## 0 194 47
## 1 9 15
print(paste("Accuracy:", accuracy))
## [1] "Accuracy: 0.788679245283019"
cover0_conversion <- motion_df %>%
mutate(cover0_used = ifelse(isCover0 == 1, "Cover 0", "Not Cover 0")) %>%
filter(!is.na(cover0_used)) %>% # Ensure no NA rows are included
group_by(cover0_used) %>%
summarise(conversion_rate = mean(got1D, na.rm = TRUE))
cover1_conversion <- motion_df %>%
mutate(cover1_used = ifelse(isCover1 == 1, "Cover 1", "Not Cover 1")) %>%
filter(!is.na(cover1_used)) %>% # Ensure no NA rows are included
group_by(cover1_used) %>%
summarise(conversion_rate = mean(got1D, na.rm = TRUE))
cover2_conversion <- motion_df %>%
mutate(cover2_used = ifelse(isCover2 == 1, "Cover 2", "Not Cover 2")) %>%
filter(!is.na(cover2_used)) %>% # Ensure no NA rows are included
group_by(cover2_used) %>%
summarise(conversion_rate = mean(got1D, na.rm = TRUE))
cover3_conversion <- motion_df %>%
mutate(cover3_used = ifelse(isCover3 == 1, "Cover 3", "Not Cover 3")) %>%
filter(!is.na(cover3_used)) %>% # Ensure no NA rows are included
group_by(cover3_used) %>%
summarise(conversion_rate = mean(got1D, na.rm = TRUE))
cover4_conversion <- motion_df %>%
mutate(cover4_used = ifelse(isCover4 == 1, "Cover 4", "Not Cover 4")) %>%
filter(!is.na(cover4_used)) %>% # Ensure no NA rows are included
group_by(cover4_used) %>%
summarise(conversion_rate = mean(got1D, na.rm = TRUE))
cover6_conversion <- motion_df %>%
mutate(cover6_used = ifelse(isCover6 == 1, "Cover 6", "Not Cover 6")) %>%
filter(!is.na(cover6_used)) %>% # Ensure no NA rows are included
group_by(cover6_used) %>%
summarise(conversion_rate = mean(got1D, na.rm = TRUE))
RZ_GL_conversion <- motion_df %>%
mutate(RZ_GL_used = ifelse(isRZ_GL == 1, "Redzone/Goaline", "Not Redzone/Goaline")) %>%
filter(!is.na(RZ_GL_used)) %>% # Ensure no NA rows are included
group_by(RZ_GL_used) %>%
summarise(conversion_rate = mean(got1D, na.rm = TRUE))
twoMinuteDrillDF <- motion_df%>%
mutate(NumDenCover0 = exp(-1.876469 + (.2944736 * 1) - (.1724887 * 10) - (.01205532 * -6) - (.4995004 * 0) + (.6761362 * 0) + (.1290696 * 1)
+ (0 * 0) + (.988824 * 0) + (1.330573 * 0) + (1.243216 * 0) + (1.199649 * 1) + (0 * 0) - (2.67033 * 1) + (.06137213 * 0)
- (.2771084 * 0) + (.3625931 * 0) + (.09938819 * 0) + (.3766469 * 0) - (1.51921 * 0))) %>%
mutate(conversionChanceCover0 = NumDenCover0 / (1 - NumDenCover0)) %>%
mutate(NumDenCover1 = exp(-1.876469 + (.2944736 * 1) - (.1724887 * 10) - (.01205532 * -6) - (.4995004 * 0) + (.6761362 * 0) + (.1290696 * 1)
+ (0 * 0) + (.988824 * 0) + (1.330573 * 0) + (1.243216 * 0) + (1.199649 * 1) + (0 * 0) - (2.67033 * 0) + (.06137213 * 1)
- (.2771084 * 0) + (.3625931 * 0) + (.09938819 * 0) + (.3766469 * 0) - (1.51921 * 0))) %>%
mutate(conversionChanceCover1 = NumDenCover1 / (1 - NumDenCover1)) %>%
mutate(NumDenCover2 = exp(-1.876469 + (.2944736 * 1) - (.1724887 * 10) - (.01205532 * -6) - (.4995004 * 0) + (.6761362 * 0) + (.1290696 * 1)
+ (0 * 0) + (.988824 * 0) + (1.330573 * 0) + (1.243216 * 0) + (1.199649 * 1) + (0 * 0) - (2.67033 * 0) + (.06137213 * 0)
- (.2771084 * 1) + (.3625931 * 0) + (.09938819 * 0) + (.3766469 * 0) - (1.51921 * 0))) %>%
mutate(conversionChanceCover2 = NumDenCover2 / (1 - NumDenCover2)) %>%
mutate(NumDenCover3 = exp(-1.876469 + (.2944736 * 1) - (.1724887 * 10) - (.01205532 * -6) - (.4995004 * 0) + (.6761362 * 0) + (.1290696 * 1)
+ (0 * 0) + (.988824 * 0) + (1.330573 * 0) + (1.243216 * 0) + (1.199649 * 1) + (0 * 0) - (2.67033 * 0) + (.06137213 * 0)
- (.2771084 * 0) + (.3625931 * 1) + (.09938819 * 0) + (.3766469 * 0) - (1.51921 * 0))) %>%
mutate(conversionChanceCover3 = NumDenCover3 / (1 - NumDenCover3)) %>%
mutate(NumDenCover4 = exp(-1.876469 + (.2944736 * 1) - (.1724887 * 10) - (.01205532 * -6) - (.4995004 * 0) + (.6761362 * 0) + (.1290696 * 1)
+ (0 * 0) + (.988824 * 0) + (1.330573 * 0) + (1.243216 * 0) + (1.199649 * 1) + (0 * 0) - (2.67033 * 0) + (.06137213 * 0)
- (.2771084 * 0) + (.3625931 * 0) + (.09938819 * 1) + (.3766469 * 0) - (1.51921 * 0))) %>%
mutate(conversionChanceCover4 = NumDenCover4 / (1 - NumDenCover4)) %>%
mutate(NumDenCover6 = exp(-1.876469 + (.2944736 * 1) - (.1724887 * 10) - (.01205532 * -6) - (.4995004 * 0) + (.6761362 * 0) + (.1290696 * 1)
+ (0 * 0) + (.988824 * 0) + (1.330573 * 0) + (1.243216 * 0) + (1.199649 * 1) + (0 * 0) - (2.67033 * 0) + (.06137213 * 0)
- (.2771084 * 0) + (.3625931 * 0) + (.09938819 * 0) + (.3766469 * 1) - (1.51921 * 0))) %>%
mutate(conversionChanceCover6 = NumDenCover6 / (1 - NumDenCover6)) %>%
mutate(NumDenRZGL = exp(-1.876469 + (.2944736 * 1) - (.1724887 * 10) - (.01205532 * -6) - (.4995004 * 0) + (.6761362 * 0) + (.1290696 * 1)
+ (0 * 0) + (.988824 * 0) + (1.330573 * 0) + (1.243216 * 0) + (1.199649 * 1) + (0 * 0) - (2.67033 * 0) + (.06137213 * 0)
- (.2771084 * 0) + (.3625931 * 0) + (.09938819 * 0) + (.3766469 * 0) - (1.51921 * 1))) %>%
mutate(conversionChanceRZGL = NumDenRZGL / (1 - NumDenRZGL)) %>%
select(conversionChanceCover0, conversionChanceCover1, conversionChanceCover2, conversionChanceCover3,
conversionChanceCover4, conversionChanceCover6, conversionChanceRZGL)%>%
data.frame()
twoMinuteDrillDF_long <- twoMinuteDrillDF %>%
pivot_longer(
cols = everything(),
names_to = "CoverageType",
values_to = "ConversionChance"
) %>%
mutate(CoverageType = gsub("conversionChance", "", CoverageType)) %>% # Clean up names
distinct()
# Touchback
touchback_df <- motion_df%>%
mutate(NumDenCover0 = exp(-1.876469 + (.2944736 * 1) - (.1724887 * 10) - (.01205532 * 0) - (.4995004 * 0) + (.6761362 * 0) + (.1290696 * 1)
+ (0 * 0) + (.988824 * 1) + (1.330573 * 0) + (1.243216 * 0) + (1.199649 * 0) + (0 * 0) - (2.67033 * 1) + (.06137213 * 0)
- (.2771084 * 0) + (.3625931 * 0) + (.09938819 * 0) + (.3766469 * 0) - (1.51921 * 0))) %>%
mutate(conversionChanceCover0 = NumDenCover0 / (1 - NumDenCover0)) %>%
mutate(NumDenCover1 = exp(-1.876469 + (.2944736 * 1) - (.1724887 * 10) - (.01205532 * 0) - (.4995004 * 0) + (.6761362 * 0) + (.1290696 * 1)
+ (0 * 0) + (.988824 * 1) + (1.330573 * 0) + (1.243216 * 0) + (1.199649 * 0) + (0 * 0) - (2.67033 * 0) + (.06137213 * 1)
- (.2771084 * 0) + (.3625931 * 0) + (.09938819 * 0) + (.3766469 * 0) - (1.51921 * 0))) %>%
mutate(conversionChanceCover1 = NumDenCover1 / (1 - NumDenCover1)) %>%
mutate(NumDenCover2 = exp(-1.876469 + (.2944736 * 1) - (.1724887 * 10) - (.01205532 * 0) - (.4995004 * 0) + (.6761362 * 0) + (.1290696 * 1)
+ (0 * 0) + (.988824 * 1) + (1.330573 * 0) + (1.243216 * 0) + (1.199649 * 0) + (0 * 0) - (2.67033 * 0) + (.06137213 * 0)
- (.2771084 * 1) + (.3625931 * 0) + (.09938819 * 0) + (.3766469 * 0) - (1.51921 * 0))) %>%
mutate(conversionChanceCover2 = NumDenCover2 / (1 - NumDenCover2)) %>%
mutate(NumDenCover3 = exp(-1.876469 + (.2944736 * 1) - (.1724887 * 10) - (.01205532 * 0) - (.4995004 * 0) + (.6761362 * 0) + (.1290696 * 1)
+ (0 * 0) + (.988824 * 1) + (1.330573 * 0) + (1.243216 * 0) + (1.199649 * 0) + (0 * 0) - (2.67033 * 0) + (.06137213 * 0)
- (.2771084 * 0) + (.3625931 * 1) + (.09938819 * 0) + (.3766469 * 0) - (1.51921 * 0))) %>%
mutate(conversionChanceCover3 = NumDenCover3 / (1 - NumDenCover3)) %>%
mutate(NumDenCover4 = exp(-1.876469 + (.2944736 * 1) - (.1724887 * 10) - (.01205532 * 0) - (.4995004 * 0) + (.6761362 * 0) + (.1290696 * 1)
+ (0 * 0) + (.988824 * 1) + (1.330573 * 0) + (1.243216 * 0) + (1.199649 * 0) + (0 * 0) - (2.67033 * 0) + (.06137213 * 0)
- (.2771084 * 0) + (.3625931 * 0) + (.09938819 * 1) + (.3766469 * 0) - (1.51921 * 0))) %>%
mutate(conversionChanceCover4 = NumDenCover4 / (1 - NumDenCover4)) %>%
mutate(NumDenCover6 = exp(-1.876469 + (.2944736 * 1) - (.1724887 * 10) - (.01205532 * 0) - (.4995004 * 0) + (.6761362 * 0) + (.1290696 * 1)
+ (0 * 0) + (.988824 * 1) + (1.330573 * 0) + (1.243216 * 0) + (1.199649 * 0) + (0 * 0) - (2.67033 * 0) + (.06137213 * 0)
- (.2771084 * 0) + (.3625931 * 0) + (.09938819 * 0) + (.3766469 * 1) - (1.51921 * 0))) %>%
mutate(conversionChanceCover6 = NumDenCover6 / (1 - NumDenCover6)) %>%
mutate(NumDenRZGL = exp(-1.876469 + (.2944736 * 1) - (.1724887 * 10) - (.01205532 * 0) - (.4995004 * 0) + (.6761362 * 0) + (.1290696 * 1)
+ (0 * 0) + (.988824 * 1) + (1.330573 * 0) + (1.243216 * 0) + (1.199649 * 0) + (0 * 0) - (2.67033 * 0) + (.06137213 * 0)
- (.2771084 * 0) + (.3625931 * 0) + (.09938819 * 0) + (.3766469 * 0) - (1.51921 * 1))) %>%
mutate(conversionChanceRZGL = NumDenRZGL / (1 - NumDenRZGL)) %>%
select(conversionChanceCover0, conversionChanceCover1, conversionChanceCover2, conversionChanceCover3,
conversionChanceCover4, conversionChanceCover6, conversionChanceRZGL)%>%
data.frame()
touchback_df_long <- touchback_df %>%
pivot_longer(
cols = everything(),
names_to = "CoverageType",
values_to = "ConversionChance"
) %>%
mutate(CoverageType = gsub("conversionChance", "", CoverageType)) %>% # Clean up names
distinct()
ggplot(twoMinuteDrillDF_long, aes(x = CoverageType, y = ConversionChance, fill = CoverageType)) +
geom_bar(stat = "identity") +
labs(
title = "Conversion Chance by Coverage Type In a Two Minute Drill On First & Ten In Opponent's Terrirory",
x = "Coverage Type",
y = "Conversion Chance"
) +
theme_minimal() +
scale_fill_brewer(palette = "Set2") + # Optional color scheme
theme(
axis.text.x = element_text(angle = 45, hjust = 1) # Tilt x-axis labels for readability
)+
geom_text(aes(label = scales::percent(ConversionChance)),
vjust = 0, size = 5)
ggplot(touchback_df_long, aes(x = CoverageType, y = ConversionChance, fill = CoverageType)) +
geom_bar(stat = "identity") +
labs(
title = "Conversion Chance by Coverage Type After the Opening Kickoff Goes For A Touchback",
x = "Coverage Type",
y = "Conversion Chance"
) +
theme_minimal() +
scale_fill_brewer(palette = "Set2") + # Optional color scheme
theme(
axis.text.x = element_text(angle = 45, hjust = 1) # Tilt x-axis labels for readability
)+
geom_text(aes(label = scales::percent(ConversionChance)),
vjust = 0, size = 5)
ggplot(cover0_conversion, aes(x = cover0_used, y = conversion_rate, fill = cover0_used)) +
geom_bar(stat = "identity", width = 0.6) +
scale_fill_manual(values = c("Cover 0" = "steelblue", "Not Cover 0" = "gray")) +
labs(title = "Conversion Rates Based on Cover 0 Usage",
x = "Coverage Type",
y = "Conversion Rate") +
theme_minimal() +
theme(legend.position = "none") +
geom_text(aes(label = scales::percent(conversion_rate)),
vjust = 0, size = 5)
ggplot(cover1_conversion, aes(x = cover1_used, y = conversion_rate, fill = cover1_used)) +
geom_bar(stat = "identity", width = 0.6) +
scale_fill_manual(values = c("Cover 1" = "steelblue", "Not Cover 1" = "gray")) +
labs(title = "Conversion Rates Based on Cover 1 Usage",
x = "Coverage Type",
y = "Conversion Rate") +
theme_minimal() +
theme(legend.position = "none") +
geom_text(aes(label = scales::percent(conversion_rate)),
vjust = 0, size = 5)
ggplot(cover2_conversion, aes(x = cover2_used, y = conversion_rate, fill = cover2_used)) +
geom_bar(stat = "identity", width = 0.6) +
scale_fill_manual(values = c("Cover 2" = "steelblue", "Not Cover 2" = "gray")) +
labs(title = "Conversion Rates Based on Cover 2 Usage",
x = "Coverage Type",
y = "Conversion Rate") +
theme_minimal() +
theme(legend.position = "none") +
geom_text(aes(label = scales::percent(conversion_rate)),
vjust = 0, size = 5)
ggplot(cover3_conversion, aes(x = cover3_used, y = conversion_rate, fill = cover3_used)) +
geom_bar(stat = "identity", width = 0.6) +
scale_fill_manual(values = c("Cover 3" = "steelblue", "Not Cover 3" = "gray")) +
labs(title = "Conversion Rates Based on Cover 3 Usage",
x = "Coverage Type",
y = "Conversion Rate") +
theme_minimal() +
theme(legend.position = "none") +
geom_text(aes(label = scales::percent(conversion_rate)),
vjust = 0, size = 5)
ggplot(cover4_conversion, aes(x = cover4_used, y = conversion_rate, fill = cover4_used)) +
geom_bar(stat = "identity", width = 0.6) +
scale_fill_manual(values = c("Cover 4" = "steelblue", "Not Cover 4" = "gray")) +
labs(title = "Conversion Rates Based on Cover 4 Usage",
x = "Coverage Type",
y = "Conversion Rate") +
theme_minimal() +
theme(legend.position = "none") +
geom_text(aes(label = scales::percent(conversion_rate)),
vjust = 0, size = 5)
ggplot(cover6_conversion, aes(x = cover6_used, y = conversion_rate, fill = cover6_used)) +
geom_bar(stat = "identity", width = 0.6) +
scale_fill_manual(values = c("Cover 6" = "steelblue", "Not Cover 6" = "gray")) +
labs(title = "Conversion Rates Based on Cover 6 Usage",
x = "Coverage Type",
y = "Conversion Rate") +
theme_minimal() +
theme(legend.position = "none") +
geom_text(aes(label = scales::percent(conversion_rate)),
vjust = 0, size = 5)
ggplot(RZ_GL_conversion, aes(x = RZ_GL_used, y = conversion_rate, fill = RZ_GL_used)) +
geom_bar(stat = "identity", width = 0.6) +
scale_fill_manual(values = c("Redzone/Goaline" = "steelblue", "Not Redzone/Goaline" = "gray")) +
labs(title = "Conversion Rates Based on Redzone & Goaline Defense Usage",
x = "Coverage Type",
y = "Conversion Rate") +
theme_minimal() +
theme(legend.position = "none") +
geom_text(aes(label = scales::percent(conversion_rate)),
vjust = 0, size = 5)
We can conclude from our predictive model that cover 0 tends to limit first down odds the best of all coverages we analyzed, excluding redzone and goaline defense due to teams not running a lot of motion on the goaline. This was followed by cover 2, cover 4, cover 6, cover 3 and cover 1. In football terms this makes sense because cover 1, 3, 6 and 4 all tend to not guard the flats as frequently, meaning that they are willing to give up the short play in order to prevent a big play. The specific situation graphs make this accionable because when looking at the two situations- First and ten after a touchback on the opening kick-off, and first and ten in a two minute drill in opponents territory down a touchdown- you can see what coverages would be best to run to limit yards gained and moreover a first down. Obviously a defense needs to switch up its looks, so looking at the lowest three coverages gives the devensive coordinator enough variety in his playcalling. In these two examples it would best to run either cover 0, cover 2 or cover 1 in order to prevent the offense from getting a first down.