{"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":"code","source":"options(warn=-1)","metadata":{"execution":{"iopub.status.busy":"2022-01-06T22:04:01.979773Z","iopub.execute_input":"2022-01-06T22:04:01.981579Z","iopub.status.idle":"2022-01-06T22:04:01.996587Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"The objective of this notebook is to determine what makes a successful fake punt. Psychology is the key to success in any type of \"fake\" play. Your opponent needs to think you are doing one thing, while you do something completely different before they realize what's happening.\n\nSuccess here is defined as playResult > yardsToGo. In other words, it means the play achieves at least a first down.\n\nFake punts in this dataset are listed as a punt with a \"non-special teams result\". However, there are a few such plays that are described as \"aborted\" punts. Since aborted punts were intended to be true punts, those will not be included in this analysis.\n\n#### First I looked at attributes of the game/play itself:\n\n- How many yards to go\n\n- Home vs Away team (home team advantage)\n\n- Quarter (is a fake punt less expected earlier vs later on in the game?)\n\n- Pass vs Run\n\n- How many points behind the possessing team is\n\n- If the play happens on the possessing team's side of the field\n\n#### Next I looked at attributes of how the play itself is conducted:\n\n- Delay between snaps and punt fakes\n\n- Delay between punt fakes and passes/runs\n\n- How far the punter moves between snap and punt fake.\n\n- How quickly the punter moves between snap and punt fake per frame.\n\nThe variables that seem meaningful will be put into a decision tree. The tree will be used to calculate variable importance, as well as some specific scenarios that are likely to lead to success or failure. 46 out of 68 (67.6%) plays in this dataset are successful.\n\nIt is likely a no-brainer, but successful plays generally have fewer yards to go. The median for successful plays is 3 yards, and 5.5 yards for failed plays.\n","metadata":{}},{"cell_type":"markdown","source":"","metadata":{}},{"cell_type":"code","source":"players <- read.csv('../input/nfl-big-data-bowl-2022/players.csv')\ngames <- read.csv('../input/nfl-big-data-bowl-2022/games.csv')\nscouting <- read.csv('../input/nfl-big-data-bowl-2022/PFFScoutingData.csv')\nplays <- read.csv('../input/nfl-big-data-bowl-2022/plays.csv')\ntrack_2018 <- read.csv('../input/nfl-big-data-bowl-2022/tracking2018.csv')\ntrack_2019 <- read.csv('../input/nfl-big-data-bowl-2022/tracking2019.csv')\ntrack_2020 <- read.csv('../input/nfl-big-data-bowl-2022/tracking2020.csv')","metadata":{"execution":{"iopub.status.busy":"2022-01-06T21:14:45.487751Z","iopub.execute_input":"2022-01-06T21:14:45.489420Z","iopub.status.idle":"2022-01-06T21:20:23.901809Z"},"jupyter":{"source_hidden":true},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"#install.packages('expss')\n#install.packages('rpart')\n#install.packages('rpart.plot')\n#install.packages('expss')\nlibrary(tidyverse)\nlibrary(expss)\nlibrary(rpart)\nlibrary(rpart.plot)\nlibrary(expss)\nlibrary(rattle)","metadata":{"_kg_hide-output":true,"execution":{"iopub.status.busy":"2022-01-06T21:58:02.366578Z","iopub.execute_input":"2022-01-06T21:58:02.368415Z","iopub.status.idle":"2022-01-06T21:58:02.477705Z"},"jupyter":{"source_hidden":true},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"#Removing true and aborted punts\n\nnon_spec <- plays %>%\n  filter(specialTeamsResult == 'Non-Special Teams Result')\n\nfake_punt <- non_spec %>%\n  filter(specialTeamsPlayType == 'Punt')\n\naborted_punt <- fake_punt %>%\n  filter(str_detect(playDescription, 'Abort'))\n\nint_fake_punt <- anti_join(fake_punt, aborted_punt)\n\nint_fake_punt <- inner_join(scouting, int_fake_punt)","metadata":{"execution":{"iopub.status.busy":"2022-01-06T21:20:24.333325Z","iopub.execute_input":"2022-01-06T21:20:24.334900Z","iopub.status.idle":"2022-01-06T21:20:24.414212Z"},"jupyter":{"source_hidden":true},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"int_fake_punt <- int_fake_punt %>%\n  mutate(is_pass = !(is.na(passResult))) %>%\n  mutate(success = (yardsToGo - playResult) <= 0)","metadata":{"execution":{"iopub.status.busy":"2022-01-06T21:20:24.417872Z","iopub.execute_input":"2022-01-06T21:20:24.419631Z","iopub.status.idle":"2022-01-06T21:20:24.442527Z"},"jupyter":{"source_hidden":true},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"fp_2018 <- track_2018 %>%\n  inner_join(int_fake_punt) %>%\n  select(colnames(track_2018))\n\nfp_2019 <- track_2019 %>%\n  inner_join(int_fake_punt) %>%\n  select(colnames(track_2019))\n\nfp_2020 <- track_2020 %>%\n  inner_join(int_fake_punt) %>%\n  select(colnames(track_2020))\n\ntracking <- rbind(fp_2018, fp_2019, fp_2020)","metadata":{"execution":{"iopub.status.busy":"2022-01-06T21:20:24.445097Z","iopub.execute_input":"2022-01-06T21:20:24.446580Z","iopub.status.idle":"2022-01-06T21:20:27.895540Z"},"jupyter":{"source_hidden":true},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"It is likely a no-brainer, but successful plays generally have fewer yards to go. The median for successful plays is 3 yards, and 5.5 yards for failed plays.","metadata":{}},{"cell_type":"code","source":"ggplot(int_fake_punt, aes(x = success, y = yardsToGo)) +\n  geom_boxplot() +\n  labs(x = 'Success', y = 'Yards To Go')","metadata":{"execution":{"iopub.status.busy":"2022-01-06T21:20:27.897992Z","iopub.execute_input":"2022-01-06T21:20:27.899404Z","iopub.status.idle":"2022-01-06T21:20:28.443366Z"},"jupyter":{"source_hidden":true},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"unique_id <- unique(tracking[c('gameId', 'playId')])\nrow.names(tracking) <- NULL\n\nhome_away <- c()\n\nfor(i in 1:nrow(unique_id)){\n  h <- unique(tracking[(tracking$gameId == unique_id$gameId[i]) &\n             (tracking$playId == unique_id$playId[i]) &\n             (tracking$position == 'P'), ]$team)[1]\n  home_away <- c(home_away, h)\n}\n\nhome_away_df <- cbind(unique_id, home_away)\n\nint_fake_punt <- inner_join(int_fake_punt, home_away_df)\n\nint_fake_punt <- int_fake_punt %>%\n  mutate(points_down = ifelse(home_away == 'home', preSnapVisitorScore - preSnapHomeScore,preSnapHomeScore - preSnapVisitorScore))","metadata":{"execution":{"iopub.status.busy":"2022-01-06T21:20:28.447423Z","iopub.execute_input":"2022-01-06T21:20:28.449002Z","iopub.status.idle":"2022-01-06T21:20:29.002437Z"},"jupyter":{"source_hidden":true},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"There seems to be a small, negative relationship between points down and success. This may just be an analog for which team is better. When playing against a team that's better than yours, the success of any play in general will be lower.\n","metadata":{}},{"cell_type":"code","source":"ggplot(int_fake_punt, aes(x = points_down, y = as.numeric(success))) +\n  geom_point() +\n  geom_smooth(method = 'glm',\n              method.args = list(family = 'binomial'),\n              se = TRUE) +\n  labs(x = 'Points Behind', y = 'Success')","metadata":{"execution":{"iopub.status.busy":"2022-01-06T21:23:43.218776Z","iopub.execute_input":"2022-01-06T21:23:43.220507Z","iopub.status.idle":"2022-01-06T21:23:45.735283Z"},"jupyter":{"source_hidden":true},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"tracking_events <- tracking %>%\n  select(c('gameId', 'playId', 'displayName', 'position', 'event', 'playDirection', 'frameId')) %>%\n  filter(event != 'None') %>%\n  filter(position == 'P') %>%\n  group_by(across(all_of(c('playId', 'gameId')))) %>%\n  pivot_wider(names_from = event, values_from = frameId) %>%\n  select(-c('position'))\n\nint_fake_punt <- inner_join(int_fake_punt, tracking_events)\n\nevent_names <- unique(tracking[tracking$event != 'None' &\n                                   tracking$event != 'first_contact' &\n                                   tracking$event != 'handoff' &\n                                   tracking$event != 'pass_arrived', ]$event)\n\nint_fake_punt <- int_fake_punt %>%\n  unnest_wider(first_contact, names_sep = '_') %>%\n  unnest_wider(handoff, names_sep = '_') %>%\n  unnest_wider(pass_arrived, names_sep = '_') %>%\n  unnest(event_names, keep_empty = TRUE)\n\nint_fake_punt <- int_fake_punt %>%\n  mutate(direct_snap = is.na(ball_snap))\n\nint_fake_punt <- int_fake_punt %>%\n  mutate(fake = !(is.na(punt_fake)))\n\nint_fake_punt <- int_fake_punt %>%\n  mutate(pass_delay = pass_forward - punt_fake)\n\nint_fake_punt <- int_fake_punt %>%\n  mutate(run_delay = run - punt_fake)","metadata":{"execution":{"iopub.status.busy":"2022-01-06T21:26:05.784358Z","iopub.execute_input":"2022-01-06T21:26:05.786043Z","iopub.status.idle":"2022-01-06T21:26:05.954041Z"},"jupyter":{"source_hidden":true},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"punters_only <- tracking[tracking$position == 'P', ]\n\nmovement <- punters_only %>%\n  group_by(across(all_of(c('playId', 'gameId')))) %>%\n  summarise(snap_to_fake = dis[match('punt_fake', event)] -\n              dis[match('ball_snap', event)])\n\nmovement2 <- punters_only %>%\n  group_by(across(all_of(c('playId', 'gameId')))) %>%\n  summarise(d_snap_to_fake = dis[match('punt_fake', event)] -\n              dis[match('snap_direct', event)])\n\nint_fake_punt <- int_fake_punt %>%\n  inner_join(movement) %>%\n  inner_join(movement2)","metadata":{"execution":{"iopub.status.busy":"2022-01-06T21:26:55.919651Z","iopub.execute_input":"2022-01-06T21:26:55.921492Z","iopub.status.idle":"2022-01-06T21:26:56.012180Z"},"jupyter":{"source_hidden":true},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"Punters generally move less between the snap and fake on successful plays, although there is one outlier.  \n","metadata":{}},{"cell_type":"code","source":"ggplot(int_fake_punt, aes(x = snap_to_fake, y = success)) +\n  geom_boxplot() +\n  labs(x = 'Total Movement between Snap and Punt Fake',\n       y = 'Success')","metadata":{"execution":{"iopub.status.busy":"2022-01-06T21:27:49.588449Z","iopub.execute_input":"2022-01-06T21:27:49.590385Z","iopub.status.idle":"2022-01-06T21:27:49.925894Z"},"jupyter":{"source_hidden":true},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"int_fake_punt <- int_fake_punt %>%\n  mutate(snap_to_fake_pf = snap_to_fake/(punt_fake - 11)) %>%\n  mutate(d_snap_to_fake_pf = d_snap_to_fake/(punt_fake - 11))","metadata":{"execution":{"iopub.status.busy":"2022-01-06T21:30:40.873320Z","iopub.execute_input":"2022-01-06T21:30:40.875102Z","iopub.status.idle":"2022-01-06T21:30:40.898553Z"},"jupyter":{"source_hidden":true},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"This difference is even more pronounced once you include the number of frames in between snap and fake. The 75th percentile of punter movement per frame for successful plays is approximately the median for unsuccessful plays.","metadata":{}},{"cell_type":"code","source":"ggplot(int_fake_punt, aes(x = snap_to_fake_pf, y = as.numeric(success))) +\n  geom_point() +\n  geom_smooth(method = 'glm',\n              method.args = list(family = 'binomial'),\n              se = TRUE) +\n  labs(x = 'Average Movement per Frame between Snap and Punt Fake', y = 'Estimated Chance of Success')\n\nggplot(int_fake_punt, aes(x = snap_to_fake_pf, y = success)) +\n  geom_boxplot() +\n  labs(x = 'Average Movement per Frame between Snap and Punt Fake', y = 'Success')\n","metadata":{"execution":{"iopub.status.busy":"2022-01-06T21:31:52.722742Z","iopub.execute_input":"2022-01-06T21:31:52.724549Z","iopub.status.idle":"2022-01-06T21:31:53.326225Z"},"jupyter":{"source_hidden":true},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"There does appear to be a linear relationship between total movement between snap and fake and average movement per frame. However, it's very heteroskedastic, so it would be reasonable to include both of these variables in the final decision tree.\n","metadata":{}},{"cell_type":"code","source":"ggplot(int_fake_punt, aes(x = snap_to_fake, y = snap_to_fake_pf)) +\n  geom_point() +\n  labs(x = 'Total Snap to Fake Movement', y = 'Snap to Fake Movement per Frame')","metadata":{"execution":{"iopub.status.busy":"2022-01-06T21:32:23.107248Z","iopub.execute_input":"2022-01-06T21:32:23.109223Z","iopub.status.idle":"2022-01-06T21:32:23.316312Z"},"jupyter":{"source_hidden":true},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"Some of these plays don't have a punt fake. As a workaround, I decided to look at the total movement and movement speed between the snap and the first event after the snap. The boxplot below shows that the successful plays have far less movement.\n","metadata":{}},{"cell_type":"code","source":"tracking_no_fake <- tracking %>%\n  inner_join(int_fake_punt[is.na(int_fake_punt$punt_fake), ]) %>%\n  select(colnames(tracking)) %>%\n  filter(event != 'None')\n\ntracking_no_fake2 <- tracking_no_fake %>%\n  mutate(frameId2 = frameId) %>%\n  mutate(event2 = event) %>%\n  group_by(across(all_of(c('playId', 'gameId')))) %>%\n  mutate(next_event_frame = ifelse(event[2] == 'snap_direct' | event[2] == 'ball_snap',frameId[3],frameId[2])) %>%\n  mutate(next_event = ifelse(event[2] == 'snap_direct' | event[2] == 'ball_snap', event[3], event[2])) %>%\n  mutate(bsnap_to_next = dis[match(next_event, event)] -\n           dis[match('ball_snap', event)]) %>%\n  mutate(dsnap_to_next = dis[match(next_event, event)] -\n           dis[match('snap_direct', event)]) %>%\n  mutate(snap_to_next = ifelse(is.na(bsnap_to_next), dsnap_to_next, bsnap_to_next)) %>%\n  select(c('gameId', 'playId', 'next_event', 'next_event_frame', 'snap_to_next')) %>%\n  distinct()\n\nint_fake_punt <- merge(int_fake_punt, tracking_no_fake2, all.x = TRUE)\n\nint_fake_punt <- int_fake_punt %>%\n  mutate(snap_to_next_pf = snap_to_next/(next_event_frame -11))\n\n","metadata":{"execution":{"iopub.status.busy":"2022-01-06T21:50:55.153177Z","iopub.execute_input":"2022-01-06T21:50:55.154891Z","iopub.status.idle":"2022-01-06T21:50:55.246653Z"},"jupyter":{"source_hidden":true},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"ggplot(int_fake_punt, aes(x = snap_to_next_pf, y = success)) +\n  geom_boxplot() +\n  labs(x = 'Avg. Movement per Frame between Snap and Next Event',\n       y = 'Success')","metadata":{"execution":{"iopub.status.busy":"2022-01-06T21:51:41.349217Z","iopub.execute_input":"2022-01-06T21:51:41.350961Z","iopub.status.idle":"2022-01-06T21:51:41.569141Z"},"jupyter":{"source_hidden":true},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"#### I selected the following variables for the decision tree:\n\n- Yards to go\n\n- Pass/Not a pass\n\n- How many points behind the possessing team is (points down)\n\n- Delay between snaps and punt fakes\n\n- Number of frames between punt fake and pass (pass delay)\n\n- All movement variables (total and average per frame)\n\n- Next event (in the case that there is no punt fake)\n","metadata":{}},{"cell_type":"code","source":"myvars <- c('success', 'yardsToGo', 'is_pass',\n            'points_down','punt_fake', 'pass_delay',\n            'snap_to_fake', 'd_snap_to_fake',\n            'snap_to_fake_pf', 'd_snap_to_fake_pf',\n            'snap_to_next', 'snap_to_next_pf', 'next_event')\n\nfp_tree <- int_fake_punt %>%\n  select(myvars)\n\nfake_punt_tree <- rpart(success ~ ., data = fp_tree\n                        ,control=rpart.control(minsplit=8, minbucket=4, cp=0.01))\n","metadata":{"execution":{"iopub.status.busy":"2022-01-06T21:53:37.986783Z","iopub.execute_input":"2022-01-06T21:53:38.097037Z","iopub.status.idle":"2022-01-06T21:53:38.131390Z"},"jupyter":{"source_hidden":true},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"Below is the variable importance table. By far, the most important variables are the total movement (snap_to_fake) and movement speed (snap_to_fake_pf) between the snap and punt fake. The timing of the punt fake and yards to go are also very important.\n","metadata":{}},{"cell_type":"code","source":"fake_punt_tree$variable.importance\n","metadata":{"execution":{"iopub.status.busy":"2022-01-06T21:58:29.890435Z","iopub.execute_input":"2022-01-06T21:58:29.892396Z","iopub.status.idle":"2022-01-06T21:58:29.913036Z"},"jupyter":{"source_hidden":true},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"#### Some takeaways from this tree:\n\nIf the snap is a normal snap, the punter moving quicker will generally result in a low chance of success. The #1 recipe for disaster is moving a shorter distance at a faster speed and taking longer to punt fake (0% chance of success in this dataset). The one exception to this is moving a longer total distance with fewer yards to go (83% chance of success).\n\nEvery normal snap play where the punter moves more slowly is successful if there is a shorter pass delay, or if it is a run. If there is a longer delay, the chance of sucess is still high if the total movement is smaller than 0.045.\n\nEvery play in this dataset without a punt fake is successful if the movement between snap and the following event is less than 0.3.\n\n#### In conclusion: \nThe punter should not move faster than 0.11 yards per second before the fake. And if passing, he shouldn't wait longer than 13 frames (1.3 seconds) after the fake to throw the pass.\n","metadata":{}},{"cell_type":"code","source":"fancyRpartPlot(fake_punt_tree, sub = 'Fake Punt Success Tree')\n","metadata":{"execution":{"iopub.status.busy":"2022-01-06T21:59:14.932648Z","iopub.execute_input":"2022-01-06T21:59:14.934463Z","iopub.status.idle":"2022-01-06T21:59:15.325089Z"},"jupyter":{"source_hidden":true},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"","metadata":{}},{"cell_type":"markdown","source":"","metadata":{}}]}