This project builds upon an initial code demonstration by my professor, which visualized a single play from a January 3, 2021 game between Buffalo and Miami, where Isaiah McKenzie returned the ball for an 84-yard touchdown despite heavy defensive pressure. In the original code, each player is represented by a circle (Miami in blue, Buffalo in red), with analytics displayed to indicate opposing traffic. A halo surrounding McKenzie (#19) changes color based on the proximity of Miami players, growing darker as opposing players approach, effectively illustrating the increasing difficulty he faced as he moved toward the end zone.
To expand this analysis, I introduced an ideal path for McKenzie to see how closely his actual path aligned with an optimal route that could have further reduced his chances of being tackled or pushed out of bounds. I determined McKenzie’s location and the locations of his two nearest defenders in each frame, calculating the midpoint between these defenders as an ideal position. Since the play is oriented toward the end zone, I only considered y-coordinates, which effectively represent lateral movement. I also started analyzing at frame 82 because this is when McKanzie caught the ball.
I created two visualizations to represent the data:
Frame-by-Frame Comparison: This plot shows both McKenzie’s y-values and the ideal y-values over time. The x-axis represents each frame in the play, with McKenzie’s y-coordinates plotted in red and the ideal y-coordinates in green. The shaded are in between the lines shows when Mckensie was above the ideal path (blue shading) and when he was below the ideal path (red shading).
Field Plot: This plot depicts the paths on the field, with McKenzie’s actual path shown in yellow and the ideal path in green. This provides a clear visual representation of each route as it would appear on the field.
load_packages <- function(packages = c()) {
if (length(packages) == 0) {
print('You did not specify any packages/libraries to load')}
else {
for (i in packages){
if(! i %in% installed.packages()){
install.packages(i, dependencies = TRUE)}
suppressMessages(suppressWarnings(library(i, character.only=T)))}}}
# ---------- main code starts here --------------------------------------------------------------
load_packages(c("ggplot2", "ggalt", "ggforce", "hms", "gganimate", "lubridate", "data.table", "dplyr", "nflfastR", "gifski", "png", "ggimage", "ggh4x"))
df_tracking <- fread("Data/NFLBDB2022/NFL2022/tracking2020.csv")
df_plays <- fread("Data/NFLBDB2022/NFL2022/plays.csv")
df_games <- fread("Data/NFLBDB2022/NFL2022/games.csv")
# you are creating a metric for a player in a play (playId) in a game (gameId)
my_gameId <- 2021010300
my_playId <- 1586
my_playerNumber <- 19
df <- df_tracking %>%
filter(gameId == my_gameId & playId == my_playId ) %>%
left_join(df_plays, by = c("playId" = "playId", "gameId" = "gameId")) %>%
select(x, y, displayName, jerseyNumber, team, gameId, playId, frameId, time,
nflId, dis, playDescription, s) %>%
data.frame()
team1 <- as.character( df_games[df_games$gameId == my_gameId, "homeTeamAbbr"] )
team2 <- as.character( df_games[df_games$gameId == my_gameId, "visitorTeamAbbr"] )
df_team1 <- teams_colors_logos %>%
filter(team_abbr == team1)
df_team2 <- teams_colors_logos %>%
filter(team_abbr == team2)
df_player1 <- df %>%
filter(jerseyNumber == my_playerNumber) %>%
select(gameId, playId, frameId, x, y, dis, team) %>%
data.frame()
#-------- create my data frames -------------------------------------------------------------
midpoints_df <- df %>% #df of midpoints of the 2 closest players to McKenzie by frame
filter(team == "away" | displayName == "Isaiah McKenzie")%>%
mutate(Isaiah_x = ifelse(displayName == "Isaiah McKenzie", x, 0),
Isaiah_y = ifelse(displayName == "Isaiah McKenzie", y, 0),
dist_to_Isaiah = ifelse(displayName != "Isaiah McKenzie",
sqrt((Isaiah_x - x )^2 + (Isaiah_y - y)^2),0 ))%>%
filter(displayName!= "Isaiah McKenzie")%>%
group_by(frameId)%>%
arrange(dist_to_Isaiah)%>%
slice_head(n = 2)%>%
summarize(midpoint_x = mean(x), midpoint_y = mean(y))%>%
data.frame()
positions <- df %>% # df of midpoints and McKenzie's location by frame
filter( displayName == "Isaiah McKenzie" & frameId > 106)%>%
left_join(midpoints_df, by = "frameId")%>%
mutate(dist_to_midpoint = sqrt((x - midpoint_x )^2 + (y - midpoint_y)^2),0 )%>%
data.frame()
ggplot(positions, aes(x = frameId)) +
geom_line(aes(y = midpoint_y, color = "Ideal Path"), size = 1.2) +
geom_line(aes(y = y, color = "McKenzie's Path"), size = 1.2) +
stat_difference(aes(ymin = y, ymax = midpoint_y), alpha = 0.3) +
scale_color_manual(values = c("McKenzie's Path" = "red", "Ideal Path" = "green")) +
labs(title = "McKenzie's Path vs. Ideal Path", x = "Frame", y = "Y Position", color = "Path Type") +
theme_minimal() +
theme(legend.position = "bottom")
source('https://raw.githubusercontent.com/mlfurman3/gg_field/main/gg_field.R')
ggplot(positions,aes(x = x)) +
gg_field(yardmin = -5, yardmax = 125) +
geom_point(aes(y = y,color = "McKenzie's Path"), shape = 16, size = 2,) +
geom_point(aes(y = midpoint_y, color = "Ideal Path"), shape = 16, size = 2) +
scale_color_manual(values = c("McKenzie's Path" = "yellow", "Ideal Path" = "green")) +
labs(title = "McKenzie's Path vs. Ideal Path", color = "Path") +
theme_minimal() +
theme(legend.position = "bottom")
In conclusion, McKenzie’s path was mostly aligned with the ideal path. The distance from his path and the ideal path is greatest between the 50 to 30 yard lines. We can see that he corrected this around the 30 yard line to avoid moving closer to the bottom sideline. Both plots suggest that staying closer to the field center earlier on in the play could have helped him avoid sideline pressure towards the endzone. In the frame-by-frame comparison, we see McKenzie remaining close to the ideal y-values, only moving above them briefly before staying below until he reached the end zone. The field plot reinforces this finding, demonstrating that the ideal path would have kept him more central and further away from the risk of being forced out of bounds.
If you have any questions, please feel free to email me at abademosi@loyola.edu