{"metadata":{"kernelspec":{"name":"ir","display_name":"R","language":"R"},"language_info":{"name":"R","codemirror_mode":"r","pygments_lexer":"r","mimetype":"text/x-r-source","file_extension":".r","version":"4.0.5"}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"The focus on the 2021 - 2022 Big Data Bowl is special teams. This demo will provide sample code to help with the process of getting started. This demo includes sample code to do the following:\n\n[What is Kick Attempt Offset from Center?](#whatis) - explains the metric of interest and how it can be used to analyze kickers.\n\n[Read Data](#ReadData) - will read the tracking / non-tracking data.\n\n[Clean Data](#CleanData) - will align the tracking data x-y coordinates with the kick direction and filter to what is needed for this analysis\n\n[Animate Plays](#AnimatePlays) - will animate a play with high/low offset from center.\n\n[Visualize Metric by Kicker](#FieldGoalAccuracy) - will create a few plots showing the metric by place kicker.","metadata":{}},{"cell_type":"markdown","source":"<a id=\"whatis\"></a>\n# What is Kick Attempt Offset from Center?\n\nWhen NFL place kickers come on to the field for a field goal or extra point play, they attempt to kick the football between the uprights that are located behind the endzone in order to score points for their team:\n\n\n<img src=\"https://clipartstation.com/wp-content/uploads/2018/09/goal-post-football-clipart-3.jpg\" style=\"width:300px;\">\n\n\nWhether the ball goes right down the middle or sneaks within the uprights by an inch does not have an effect on points scored. However, kickers typically attempt to place the ball in the middle of the uprights as that increases the chances of a succesful field goal or extra point. Thus, the place kicker's <b> offset from center </b> can be measured as the absolute difference between where the ball went through the uprights and the center point of the uprights. The lower the offset from center, the more accurate the kick.","metadata":{}},{"cell_type":"markdown","source":"<a id=\"ReadData\"></a>\n## Reading Data\n","metadata":{}},{"cell_type":"code","source":"#Loading pre-installed libraries\nlibrary(tidyverse)\nlibrary(gganimate)\nlibrary(cowplot)\nlibrary(ggridges)\nlibrary(repr)\n\n\n#turning off warnings\noptions(warn=-1)\n\n#setting plot width and height\noptions(repr.plot.width=15, repr.plot.height = 10) ","metadata":{},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"For this work, we will use the plays, players and tracking data.\n\n* plays.csv\n* players.csv\n* tracking[s].csv","metadata":{}},{"cell_type":"code","source":"##reading in non-tracking data\n\n#includes play-by-play info on specific plays\ndf_plays <- read_csv(\"../input/nfl-big-data-bowl-2022/plays.csv\",\n                    col_types = cols())\n\n#includes background info for players\ndf_players <- read_csv(\"../input/nfl-big-data-bowl-2022/players.csv\",\n                      col_types = cols())","metadata":{"execution":{"iopub.status.busy":"2021-09-24T17:28:12.57512Z","iopub.execute_input":"2021-09-24T17:28:12.576754Z","iopub.status.idle":"2021-09-24T17:28:13.746839Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"head(df_players)","metadata":{"execution":{"iopub.status.busy":"2021-09-24T17:28:13.780506Z","iopub.execute_input":"2021-09-24T17:28:13.782002Z","iopub.status.idle":"2021-09-24T17:28:13.806728Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"head(df_plays)","metadata":{"execution":{"iopub.status.busy":"2021-09-24T17:28:13.912962Z","iopub.execute_input":"2021-09-24T17:28:13.914817Z","iopub.status.idle":"2021-09-24T17:28:13.952036Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"##Reading tracking data (needs to be done iteratively)\n\n#weeks of NFL season\nseasons <- seq(2018, 2020)\n\n#blank dataframe to store tracking data\ndf_tracking <- data.frame()\n\n#iterating through all weeks\nfor(s in seasons){\n    \n    #temperory dataframe used for reading season for given iteration\n    df_tracking_temp <- read_csv(paste0(\"../input/nfl-big-data-bowl-2022/tracking\",s,\".csv\"),\n                                col_types = cols())\n    \n    #storing temporary dataframe in full season dataframe\n    df_tracking <- bind_rows(df_tracking_temp, df_tracking)                            \n    \n}","metadata":{"execution":{"iopub.status.busy":"2021-09-24T17:28:16.742847Z","iopub.execute_input":"2021-09-24T17:28:16.744574Z","iopub.status.idle":"2021-09-24T17:30:00.415187Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"head(df_tracking)","metadata":{"execution":{"iopub.status.busy":"2021-09-24T17:30:00.417943Z","iopub.execute_input":"2021-09-24T17:30:00.41943Z","iopub.status.idle":"2021-09-24T17:30:00.446941Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"<a id=\"CleanData\"></a>\n# Cleaning Data","metadata":{}},{"cell_type":"markdown","source":"The tracking data is laid out in absolute coordinates and does not flip with the change of possession or end of quarter. The code below flips the coordinates so they always align with the direction of the kicking team.","metadata":{}},{"cell_type":"code","source":"#Standardizing tracking data so its always in direction of kicking team.\ndf_tracking <- df_tracking %>%\n                mutate(x = ifelse(playDirection == \"left\", 120-x, x),\n                       y = ifelse(playDirection == \"left\", 160/3 - y, y))","metadata":{"execution":{"iopub.status.busy":"2021-09-24T18:19:57.056115Z","iopub.execute_input":"2021-09-24T18:19:57.058119Z","iopub.status.idle":"2021-09-24T18:20:05.208258Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"Filtering for information of interest from tracking data. This dataframe will store where ball was when kick is crosses uprights.","metadata":{}},{"cell_type":"code","source":"#will store where ball was when kick is crosses uprights\ndf_ballFieldGoal <- df_tracking %>%\n\n        #filtering for football\n        filter( displayName == \"football\") %>%\n\n        #selecting plays as ball crosses through uprights\n        #grouping by gameId and playId\n        group_by(gameId, playId) %>%\n        \n        arrange(gameId, playId, frameId) %>%\n\n        #filtering for play immediately after ball goes through uprights\n        filter(lag(x) < 120, x >= 120) %>%\n\n        #selecting first occurence in case it crosses uprights multiple times\n        filter(row_number() == 1) %>%\n\n        #ungrouping\n        ungroup() %>%\n\n        #creating variable for absolute offset from center.\n        #Center of field is at coordinate 160/6\n        mutate(offsetFromCenter = abs(y - 160/6)) %>%\n\n\n        #selecting offsetFromCenter and key variables only\n        select(gameId, playId, offsetFromCenter)\n","metadata":{"execution":{"iopub.status.busy":"2021-09-24T18:20:05.440392Z","iopub.execute_input":"2021-09-24T18:20:05.442221Z","iopub.status.idle":"2021-09-24T18:20:10.545658Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"Merging the previous dataframe to the plays and players data to have final dataframe used for this analysis.","metadata":{}},{"cell_type":"code","source":"#storing kick offset for each kicker on each play\ndf_fieldGoalAnalysis <- df_plays %>%\n\n    #filtering for unblocked extra points and field goals only\n    filter(specialTeamsResult %in% c(\"Kick Attempt Good\",\n                                     \"Kick Attempt No Good\"),\n          \n    #using play description to remove attempts that were missed short           \n           !grepl('No Good, Short', playDescription)) %>%\n\n    #kickLength is sometimes missing on extra points.\n    #In that case we use impute as yards from target endzone + 18.\n    mutate(yardsFromTargetEndzone = case_when(yardlineNumber == 50 ~ 50,\n                                              \n                                             possessionTeam == yardlineSide ~\n                                              50 + yardlineNumber,\n                                              \n                                             possessionTeam != yardlineSide ~\n                                              yardlineNumber),\n           \n           #imputing kick length as yards from target endzone + 18\n           kickLength = ifelse(is.na(kickLength),\n                               yardsFromTargetEndzone + 18,\n                               kickLength)) %>%\n\n    #joining players by kickerId to get displayName\n    inner_join(df_players, by = c('kickerId' = 'nflId')) %>%\n\n    #joining filtered tracking data\n    inner_join(df_ballFieldGoal, by = c(\"gameId\", 'playId')) %>%\n    \n    #selecting only variables of interest:\n    select(gameId, playId,  displayName,\n           kickLength, offsetFromCenter,\n           playDescription)","metadata":{"execution":{"iopub.status.busy":"2021-09-24T18:20:10.548458Z","iopub.execute_input":"2021-09-24T18:20:10.550069Z","iopub.status.idle":"2021-09-24T18:20:10.69642Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"df_fieldGoalAnalysis %>% head()","metadata":{"execution":{"iopub.status.busy":"2021-09-24T18:20:10.698917Z","iopub.execute_input":"2021-09-24T18:20:10.700347Z","iopub.status.idle":"2021-09-24T18:20:10.732714Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"<a id=\"AnimatePlays\"></a>\n# Animating Plays","metadata":{}},{"cell_type":"markdown","source":"To get a sense of what the data looks like, it may be helpful to animate a few plays.","metadata":{}},{"cell_type":"code","source":"#loading command to make NFL field in ggplot (credit to Marschall Furman)\nsource('https://raw.githubusercontent.com/mlfurman3/gg_field/main/gg_field.R')","metadata":{"execution":{"iopub.status.busy":"2021-09-24T18:27:30.426418Z","iopub.execute_input":"2021-09-24T18:27:30.428089Z","iopub.status.idle":"2021-09-24T18:27:31.075158Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"## Animating Lowest Offset from Center 50-yard Kick Attempt (most accurate kick)","metadata":{}},{"cell_type":"code","source":"#picking the lowest offset play\n\nexample_play <- df_fieldGoalAnalysis %>%\n                filter(kickLength == 50) %>%\n                filter(offsetFromCenter == min(offsetFromCenter))\n\n#merging tracking data to play\nexample_play <- inner_join(example_play,\n                           df_tracking,\n                           by = c(\"gameId\" = \"gameId\",\n                                  \"playId\" = \"playId\"))","metadata":{"execution":{"iopub.status.busy":"2021-09-24T18:58:44.153874Z","iopub.execute_input":"2021-09-24T18:58:44.155885Z","iopub.status.idle":"2021-09-24T18:58:45.998876Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"#upright dimensions\nuprightLength = 18.5/3\nuprightYardline = 120\nuprightAccrossFieldLocation = 160/6\nuprightColor = \"#E8DE35\"\nuprightlineWidth = 2\nuprightShape = 21\nuprightSize = 4\nuprightOutlineColor = 'black'\n\n#attributes used for plot. first is away, second is football, third is home.\ncols_fill <- c(\"dodgerblue1\", \"#663300\", \"firebrick1\")\ncols_col <- c(\"#000000\", \"#663300\", \"#000000\")\nsize_vals <- c(6, 4, 6)\nshape_vals <- c(21, 16, 21)\nplot_title <- example_play$playDescription[1]\nnFrames <- max(example_play$frameId)\n\n#plotting\nanim <- ggplot() +\n\n\n        #creating field underlay\n        gg_field(yardmin = 65, yardmax = 122) +\n\n        #filling forest green for behind back of endzone\n        theme(panel.background = element_rect(fill = 'forestgreen',\n                                              color = 'forestgreen'),\n              panel.grid = element_blank()) +\n\n\n        #adding field goal uprights\n        annotate(geom = 'segment',\n                 x = uprightYardline,\n                 xend = uprightYardline,\n                 y = uprightAccrossFieldLocation + uprightLength/2,\n                 yend = uprightAccrossFieldLocation - uprightLength/2,\n                 color = uprightColor,\n                 lwd = uprightlineWidth) +\n\n        annotate(geom = 'point',\n                 x = uprightYardline,\n                 y = uprightAccrossFieldLocation + uprightLength/2,\n                 size = uprightSize,\n                 shape = uprightShape,\n                 fill = uprightColor,\n                 color = uprightOutlineColor) +\n\n        annotate(geom = 'point',\n                 x = uprightYardline,\n                 y = uprightAccrossFieldLocation - uprightLength/2,\n                 size = uprightSize,\n                 shape = uprightShape,\n                 fill = uprightColor,\n                 color = uprightOutlineColor) +\n\n\n        #setting size and color parameters\n        scale_size_manual(values = size_vals, guide = FALSE) + \n        scale_shape_manual(values = shape_vals, guide = FALSE) +\n        scale_fill_manual(values = cols_fill, guide = FALSE) + \n        scale_colour_manual(values = cols_col, guide = FALSE) +\n\n\n        #adding players\n        geom_point(data = example_play, aes(x = x,\n                                          y = y, \n                                          shape = team,\n                                          fill = team,\n                                          group = nflId,\n                                          size = team,\n                                          colour = team), \n                 alpha = 0.7) +  \n\n        #adding jersey numbers\n        geom_text(data = example_play,\n                  aes(x = x, y = y, label = jerseyNumber),\n                  colour = \"white\", \n                vjust = 0.36, size = 3.5) + \n\n\n        #titling plot with play description\n        labs(title = plot_title) +\n\n        #setting animation parameters\n        transition_time(frameId)  +\n        ease_aes('linear') + \n        NULL \n\n\n#saving animation to display in markdown cell below:\nanim_save('LowestOffsetAnimation.gif',\n          animate(anim, width = 720, height = 440,\n                  fps = 10, nframe = nFrames))","metadata":{"execution":{"iopub.status.busy":"2021-09-24T19:00:23.266761Z","iopub.execute_input":"2021-09-24T19:00:23.269717Z","iopub.status.idle":"2021-09-24T19:00:37.734439Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"<img src=\"./LowestOffsetAnimation.gif\">","metadata":{}},{"cell_type":"markdown","source":"## Animating Highest Offset from Center 50-yard Kick Attempt (least accurate kick)","metadata":{}},{"cell_type":"code","source":"#picking the highest offset play\n\nexample_play <- df_fieldGoalAnalysis %>%\n                filter(kickLength == 50) %>%\n                filter(offsetFromCenter == max(offsetFromCenter))\n\n#merging tracking data to play\nexample_play <- inner_join(example_play,\n                           df_tracking,\n                           by = c(\"gameId\" = \"gameId\",\n                                  \"playId\" = \"playId\"))","metadata":{"execution":{"iopub.status.busy":"2021-09-24T18:56:52.667705Z","iopub.execute_input":"2021-09-24T18:56:52.669679Z","iopub.status.idle":"2021-09-24T18:56:54.541634Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"#upright dimensions\nuprightLength = 18.5/3\nuprightYardline = 120\nuprightAccrossFieldLocation = 160/6\nuprightColor = \"#E8DE35\"\nuprightlineWidth = 2\nuprightShape = 21\nuprightSize = 4\nuprightOutlineColor = 'black'\n\n#attributes used for plot. first is away, second is football, third is home.\ncols_fill <- c(\"dodgerblue1\", \"#663300\", \"firebrick1\")\ncols_col <- c(\"#000000\", \"#663300\", \"#000000\")\nsize_vals <- c(6, 4, 6)\nshape_vals <- c(21, 16, 21)\nplot_title <- example_play$playDescription[1]\nnFrames <- max(example_play$frameId)\n\n#plotting\nanim <- ggplot() +\n\n\n        #creating field underlay\n        gg_field(yardmin = 65, yardmax = 122) +\n\n        #filling forest green for behind back of endzone\n        theme(panel.background = element_rect(fill = 'forestgreen',\n                                              color = 'forestgreen'),\n              panel.grid = element_blank()) +\n\n\n        #adding field goal uprights\n        annotate(geom = 'segment',\n                 x = uprightYardline,\n                 xend = uprightYardline,\n                 y = uprightAccrossFieldLocation + uprightLength/2,\n                 yend = uprightAccrossFieldLocation - uprightLength/2,\n                 color = uprightColor,\n                 lwd = uprightlineWidth) +\n\n        annotate(geom = 'point',\n                 x = uprightYardline,\n                 y = uprightAccrossFieldLocation + uprightLength/2,\n                 size = uprightSize,\n                 shape = uprightShape,\n                 fill = uprightColor,\n                 color = uprightOutlineColor) +\n\n        annotate(geom = 'point',\n                 x = uprightYardline,\n                 y = uprightAccrossFieldLocation - uprightLength/2,\n                 size = uprightSize,\n                 shape = uprightShape,\n                 fill = uprightColor,\n                 color = uprightOutlineColor) +\n\n\n        #setting size and color parameters\n        scale_size_manual(values = size_vals, guide = FALSE) + \n        scale_shape_manual(values = shape_vals, guide = FALSE) +\n        scale_fill_manual(values = cols_fill, guide = FALSE) + \n        scale_colour_manual(values = cols_col, guide = FALSE) +\n\n\n        #adding players\n        geom_point(data = example_play, aes(x = x,\n                                          y = y, \n                                          shape = team,\n                                          fill = team,\n                                          group = nflId,\n                                          size = team,\n                                          colour = team), \n                 alpha = 0.7) +  \n\n        #adding jersey numbers\n        geom_text(data = example_play,\n                  aes(x = x, y = y, label = jerseyNumber),\n                  colour = \"white\", \n                vjust = 0.36, size = 3.5) + \n\n\n        #titling plot with play description\n        labs(title = plot_title) +\n\n        #setting animation parameters\n        transition_time(frameId)  +\n        ease_aes('linear') + \n        NULL \n\n\n#saving animation to display in markdown cell below:\nanim_save('HighestOffsetAnimation.gif',\n          animate(anim, width = 720, height = 440,\n                  fps = 10, nframe = nFrames))","metadata":{"execution":{"iopub.status.busy":"2021-09-24T18:57:43.50209Z","iopub.execute_input":"2021-09-24T18:57:43.504155Z","iopub.status.idle":"2021-09-24T18:58:00.260324Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"<img src=\"./HighestOffsetAnimation.gif\">","metadata":{}},{"cell_type":"markdown","source":"<a id=\"FieldGoalAccuracy\"></a>\n# Visualize Metric by Kicker\n\nHere we will compare NFL kickers with our offset from center metric. First we make a bar graph:","metadata":{}},{"cell_type":"code","source":"df_fieldGoalAnalysis %>%\n\n    #filtering for length between 30 and 40 yards\n    filter(kickLength >= 30,\n           kickLength <= 40) %>%\n    \n    #grouping by kickerId\n    group_by(displayName) %>%\n\n    #filtering for only kickers with 75+ attempts\n    filter(n() >= 75) %>%\n\n    #taking mean of data\n    summarize(avgOffsetFromCenter = mean(offsetFromCenter)) %>%\n\n    ungroup() %>%\n\n    ggplot(aes(avgOffsetFromCenter,\n               reorder(displayName, -avgOffsetFromCenter))) +\n\n    geom_bar(stat = 'identity', fill = 'lightblue') +\n    \n    theme_bw() +\n    theme(text = element_text(size=22),\n         plot.title = element_text(hjust = 0.5),\n         plot.subtitle = element_text(hjust = 0.5)) +\n\n    xlab('Average Offset from Center') +\n\n    ylab('') +\n\n    ggtitle(\"Avg Offset from Center by Kicker on 30 to 40 yard Attempts\") +\n    labs(subtitle = '(Ordered from Best to Worst)')\n","metadata":{"execution":{"iopub.status.busy":"2021-09-24T19:06:42.749868Z","iopub.execute_input":"2021-09-24T19:06:42.751683Z","iopub.status.idle":"2021-09-24T19:06:43.132432Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"Next we make a ridge plot:","metadata":{}},{"cell_type":"code","source":"df_fieldGoalAnalysis %>%\n\n    #filtering for length between 30 and 40 yards\n    filter(kickLength >= 30,\n           kickLength <= 40) %>%\n    \n    #grouping by kickerId\n    group_by(displayName) %>%\n\n    #filtering for only kickers with 75+ attempts\n    filter(n() >= 75) %>%\n\n    #taking mean of data to use as order for ridge plot\n    mutate(order = mean(offsetFromCenter)) %>%\n\n    ungroup() %>%\n\n    ggplot(aes(offsetFromCenter, reorder(displayName, -order))) +\n\n    geom_density_ridges(fill = 'lightblue') +\n    \n    theme_bw() +\n    theme(text = element_text(size=22),\n         plot.title = element_text(hjust = 0.5),\n         plot.subtitle = element_text(hjust = 0.5)) +\n\n    xlab('Kick Attempt Offset from Center Values') +\n\n    ylab('') +\n\n    ggtitle(\"Density of Offset from Center by Kicker on 30 to 40 yard Attempts\") +\n    labs(subtitle = '(Ordered from Best to Worst)')","metadata":{"execution":{"iopub.status.busy":"2021-09-24T19:12:57.124296Z","iopub.execute_input":"2021-09-24T19:12:57.126817Z","iopub.status.idle":"2021-09-24T19:12:57.814884Z"},"trusted":true},"execution_count":null,"outputs":[]}]}