{"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":"Given the special teams data, our team decided to explore punts, which we assumed to be the least studied aspect of special teams, considering it rarely produces points on its own. Specifically, we focused on punt returns and comparing returns interactively within and between players. To begin, we loaded the necessary packages, and the player, game, and tracking data.","metadata":{}},{"cell_type":"code","source":"library(tidyverse)\ninstall.packages(\"edeaR\")\nlibrary(edeaR)\ninstall.packages(\"sportyR\")\nlibrary(sportyR)\ngames <- read.csv(\"../input/nfl-big-data-bowl-2022/games.csv\")\nscouting <- read.csv(\"../input/nfl-big-data-bowl-2022/PFFScoutingData.csv\")\nplayers <- read.csv(\"../input/nfl-big-data-bowl-2022/players.csv\")\nplays <- read.csv(\"../input/nfl-big-data-bowl-2022/plays.csv\")\ntracking2018 <- read.csv(\"../input/nfl-big-data-bowl-2022/tracking2018.csv\")\ntracking2019 <- read.csv(\"../input/nfl-big-data-bowl-2022/tracking2019.csv\")\ntracking2020 <- read.csv(\"../input/nfl-big-data-bowl-2022/tracking2020.csv\")","metadata":{"execution":{"iopub.status.busy":"2022-01-05T15:41:15.943431Z","iopub.execute_input":"2022-01-05T15:41:15.982561Z","iopub.status.idle":"2022-01-05T15:47:37.196193Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"After filtering the play data to only include punting plays, we began exploratory analysis, which included familiarizing ourselves with variables of interest, such as return length and kick length, and plotting single variable distributions for these and other variables. We moved forward from univariate analysis to explore the relationship between kick length and return length, hypothesizing that longer punts would lead to longer returns. However, the correlation between these two variables was low, only 0.18, and the resulting scatter plot confirmed the weak linear relationship between kick and return length. As a result, we turned to a deeper dive into returns alone, focusing on the paths of each returner using player tracking data. ","metadata":{}},{"cell_type":"code","source":"punt_plays <- plays %>% filter(specialTeamsPlayType==\"Punt\")\n\nhist(punt_plays$kickLength, main=\"Distribution of Kick Lengths\", xlab=\"Kick Length\")\nhist(punt_plays$kickReturnYardage, main=\"Distribution of Return Lengths\", xlab=\"Return Length\")\n\n\npunt_plays %>% ggplot(aes(kickLength, kickReturnYardage)) +\n  geom_point() +\n  geom_smooth() +\n  labs(title = \"Relationship Between Kick and Return Length\", y=\"Kick Return Yardage\", x=\"Kick Length\") +\n  annotate(\"text\",x=20, y=35, label=\"r = 0.183458\")\ncor(punt_plays$kickReturnYardage, punt_plays$kickLength, use = \"complete.obs\")","metadata":{"execution":{"iopub.status.busy":"2022-01-05T15:47:37.200416Z","iopub.execute_input":"2022-01-05T15:47:37.202873Z","iopub.status.idle":"2022-01-05T15:47:40.286079Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"Our project was inspired by NFL NextGenStats wide receiver route charts. We wanted to create a way that punt returns could be graphed and compared in a similar way. Our vision was an app that could display all of the returns for a particular player standardized based on the location the ball was caught. To accomplish this task, we had to analyze the way that the data was reported and filter out the location data that we needed. Due to the size of the datasets for the player tracking data, we chose to focus only on the 2018 season. In the future, we could expand our app to include other seasons and potentially combine seasons to get a better picture of a player’s returns over time. ","metadata":{}},{"cell_type":"code","source":"punt_plays_players <- left_join(punt_plays, players, by=c(\"kickerId\"=\"nflId\")) %>%\n  rename(punter_height=height, punter_weight=weight, punter_college=collegeName, punter_name = displayName)\npunt_plays_players$returnerId <- as.numeric(punt_plays_players$returnerId)\n\npunt_tracking <- left_join(punt_plays_players, tracking2018, by=c(\"gameId\", \"playId\"))","metadata":{"execution":{"iopub.status.busy":"2022-01-05T15:47:40.288642Z","iopub.execute_input":"2022-01-05T15:47:40.290137Z","iopub.status.idle":"2022-01-05T15:47:41.933519Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"When beginning to look at tracking data, we created a new dataset with only the first punt play so that we could get an understanding of plotting the data without dealing with the size of the full 2018 dataset. We then filtered down to only have the data from the returner. By manually looking through the data, we identified the frames when the ball was caught and when the play ended and then graphed the location data onto a football field using the sportyR package. While this worked, we wanted to find a way to systematically filter the event data to only include instances when the return is occurring. ","metadata":{}},{"cell_type":"code","source":"punt_tracking_simple <- punt_tracking[1:2944,]\n\nplayernames <- players %>% select(nflId, displayName)\n\npunt_tracking_simple$returnerId <- as.numeric(punt_tracking_simple$returnerId)\npunt_tracking_returner <- left_join(punt_tracking_simple, playernames, by=c(\"returnerId\"=\"nflId\")) %>%\n  rename(returner_name = displayName.y, tracking_player = displayName.x) %>%\n  filter(returner_name==tracking_player)\n\ngeom_football(league = \"nfl\", grass_color=\"darkgreen\") +\n  geom_point(data=punt_tracking_returner[77:length(punt_tracking_returner),], aes(x,y)) +\n  labs(title = \"First Return: Justin Hardy\")","metadata":{"execution":{"iopub.status.busy":"2022-01-05T15:47:41.936151Z","iopub.execute_input":"2022-01-05T15:47:41.937600Z","iopub.status.idle":"2022-01-05T15:47:43.513905Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"For our next step, we plotted all returns for one player: Darren Sproles. We were able to implement an algorithm to filter the data down to instances that occurred between the punt being received and an event that would end the return such as a tackle, fumble, touchback, lateral, safety, fair catch, touchdown, handoff, or out of bounds. We then separated Sproles’ returns by the direction that he was moving, left to right or right to left, in order to understand whether his returns were positive or negative. We plotted the two directions separately to create a map of Sproles’ total returns from left to right and right to left. ","metadata":{}},{"cell_type":"code","source":"#punt tracking data for specific returner\npunt_tracking_by_returner_sproles <- left_join(punt_tracking, playernames, by=c(\"returnerId\"=\"nflId\")) %>%\n  rename(returner_name = displayName.y, tracking_player = displayName.x) %>%\n  filter(returner_name==tracking_player) %>%\n  filter(returner_name==\"Darren Sproles\")\n\n#only from catch to end of play\npunt_tracking_by_returner2_sproles <- punt_tracking_by_returner_sproles %>%\n  group_by(gameId, playId) %>%\n  filter(any(event==\"punt_received\")) %>%\n  mutate(counter = ifelse(event==\"punt_received\", 1, ifelse(event %in% c(\"tackle\",\"fumble_defense_recovered\",\"touchback\",\"fumble\",\"lateral\",\"safety\",\"fair_catch\",\"fumble_offense_recovered\",\"punt_muffed\",\"touchdown\",\"handoff\",\"out_of_bounds\"), -1, 0)), counter_sum = cumsum(counter)) %>%\n  filter(counter_sum == 1)\n\n# only going in one direction: punt to the right, return to the left\npunt_tracking_by_returner_right2left_sproles <- punt_tracking_by_returner2_sproles %>%\n  filter(playDirection == \"right\")\n\n# only going in one direction: punt to the left, return to the right\npunt_tracking_by_returner_left2right_sproles <- punt_tracking_by_returner2_sproles %>%\n  filter(playDirection == \"left\")\n\nfirst_sproles_return <- punt_tracking_by_returner2_sproles %>%\n  filter(gameId==2018090600, playId==1989)\n\ngeom_football(league = \"nfl\") +\n  geom_point(data=first_sproles_return, aes(x,y)) +\n    labs(title=\"First Sproles Return\")\n\n#plotting return path on football field\ngeom_football(league = \"nfl\", grass_color=\"darkgreen\") +\n  geom_point(data=punt_tracking_by_returner_left2right_sproles, aes(x,y))+\n  labs(title = \"Left to Right\")\n\ngeom_football(league = \"nfl\", grass_color=\"darkgreen\") +\n  geom_point(data=punt_tracking_by_returner_right2left_sproles, aes(x,y)) +\n  labs(title = \"Right to Left\")","metadata":{"execution":{"iopub.status.busy":"2022-01-05T15:47:43.516599Z","iopub.execute_input":"2022-01-05T15:47:43.518002Z","iopub.status.idle":"2022-01-05T15:47:50.137340Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"Finally, we standardized all player returns so they could be plotted on the same graph and would all be moving in the same direction and from the same yardline. By subtracting the initial x coordinate at the time of catch from each x coordinate, we were able to standardize all plays to an initial start line, but the plays were still in opposite directions so we had to combine them. To do this we multiplied every x coordinate in the right to left dataset by negative one so that it would start from the same point as the left to right dataset. We originally tried using the absolute value of the difference between the initial x and the current x, but this did not work because it made all plays look positive even if the return had actually been negative. ","metadata":{}},{"cell_type":"code","source":"punt_tracking_standardized_by_returner_right2left_sproles <- punt_tracking_by_returner_right2left_sproles %>%\n  group_by(gameId, playId) %>%\n  mutate(x2 = x-first(x))\n\npunt_tracking_standardized_by_returner_left2right_sproles <- punt_tracking_by_returner_left2right_sproles %>%\n  group_by(gameId, playId) %>%\n  mutate(x2 = x-first(x))\n\ngeom_football(league = \"nfl\", grass_color=\"darkgreen\") +\n  geom_point(data=punt_tracking_standardized_by_returner_right2left_sproles, aes(-x2,y)) +\n  labs(title = \"Right to Left Standard\")\n\ngeom_football(league = \"nfl\", grass_color=\"darkgreen\") +\n  geom_point(data=punt_tracking_standardized_by_returner_left2right_sproles, aes(x2,y)) +\n  labs(title = \"Left to Right Standard\")\n\npunt_tracking_standardized_by_returner_sproles <- punt_tracking_by_returner2_sproles %>%\n  group_by(gameId, playId) %>%\n  mutate(x2 = abs(x-first(x)))\n\nggplot(data=punt_tracking_standardized_by_returner_sproles, aes(x2,y)) +\n  geom_path(data=punt_tracking_standardized_by_returner_sproles, aes(x2,y,group = playId)) +\n  labs(title = \"Standard General\") +\n  geom_vline(aes(xintercept=0))\n\ngeom_football(league = \"nfl\", grass_color=\"darkgreen\") +\n  geom_path(data=punt_tracking_standardized_by_returner_sproles, aes(x2+10,y,group = playId)) +\n  labs(title = \"Standard General\")","metadata":{"execution":{"iopub.status.busy":"2022-01-05T16:45:22.704843Z","iopub.execute_input":"2022-01-05T16:45:22.706982Z","iopub.status.idle":"2022-01-05T16:45:22.826490Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"We expanded this approach to all returns in the dataset and plotted them on a simple line graph using the same system as above. This allowed us to view the distribution of the returns and notice that there were some net negative returns, but none that exceeded a loss of 10 yards. There were more net positive returns which maxed out just under 90 yards. Knowing this, we standardized the returns about the 10 yard line in our final app, making sure that every return was neatly plotted on the football field. ","metadata":{}},{"cell_type":"code","source":"punt_tracking_by_returner <- left_join(punt_tracking, playernames, by=c(\"returnerId\"=\"nflId\")) %>%\n    rename(returner_name = displayName.y, tracking_player = displayName.x) %>%\n    filter(returner_name==tracking_player)\n\npunt_tracking_by_returner2 <- punt_tracking_by_returner %>%\n    group_by(gameId, playId) %>%\n    filter(any(event==\"punt_received\")) %>%\n    mutate(counter = ifelse(event==\"punt_received\", 1, \n                            ifelse(event %in% c(\"tackle\",\"fumble_defense_recovered\",\"touchback\",\n                                                \"fumble\",\"lateral\",\"safety\",\"fair_catch\",\n                                                \"fumble_offense_recovered\",\"punt_muffed\",\"touchdown\",\n                                                \"handoff\",\"out_of_bounds\"), -1, 0)), \n           counter_sum = cumsum(counter)) %>%\n    filter(counter_sum == 1)\n\npunt_tracking_by_returner_right2left <- punt_tracking_by_returner2 %>%\n  filter(playDirection == \"right\")\n\npunt_tracking_by_returner_left2right <- punt_tracking_by_returner2 %>%\n  filter(playDirection == \"left\")\n\npunt_tracking_standardized_by_returner_right2left <- punt_tracking_by_returner_right2left %>%\n  group_by(gameId, playId) %>%\n  mutate(x2 = x-first(x))\n\npunt_tracking_standardized_by_returner_left2right <- punt_tracking_by_returner_left2right %>%\n  group_by(gameId, playId) %>%\n  mutate(x2 = x-first(x))\n\nggplot() +\n  geom_path(data=punt_tracking_standardized_by_returner_left2right, aes(x2,y,group = playId)) +\n  geom_path(data=punt_tracking_standardized_by_returner_right2left, aes(-x2,y,group = playId)) +\n  labs(title = \"Both Standardized\")","metadata":{"execution":{"iopub.status.busy":"2022-01-05T15:47:54.698131Z","iopub.execute_input":"2022-01-05T15:47:54.699630Z","iopub.status.idle":"2022-01-05T15:47:58.065025Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"The app displays the standardized returns for each player assuming all punts were receieved at the 10 yard line. There is a dropdown menu allowing the user to pick any returner from the data. Once a player is selected, the graph responds dynamically and plots their returns. Negative returns are shown in red and posititve returns are shown in green to enhance the viewer's experience. We also created a slider that allows the user to pick a range of net return yardage. The graph then dybamically filters the returns by this range. \n\nThe output allows users to compare different returns by individual players and see which types of returns are more favorable. For example, does running up the middle or down the sideline generate more yardage for Tyreek Hill? Additionally, one can compare the return lengths between players and make a decision about who is the better punt returner. Overall trends can be identified through this process of comparison as well, making it a great tool for teams to use to optimize punt returns. Additionally, this app, or a version of it, could be displayed to the TV audience during games as a way to display the prowess of certain returners and compare returners from competing teams. \n\nNote: The shiny app may not run in the kaggle notebook, but copying and pasting the code below (written in markdown) into a shiny app file and running it locally will produce the desired app. In that case, data must be downloaded locally and the necessary datasets that lead to the data used in the shiny app must be created. ","metadata":{}},{"cell_type":"code","source":"punt_tracking_standardized_by_returner <- punt_tracking_by_returner2 %>%\n    group_by(gameId, playId) %>%\n    mutate(x2 = abs(x-first(x)))\n\ndata = punt_tracking_standardized_by_returner\n\nreturner_names <- data %>% \n    group_by(returner_name) %>%\n    filter(row_number()==1) %>%\n    pull(returner_name) %>%\n    sort()\n\ndata1 = punt_tracking_standardized_by_returner_right2left\ndata2 = punt_tracking_standardized_by_returner_left2right","metadata":{"execution":{"iopub.status.busy":"2022-01-05T15:47:58.067992Z","iopub.execute_input":"2022-01-05T15:47:58.069478Z","iopub.status.idle":"2022-01-05T15:47:58.208872Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"library(shiny)\nserver <- function(input, output, session) {\n    \n    updateSelectizeInput(session, \"returner\", choices = returner_names, server = TRUE)\n    data1_filtered <- reactive({\n        data1 %>% filter(returner_name %in% input$returner) %>%\n            group_by(gameId, playId, returner_name) %>% \n            mutate(col = ifelse(last(x2)>0, \"red\", \"green\")) %>%\n            filter(-last(x2) > input$return_yards[1], -last(x2) < input$return_yards[2])\n            \n    })\n    data2_filtered <- reactive({\n        data2 %>% filter(returner_name %in% input$returner) %>%\n            group_by(gameId, playId, returner_name) %>% \n            mutate(col = ifelse(last(x2)<0, \"red\", \"green\")) %>%\n            filter(last(x2) > input$return_yards[1], last(x2) < input$return_yards[2]) \n            \n    })\n    \n    output$scatterPlot <- renderPlot({\n        geom_football(league = \"nfl\", grass_color=\"darkgreen\") +\n            geom_path(data=data1_filtered(), aes(-x2+20,y,group = playId, color=col)) +\n            geom_path(data=data2_filtered(), aes(x2+20,y,group = playId, color=col)) +\n            scale_color_identity()\n    })\n}\n\n\nui <- fluidPage(\n    \n    titlePanel(\"Standardized Punt Returns for Selected Player: 2018 Season\"),\n    \n    sidebarLayout(\n        sidebarPanel(\n            selectizeInput(\"returner\", \"Returner\", multiple = F, choices = NULL),\n            sliderInput(\"return_yards\",\n                        \"Range of Net Return Yards:\",\n                        min = -100,\n                        max = 100,\n                        value = c(-30,30))\n        ),\n        \n        mainPanel(\n            plotOutput(\"scatterPlot\")\n        )\n    )\n)\n \nshinyApp(ui = ui, server = server)\n","metadata":{"execution":{"iopub.status.busy":"2022-01-05T15:47:58.211682Z","iopub.execute_input":"2022-01-05T15:47:58.213310Z","iopub.status.idle":"2022-01-05T15:48:38.424281Z"}}},{"cell_type":"markdown","source":"","metadata":{}}]}