{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"pygments_lexer":"ipython3","nbconvert_exporter":"python","version":"3.6.4","file_extension":".py","codemirror_mode":{"name":"ipython","version":3},"name":"python","mimetype":"text/x-python"}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"# Ranking Gunners, Vises, and Punt Return Degree of Difficulty","metadata":{}},{"cell_type":"markdown","source":"This analysis set out to understand which factors in punt coverage impact return yardage. I built Random Forest Regression and Classification Models, which were only somewhat effective in predicting return yardage. However, the models highlighted features key to limiting return yardage. These features can be used to rank special teams players. \n\nKick length and hang time are crucial to predicting return yardage, but getting close to the returner is also crucial. No surprises here. Getting downfield in punt coverage is obviously important, but the analysis supports it. Given this assumption, this workbook uses punt play tracking data to produce the following:\n\n1. ***Gunner Rankings*** - Using tracking data at the snap, I matched gunners with vises. I then calculated how far gunners could get downfield 3, 4, and 5 seconds from the snap. As I'd paired gunners with opposing vises, I could calculate their downfield penetration in both absolute yardage and yardage ***relative to the vise***. I used percentile ranking on each penetration metric for all gunners that participated in at least 15 qualifying punt plays (punt of 40+ yards with no penalties). I averaged percentile rankings of all metrics to determine the final metric for ranking gunners.\n2. ***Vise Rankings*** - Vises try to prevent gunners from getting downfield. Using the same method, I ranked vises by the average penetration allowed on qualifying punt plays.\n3. ***Punt Return Degree of Difficulty*** - For punt returns, I percentile ranked four metrics. I averaged these rankings to determine \"Degree of Difficulty,\" by which all returns are ranked. The following metrics went into the rankings: \n    - Defender Proximity - I tallied the number of frames where a defender was within two yards of the returner during the return. If multiple defenders were within two yards in the same frame, that counted as two instances.\n    - Football Movement - Total distance the football traveled during the return.\n    - Return Time - Time that passed between receiving the punt and the end of the return.\n    - Broken Tackles - Using PFF data, I determined how many tackles were broken during the return.","metadata":{}},{"cell_type":"markdown","source":"## Punt Coverage Features ","metadata":{}},{"cell_type":"markdown","source":"- **Hang Time** - Provided feature, reflects how long a punt is in the air.\n- **Kick Direction** - I converted kickDirectionActual to a one-hot encoded field where 0 represents a centered kick and 1 represents a kick to the left/right.\n- **Kick Team Position** - I calculated the average horizontal (x-axis) position of kicking team players (excluding the punter) relative to their position at the snap. Calculated at 3, 4, and 5 seconds from the snap. The theory was that the kicking team's penetration would correlate negatively with return yardage.\n- **Receiving Team Position** - Average horizontal position of the receiving team (excluding the returner) at 3, 4, and 5, seconds.\n- **Gunner Penetration** - Average horizontal position of gunners relative to snap position at 3, 4, and 5, seconds.\n- **Max Spread** - Difference between minimum and maximum vertical (y-axis) position of kicking team players at 3, 4, and 5 seconds. I wanted to explore whether vertical distance covered impacted return yardage.\n- **Gap Standard Deviation** - Standard deviation of the vertical gaps between kicking team players at 3, 4, and 5 seconds. This is a measure of the size of the holes in coverage.\n- **Minimum Defender Distance to Returner** - Distance from nearest defender to the returner at 3, 4, and 5 seconds.\n- **Mean Defender Distance to Returner** - Mean distance from all defenders (excluding the punter) to the returner at 3, 4, and 5 seconds.","metadata":{}},{"cell_type":"markdown","source":"## Machine Learning Models and Findings","metadata":{}},{"cell_type":"markdown","source":"To confirm conventional understanding of key aspects of effective punt coverage, I built machine learning models to predict punt return yardage based on the features listed above. Below I discuss the models, their effectiveness, and conclusions drawn from the process.","metadata":{}},{"cell_type":"markdown","source":"### Feature Importances","metadata":{}},{"cell_type":"markdown","source":"See below for the features of the Random Forest Regression and Classification models used to predict punt return yardage. For Regression, I used the metrics as calculated at 4 seconds from the snap of the football (where applicable). For Classification, all possible features were included and then reduced through sequential forward and backward selection.","metadata":{}},{"cell_type":"code","source":"from IPython.display import Image\n\n# Display feature importances\nimage_url = 'https://github.com/drwismer/NFL_special_teams/blob/main/images/feature_importance.png?raw=true'\nImage(url=image_url, width=1000)","metadata":{"execution":{"iopub.status.busy":"2021-10-28T19:00:38.671261Z","iopub.execute_input":"2021-10-28T19:00:38.672153Z","iopub.status.idle":"2021-10-28T19:00:38.703425Z","shell.execute_reply.started":"2021-10-28T19:00:38.672042Z","shell.execute_reply":"2021-10-28T19:00:38.702380Z"},"jupyter":{"source_hidden":true},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"### Random Forest Linear Regression","metadata":{}},{"cell_type":"markdown","source":"I first explored correlation and collinearity of the features and target variable (return yardage). In my initial check of Variance Inflation Factor (VIF), I included all available features. I eliminated features in order to reduce VIF to an acceptable level (< 5.0) and performed hyperparameter tuning to maximize R-Squared. Even with tuning, I could only achieve ***R-Squared of 0.1254***. \n\nWhile this model is particularly accurate, it does provide information about key features. In the screenshot above, I've included the feature importances for the best performing regression model. This provides hard evidence that getting close to the returner during the punt is key to limiting return yardage. Ground breaking? No. But it's nice to see the data support conventional wisdom.","metadata":{}},{"cell_type":"markdown","source":"### Random Forest Classification","metadata":{}},{"cell_type":"markdown","source":"I had more success with Classification. I separated punt plays by their return yardage into four buckets: (1) fair catch, (2) less than 5 yards, (3) 5-15 yards, and (4) greater than 15 yards. I used sequential forward and sequential backward feature selection to determine the optimal combination of features to maximize accuracy. I tuned the hyperparameters of the winning Random Forest model using grid search and ultimately achieved ***classification accuracy of 47.4%***.\n\nSee below for the confusion matrices normalized by true and predicted classification. The model is best at predicting when a punt will result in a fair catch or in a long return (15+ yards). Given that the model is reasonably predictive, I wanted to understand feature importances. Above, you'll see the features included in the best performing classification model. These features were selected through a feature reduction process, so were already deemed important to predicting return yardage, but some are slightly more important. Kick Length and Hang Time top the list. Beyond metrics related to the kick itself, we see features related to getting downfield (Gunner Penetration, Kick Team Position). Surprisingly, Gap Standard Deviation also made the cut. ","metadata":{}},{"cell_type":"code","source":"# Display punt return confusion matrices\nimage_url = 'https://github.com/drwismer/NFL_special_teams/blob/main/images/confusion_punt.png?raw=true'\nImage(url=image_url, width=1250)","metadata":{"execution":{"iopub.status.busy":"2021-10-28T19:00:38.705049Z","iopub.execute_input":"2021-10-28T19:00:38.705466Z","iopub.status.idle":"2021-10-28T19:00:38.711265Z","shell.execute_reply.started":"2021-10-28T19:00:38.705420Z","shell.execute_reply":"2021-10-28T19:00:38.710447Z"},"jupyter":{"source_hidden":true},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"## Actionable Insights","metadata":{}},{"cell_type":"markdown","source":"With knowledge acquired from analyzing the predictive models, I aimed to use tracking data to derive actionable insights. I decided to explore the most critical skill positions involved in punt return plays:  gunners, vises, and returners. The kicker is also critical, but I found tracking data to be more useful in analyzing punt coverage. Below, I transform the data to rank gunners and vises and calculate the degree of difficulty of punt returns.","metadata":{}},{"cell_type":"markdown","source":"### Gunner and Vise Rankings","metadata":{}},{"cell_type":"markdown","source":"We know the minimum distance between defenders and the returner helps to predict return yardage. We also know the player most likely to get close to the returner is the gunner. It is therefore important to understand gunners' ability to penetrate downfield. It is equally important to understand the vises' ability to prevent gunner penetration. Below, I've displayed the best and worst gunners and vises as determined by the following criteria:\n\n- **Absolute Penetration** - I calculated the gunner's change in horizontal (x-axis) position relative to his snap position at 3, 4, and 5 seconds from the snap of the football.\n- **Gunner Penetration Relative to Vise** - The gunner's horizontal position relative to the opposing vise's horizontal position at 3, 4, and 5 seconds. On each play, each gunner was matched with an opposing vise based on the players' vertical (y-axis) snap position.\n- **Excluded Plays and Players** - Plays were only included in the rankings data if the punt traveled 40+ yards. Penalized plays were excluded. Plays were excluded if there were not an equal number of gunners and vises. For fairness, I only analyzed gunner penetration and vise protection for plays where gunners and vises battled one-on-one. Players were only included in the rankings if they played on 15+ qualifying punt plays.\n- **Average Rank** - The field used to rank players is \"Average Rank.\" This represents the average of the player's rankings in each of the fields representing absolute and relative penetration at 3, 4, and 5 seconds.","metadata":{}},{"cell_type":"markdown","source":"#### Gunner Punt Penetration Rankings - Top and Bottom 10","metadata":{}},{"cell_type":"code","source":"# Display gunner penetration rankings\nimage_url = 'https://github.com/drwismer/NFL_special_teams/blob/main/images/gunner_rankings.png?raw=true'\nImage(url=image_url, width=1250)","metadata":{"execution":{"iopub.status.busy":"2021-10-28T19:00:38.712735Z","iopub.execute_input":"2021-10-28T19:00:38.712981Z","iopub.status.idle":"2021-10-28T19:00:38.725294Z","shell.execute_reply.started":"2021-10-28T19:00:38.712951Z","shell.execute_reply":"2021-10-28T19:00:38.724721Z"},"jupyter":{"source_hidden":true},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"#### Vise Return Protection Rankings - Top and Bottom 10","metadata":{}},{"cell_type":"code","source":"# Display vise protection rankings\nimage_url = 'https://github.com/drwismer/NFL_special_teams/blob/main/images/vise_rankings.png?raw=true'\nImage(url=image_url, width=1250)","metadata":{"execution":{"iopub.status.busy":"2021-10-28T19:00:38.726360Z","iopub.execute_input":"2021-10-28T19:00:38.726641Z","iopub.status.idle":"2021-10-28T19:00:38.738404Z","shell.execute_reply.started":"2021-10-28T19:00:38.726607Z","shell.execute_reply":"2021-10-28T19:00:38.737562Z"},"jupyter":{"source_hidden":true},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"### Punt Return Degree of Difficulty","metadata":{}},{"cell_type":"markdown","source":"Given the importance of Defender Proximity, I hoped to use tracking data to rank punt return probabilitites. This led me to seek out other metrics that could be used to establish a \"Degree of Difficulty\" metric to rank returns. Degree of Difficulty is calculated by averaging the percentile rank of all returns for the following metrics:\n\n- **Defender Proximity** - For each frame ***during the return***, I calculated the distance between each defender and the returner. I created a binary column representing whether each defender was within two yards of the returner. Returns were percentile ranked based on the total. A score of 1.00 in Defender Proximity would be assigned to the single punt return play that contained the most instances of a defender within two yards of the returner during the return.\n- **Total Movement** - This represents ***total movement of the football*** from reception through the end of the return. Below, you'll see that for touchdowns, the percentile rank is typically near 1.00.\n- **Return Time** - Length of the return in frames. For the touchdown plays below, the return time is near 1.00.\n- **Broken Tackles** - PFF supplied identifying information for missed tacklers. I converted this data to count of broken tackles, which was percentile ranked.\n\nThe percentile rankings include all punt plays that were returned (not only touchdowns). This is why Total Movement and Return Time are very near 1.00 for touchdown plays. You'll also notice that Defender Proximity doesn't approach 1.00 in the punt return touchdown rankings. This is because Defender Proximity is maximized when the punt returner is tackled. When I include all punt returns, the top ranking punt return touchdown has the 17th highest degree of difficulty. You can see the full rankings in my [GitHub workbook](https://github.com/drwismer/NFL_special_teams/blob/main/Punt%20Ranking%20Code.ipynb). Defender Proximity, as it is currently calculated, is only useful for ranking touchdown returns. When ranking punt returns regardless of the scoring result, it may be best to exclude Defender Proximity.\n\nSee below for a ranking of punt return touchdowns. You'll also find GIF's of one of the highest and one of the lowest ranked punt return touchdowns. [Thank you to Samira Kumar for the fantastic visualization tool!](https://github.com/samirak93/Game-Animation)","metadata":{}},{"cell_type":"markdown","source":"#### Touchdown Punt Returns - Degree of Difficulty","metadata":{}},{"cell_type":"code","source":"# Display punt return TD's sorted by degree of difficulty\nimage_url = 'https://github.com/drwismer/NFL_special_teams/blob/main/images/punt_return_rankings.png?raw=true'\nImage(url=image_url, width=1250)","metadata":{"execution":{"iopub.status.busy":"2021-10-28T19:00:38.740374Z","iopub.execute_input":"2021-10-28T19:00:38.740814Z","iopub.status.idle":"2021-10-28T19:00:38.752578Z","shell.execute_reply.started":"2021-10-28T19:00:38.740726Z","shell.execute_reply":"2021-10-28T19:00:38.751592Z"},"jupyter":{"source_hidden":true},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"#### Rank #2 - Isaiah McKenzie - BUF vs. MIA - 2020 Week 17. [Watch a video of the return.](https://www.youtube.com/watch?v=0kqp9vD3HMA&ab_channel=HighlightHeaven)\n\nNotice the punt returner is nearly tackled multiple times. His route goes straight through the defense, which increases Defender Proximity. The return is slowed by the punter forcing back inside, increasing Return Time. If you follow the YouTube link, you'll see he has to break a few tackles as well.","metadata":{}},{"cell_type":"code","source":"# Display return gif\nimage_url = 'https://github.com/drwismer/NFL_special_teams/blob/main/images/punt_return_hard.gif?raw=true'\nImage(url=image_url, width=1000)","metadata":{"execution":{"iopub.status.busy":"2021-10-28T19:00:38.753873Z","iopub.execute_input":"2021-10-28T19:00:38.754116Z","iopub.status.idle":"2021-10-28T19:00:38.766670Z","shell.execute_reply.started":"2021-10-28T19:00:38.754087Z","shell.execute_reply":"2021-10-28T19:00:38.765901Z"},"jupyter":{"source_hidden":true},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"#### Lowest Ranked - Mecole Hardman - KC vs. MIA - 2020 Week 14. [Watch a video of the return.](https://www.youtube.com/watch?v=1VVSQwPANiU&ab_channel=TopFanTV)\n\nThis play looks very different from the highly ranked return above. The returner takes the sideline route, avoiding defenders almost entirely. This play has particularly low values of Defender Proximity and Broken Tackles. Because it is a touchdown return, Return Time and Total Movement are high, but not compared to other touchdown returns. You might say this touchdown return was due more to good blocking or poor coverage than to the returner's incredible effort.","metadata":{}},{"cell_type":"code","source":"# Display return gif\nimage_url = 'https://github.com/drwismer/NFL_special_teams/blob/main/images/punt_return_easy.gif?raw=true'\nImage(url=image_url, width=1000)","metadata":{"execution":{"iopub.status.busy":"2021-10-28T19:00:38.767903Z","iopub.execute_input":"2021-10-28T19:00:38.768136Z","iopub.status.idle":"2021-10-28T19:00:38.780289Z","shell.execute_reply.started":"2021-10-28T19:00:38.768108Z","shell.execute_reply":"2021-10-28T19:00:38.779686Z"},"jupyter":{"source_hidden":true},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"## Kickoff Returns","metadata":{}},{"cell_type":"markdown","source":"This is not the focus of this analysis, but I've included some kickoff return analysis for fun. Below, you'll find a confusion matrix showing the performance of a Random Forest Classification model applied to kickoff return yardage. Note that the performance is better than for punt return classification, with ***accuracy of 65.2%***. You'll also find Degree of Difficulty rankings for all kickoff return touchdowns included in the data. You can follow links to YouTube to watch the highest ranked kickoff touchdown, scored by Jakeem Grant of the Miami Dolphins.\n\n[To see the full code, visit the workbook on my GitHub profile](https://github.com/drwismer/NFL_special_teams/blob/main/Kickoff%20Coverage.ipynb).","metadata":{}},{"cell_type":"markdown","source":"### Classification - Confusion Matrix","metadata":{}},{"cell_type":"code","source":"# Display kickoff return confusion matrices\nimage_url = 'https://github.com/drwismer/NFL_special_teams/blob/main/images/confusion_kickoff.png?raw=true'\nImage(url=image_url, width=1250)","metadata":{"execution":{"iopub.status.busy":"2021-10-28T19:00:38.781566Z","iopub.execute_input":"2021-10-28T19:00:38.782020Z","iopub.status.idle":"2021-10-28T19:00:38.792129Z","shell.execute_reply.started":"2021-10-28T19:00:38.781981Z","shell.execute_reply":"2021-10-28T19:00:38.791427Z"},"jupyter":{"source_hidden":true},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"### Touchdown Kickoff Returns - Degree of Difficulty","metadata":{}},{"cell_type":"code","source":"# Display kick return TD's\nimage_url = 'https://github.com/drwismer/NFL_special_teams/blob/main/images/kick_return_rankings.png?raw=true'\nImage(url=image_url, width=1250)","metadata":{"execution":{"iopub.status.busy":"2021-10-28T19:00:38.793435Z","iopub.execute_input":"2021-10-28T19:00:38.793702Z","iopub.status.idle":"2021-10-28T19:00:38.806141Z","shell.execute_reply.started":"2021-10-28T19:00:38.793674Z","shell.execute_reply":"2021-10-28T19:00:38.805274Z"},"jupyter":{"source_hidden":true},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"#### Rank #1 - Jakeem Grant - MIA vs. TEN - 2018 Week 1. [Watch a video of the return.](https://www.youtube.com/watch?v=UqgE-356WlU&ab_channel=HighlightHeaven)","metadata":{}},{"cell_type":"code","source":"# Display return gif\nimage_url = 'https://github.com/drwismer/NFL_special_teams/blob/main/images/kick_return_hard.gif?raw=true'\nImage(url=image_url, width=1000)","metadata":{"execution":{"iopub.status.busy":"2021-10-28T19:00:38.808388Z","iopub.execute_input":"2021-10-28T19:00:38.808816Z","iopub.status.idle":"2021-10-28T19:00:38.817846Z","shell.execute_reply.started":"2021-10-28T19:00:38.808775Z","shell.execute_reply":"2021-10-28T19:00:38.817076Z"},"jupyter":{"source_hidden":true},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"# -------------------------------- THE CODE --------------------------------","metadata":{}},{"cell_type":"markdown","source":"# Import Libraries and Data","metadata":{}},{"cell_type":"code","source":"import pandas as pd\nimport numpy as np\nfrom math import sqrt\n\nimport matplotlib.pyplot as plt\nfrom matplotlib.colors import LinearSegmentedColormap\nimport seaborn as sns\nsns.set(style='darkgrid')\n\nimport statsmodels.api as sm\nfrom statsmodels.stats.outliers_influence import variance_inflation_factor\n\nfrom sklearn.ensemble import RandomForestRegressor, RandomForestClassifier\nfrom sklearn.model_selection import train_test_split, RandomizedSearchCV\nfrom sklearn.metrics import accuracy_score, plot_confusion_matrix\n\nimport pickle","metadata":{"execution":{"iopub.status.busy":"2021-10-28T19:05:20.514759Z","iopub.execute_input":"2021-10-28T19:05:20.515332Z","iopub.status.idle":"2021-10-28T19:05:22.822886Z","shell.execute_reply.started":"2021-10-28T19:05:20.515166Z","shell.execute_reply":"2021-10-28T19:05:22.821974Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"games = pd.read_csv('../input/nfl-big-data-bowl-2022/games.csv')\nplayers = pd.read_csv('../input/nfl-big-data-bowl-2022/players.csv')\nplays = pd.read_csv('../input/nfl-big-data-bowl-2022/plays.csv')\npff = pd.read_csv('../input/nfl-big-data-bowl-2022/PFFScoutingData.csv')\ntracking_2018 = pd.read_csv('../input/nfl-big-data-bowl-2022/tracking2018.csv')\ntracking_2019 = pd.read_csv('../input/nfl-big-data-bowl-2022/tracking2019.csv')\ntracking_2020 = pd.read_csv('../input/nfl-big-data-bowl-2022/tracking2020.csv')","metadata":{"execution":{"iopub.status.busy":"2021-10-28T19:05:22.824509Z","iopub.execute_input":"2021-10-28T19:05:22.824743Z","iopub.status.idle":"2021-10-28T19:07:29.086435Z","shell.execute_reply.started":"2021-10-28T19:05:22.824717Z","shell.execute_reply":"2021-10-28T19:07:29.085566Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"## Data Setup","metadata":{}},{"cell_type":"markdown","source":"### Concatenate Tracking Data and Orient all Kicks to the Right","metadata":{}},{"cell_type":"code","source":"# Concatenate tracking data\ntracking_full = pd.concat([tracking_2018, tracking_2019, tracking_2020], axis=0)\ntracking_full.reset_index(drop=True, inplace=True)\n\n# Flip so all plays go begin with kick to the right\ntracking_full.loc[tracking_full['playDirection'] == \"left\", 'x'] = 120-tracking_full.loc[tracking_full['playDirection'] == \"left\", 'x']\ntracking_full.loc[tracking_full['playDirection'] == \"left\", 'y'] = 160/3-tracking_full.loc[tracking_full['playDirection'] == \"left\", 'y']","metadata":{"execution":{"iopub.status.busy":"2021-10-28T19:07:29.088049Z","iopub.execute_input":"2021-10-28T19:07:29.089043Z","iopub.status.idle":"2021-10-28T19:08:04.493572Z","shell.execute_reply.started":"2021-10-28T19:07:29.089001Z","shell.execute_reply":"2021-10-28T19:08:04.492483Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"# Punt Coverage and Return Analysis","metadata":{}},{"cell_type":"markdown","source":"### Filter Plays and PFF Data to Punt Plays","metadata":{}},{"cell_type":"code","source":"# Filter for punts only\npunt_plays = plays[plays['specialTeamsPlayType'] == 'Punt'].copy()\n\n# Add column for yards from endzone\npunt_plays['yardsFromEndzone'] = np.where(punt_plays['possessionTeam'] == punt_plays['yardlineSide'],\n                                          100 - punt_plays['yardlineNumber'],\n                                          punt_plays['yardlineNumber']\n                                         )\n\n# Add bucket column for yards from endzone\nconditions = [punt_plays['yardsFromEndzone'].le(50), \n              punt_plays['yardsFromEndzone'].gt(50) & punt_plays['yardsFromEndzone'].le(65),\n              punt_plays['yardsFromEndzone'].gt(65) & punt_plays['yardsFromEndzone'].le(93),\n              punt_plays['yardsFromEndzone'].gt(93)\n             ]\n\nchoices = ['0-50', '50-65', '65-93', '93+']\n\npunt_plays['yardsFromEndzoneBucket'] = np.select(conditions, choices)","metadata":{"execution":{"iopub.status.busy":"2021-10-28T19:08:04.494946Z","iopub.execute_input":"2021-10-28T19:08:04.495193Z","iopub.status.idle":"2021-10-28T19:08:04.519666Z","shell.execute_reply.started":"2021-10-28T19:08:04.495162Z","shell.execute_reply":"2021-10-28T19:08:04.518554Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# Filter PFF data for punt plays\npff_punt_plays = pff[pff['kickType'].isin(['N', 'R', 'A'])]","metadata":{"execution":{"iopub.status.busy":"2021-10-28T19:08:04.522327Z","iopub.execute_input":"2021-10-28T19:08:04.522781Z","iopub.status.idle":"2021-10-28T19:08:04.535114Z","shell.execute_reply.started":"2021-10-28T19:08:04.522747Z","shell.execute_reply":"2021-10-28T19:08:04.534005Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"### Filter Tracking Data to Punt Plays","metadata":{}},{"cell_type":"code","source":"# Only keep plays considered punt plays by PFF\npunt_tracking = tracking_full.merge(pff_punt_plays[['gameId', 'playId']],\n                                    on=['gameId', 'playId'],\n                                    how='inner'\n                                   ).reset_index(drop=True)","metadata":{"execution":{"iopub.status.busy":"2021-10-28T19:08:04.536682Z","iopub.execute_input":"2021-10-28T19:08:04.537177Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"### Prepare Tracking Data for Point in Time Metrics","metadata":{}},{"cell_type":"code","source":"# Merge player info to punt tracking data\nplay_cols = ['gameId', 'playId', 'kickerId', 'returnerId', 'possessionTeam', 'absoluteYardlineNumber']\npff_cols = ['gameId', 'playId', 'gunners', 'vises']\ngames_cols = ['gameId', 'homeTeamAbbr', 'visitorTeamAbbr']\n\npunt_tracking = punt_tracking.merge(plays[play_cols], on=['gameId', 'playId'])\\\n                                        .merge(pff[pff_cols], on=['gameId', 'playId'])\\\n                                        .merge(games[games_cols], on='gameId')","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# Convert merged ID columns to integer\npunt_tracking['kickerId'].fillna(0, inplace=True)\npunt_tracking['kickerId'] = punt_tracking['kickerId'].astype(int)\npunt_tracking['returnerId'] = punt_tracking['returnerId'].str.split(';').str[0]\npunt_tracking.dropna(subset = ['returnerId'], inplace=True)\npunt_tracking['returnerId'] = punt_tracking['returnerId'].astype(int)","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# Flip the yardline number for plays going left\npunt_tracking['absoluteYardlineNumber'] = np.where(punt_tracking['playDirection']=='left', \n                                                         (punt_tracking['absoluteYardlineNumber'] - 120) * -1,\n                                                         punt_tracking['absoluteYardlineNumber']\n                                                        )\n\n# Adjust x to be relative to line of scrimmage\npunt_tracking['x_adj'] = punt_tracking['x'] - punt_tracking['absoluteYardlineNumber']","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# Create team name, team and number combination, and kicking team True/False columns\npunt_tracking['jerseyNumber'].fillna(0, inplace=True)\npunt_tracking['teamName'] = np.where(punt_tracking['team']=='football', 'football',\n                                     np.where(punt_tracking['team']=='home', \n                                              punt_tracking['homeTeamAbbr'],\n                                              punt_tracking['visitorTeamAbbr']\n                                             )\n                                    )\n\npunt_tracking['teamNumber'] = punt_tracking['teamName'] + ' ' + punt_tracking['jerseyNumber'].astype(int).astype(str)\n\npunt_tracking['kickingTeam'] = punt_tracking['teamName']==punt_tracking['possessionTeam']","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# Create True/False gunner and vise columns to mark gunners and vises\npunt_tracking['gunner_y_n'] = punt_tracking.apply(lambda row: row['teamNumber'] in str(row['gunners']), axis=1)\npunt_tracking['vise_y_n'] = punt_tracking.apply(lambda row: row['teamNumber'] in str(row['vises']), axis=1)","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# Create role player column to track punter/return/gunner/vise/football\npunt_tracking['rolePlayer'] = np.where(punt_tracking['nflId']==punt_tracking['kickerId'], 'punter',\n                                             np.where(punt_tracking['nflId']==punt_tracking['returnerId'], 'returner',\n                                                      np.where(punt_tracking['gunner_y_n'], 'gunner',\n                                                               np.where(punt_tracking['vise_y_n'], 'vise',\n                                                                        np.where(punt_tracking['team']=='football', 'football', 'other'\n                                                                                )\n                                                                       )\n                                                              )\n                                                     )\n                                            )","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# Determine snap frame and create new frame column relative to the snap\npunt_snaps = punt_tracking[punt_tracking['event']=='ball_snap'][['gameId', 'playId', 'frameId', 'event']].drop_duplicates()\npunt_snaps.rename(columns={'frameId' : 'snap'}, inplace=True)\n\npunt_tracking = punt_tracking.merge(punt_snaps[['gameId', 'playId', 'snap']], on=['gameId', 'playId'])\npunt_tracking['frameVsSnap'] = punt_tracking['frameId'] - punt_tracking['snap']","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"## Calculate Point in Time Metrics for Each Frame","metadata":{}},{"cell_type":"markdown","source":"#### Team Penetration by Frame","metadata":{}},{"cell_type":"code","source":"# Calculate average team position and kicking team penetration by frame\nexclude = ['punter', 'returner', 'football']\ngroup_cols = ['gameId', 'playId', 'frameVsSnap', 'teamName']\n\npenetration_by_frame_kicking = punt_tracking[(~punt_tracking['rolePlayer'].isin(exclude)) &\n                                                   (punt_tracking['kickingTeam'])\n                                                  ].groupby(group_cols)['x_adj'].mean().reset_index()\n\npenetration_by_frame_receiving = punt_tracking[(~punt_tracking['rolePlayer'].isin(exclude)) &\n                                                     (~punt_tracking['kickingTeam'])\n                                                    ].groupby(group_cols)['x_adj'].mean().reset_index()\n\n\nteam_penetration_by_frame = penetration_by_frame_kicking.merge(penetration_by_frame_receiving, on=['gameId', 'playId', 'frameVsSnap'])\nteam_penetration_by_frame.columns = ['gameId', 'playId', 'frameVsSnap', 'kickTeam', 'kickTeamPosition', 'recTeam', 'recTeamPosition']\nteam_penetration_by_frame['kickTeamPenetration'] = team_penetration_by_frame['kickTeamPosition'] - team_penetration_by_frame['recTeamPosition']","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"#### Gunner Penetration by Frame","metadata":{}},{"cell_type":"code","source":"# Calculate average gunner and vise position and average gunner penetration by frame\ninclude = ['gunner', 'vise']\n\npenetration_by_frame_gunner = punt_tracking[(punt_tracking['rolePlayer'].isin(include)) &\n                                                  (punt_tracking['kickingTeam'])\n                                                 ].groupby(group_cols)['x_adj'].mean().reset_index()\n\npenetration_by_frame_vise = punt_tracking[(punt_tracking['rolePlayer'].isin(include)) &\n                                                (~punt_tracking['kickingTeam'])\n                                               ].groupby(group_cols)['x_adj'].mean().reset_index()\n\ngunner_penetration_by_frame = penetration_by_frame_gunner.merge(penetration_by_frame_vise, on=['gameId', 'playId', 'frameVsSnap'])\ngunner_penetration_by_frame.columns = ['gameId', 'playId', 'frameVsSnap', 'kickTeam', 'gunnerPosition', 'recTeam', 'visePosition']\ngunner_penetration_by_frame['gunnerPenetration'] = gunner_penetration_by_frame['gunnerPosition'] - gunner_penetration_by_frame['visePosition']","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"#### Vertical Spread and Deviation of Gaps by Frame","metadata":{}},{"cell_type":"code","source":"# Calculate vertical spread of the punt team\nkick_team_spread = punt_tracking[(punt_tracking['kickingTeam'])\n                                      ].groupby(group_cols)['y'].agg({np.min,np.max}).reset_index()\n\nkick_team_spread['max_spread'] = kick_team_spread['amax'] - kick_team_spread['amin']","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# Calculate standard deviation of the player gaps\nkick_team_gaps = punt_tracking[punt_tracking['kickingTeam']].sort_values(['gameId', 'playId', 'frameVsSnap', 'y']).copy()\n\nkick_team_gaps['y_gap'] = kick_team_gaps['y'].diff()\n\nmask = ((kick_team_gaps['gameId']==kick_team_gaps['gameId'].shift(1)) &\n        (kick_team_gaps['playId']==kick_team_gaps['playId'].shift(1)) &\n        (kick_team_gaps['frameVsSnap']==kick_team_gaps['frameVsSnap'].shift(1))\n       )\n\nkick_team_gaps['y_gap'] = np.where(mask!=True, np.nan, kick_team_gaps['y_gap'])\n\nkick_team_gaps = kick_team_gaps.groupby(['gameId', 'playId', 'frameVsSnap'])['y_gap'].std().reset_index()\nkick_team_gaps.columns = ['gameId', 'playId', 'frameVsSnap', 'gapStdDev']","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# Merge gap deviation to spread dataframe\nkick_team_spread = kick_team_spread.merge(kick_team_gaps, on=['gameId', 'playId', 'frameVsSnap'])","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"#### Nearest Defender to Returner by Frame","metadata":{}},{"cell_type":"code","source":"# Calculate the distance between each defender and the returner, as well as average and minimum by frame\nreturner_position = punt_tracking[punt_tracking['rolePlayer']=='returner'][['gameId', 'playId', 'frameVsSnap', 'x', 'y']]\nreturner_position.columns = ['gameId', 'playId', 'frameVsSnap', 'x_returner', 'y_returner']\n\npunt_tracking = punt_tracking.merge(returner_position, on=['gameId', 'playId', 'frameVsSnap'])\n\npunt_tracking['dist_to_returner'] = np.sqrt((punt_tracking['x_returner'] - punt_tracking['x'])**2 + (punt_tracking['y_returner'] - punt_tracking['y'])**2)\n\ndist_to_returner = punt_tracking[(~punt_tracking['rolePlayer'].isin(exclude)) &\n                                 (punt_tracking['kickingTeam'])\n                                ].groupby(['gameId', 'playId', 'frameVsSnap'])['dist_to_returner'].agg({np.min,np.mean}).reset_index()\n\ndist_to_returner.columns = ['gameId', 'playId', 'frameVsSnap', 'minDist', 'meanDist']","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"### Combine Punt Details Into a Single Dataframe","metadata":{}},{"cell_type":"code","source":"def merge_frame_data(df1, df2, on_cols, merge_cols, frame=[], rename_cols=[]):\n    \"\"\"\n    Merge tracking data for specified frame(s) from specified df.\n    \"\"\"\n    for f in frame:\n        df1 = df1.merge(df2[df2['frameVsSnap']==f][merge_cols], on=on_cols)\n        if rename_cols:\n            for col in rename_cols:\n                df1.rename(columns={col : col + '_frame' + str(f)}, inplace=True)\n\n    return df1","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# Merge relevant columns from plays and pff dataframes\npff_filtered = pff_punt_plays[['gameId', 'playId', 'snapDetail', 'operationTime', 'hangTime', 'kickDirectionActual']]\npunt_plays_filtered = punt_plays[['gameId', 'playId', 'specialTeamsResult', 'penaltyCodes', 'kickLength', \n                                  'kickReturnYardage', 'playResult', 'yardsFromEndzoneBucket']]\n\npunt_data_combined = pff_filtered.merge(punt_plays_filtered, on=['gameId', 'playId'], how='inner')","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# Merge team penetration data\non_cols = ['gameId', 'playId']\nmerge_cols = on_cols + ['kickTeam', 'recTeam', 'kickTeamPosition', 'recTeamPosition']\nrename_cols = ['kickTeamPosition', 'recTeamPosition']\n\npunt_data_combined = merge_frame_data(punt_data_combined,\n                                      team_penetration_by_frame,\n                                      on_cols=on_cols,\n                                      merge_cols=merge_cols,\n                                      frame=[30],\n                                      rename_cols=rename_cols\n                                     )\n\nmerge_cols = on_cols + rename_cols\n\npunt_data_combined = merge_frame_data(punt_data_combined,\n                                      team_penetration_by_frame,\n                                      on_cols=on_cols,\n                                      merge_cols=merge_cols,\n                                      frame=[40, 50],\n                                      rename_cols=rename_cols\n                                     )","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# Merge gunner penetration data\nrename_cols = ['gunnerPenetration']\nmerge_cols = on_cols + rename_cols\n\npunt_data_combined = merge_frame_data(punt_data_combined,\n                                      gunner_penetration_by_frame,\n                                      on_cols=on_cols,\n                                      merge_cols=merge_cols,\n                                      frame=[30, 40, 50],\n                                      rename_cols=rename_cols\n                                     )","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# Merge kick team spread and gap data\nrename_cols = ['max_spread', 'gapStdDev']\nmerge_cols = on_cols + rename_cols\n\npunt_data_combined = merge_frame_data(punt_data_combined,\n                                      kick_team_spread,\n                                      on_cols=on_cols,\n                                      merge_cols=merge_cols,\n                                      frame=[30, 40, 50],\n                                      rename_cols=rename_cols\n                                     )","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# Merge defender distance data\nrename_cols = ['minDist', 'meanDist']\nmerge_cols = on_cols + rename_cols\n\npunt_data_combined = merge_frame_data(punt_data_combined,\n                                      dist_to_returner,\n                                      on_cols=on_cols,\n                                      merge_cols=merge_cols,\n                                      frame=[30, 40, 50],\n                                      rename_cols=rename_cols\n                                     )","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# Reorder the columns in the final dataframe\nnew_order = ['gameId',\n             'playId',\n             'kickTeam',\n             'recTeam',\n             'snapDetail',\n             'kickDirectionActual',\n             'specialTeamsResult',\n             'penaltyCodes',\n             'yardsFromEndzoneBucket',\n             'kickLength',\n             'kickReturnYardage',\n             'playResult',\n             'operationTime',\n             'hangTime',\n             'kickTeamPosition_frame30',\n             'kickTeamPosition_frame40',\n             'kickTeamPosition_frame50',\n             'recTeamPosition_frame30',\n             'recTeamPosition_frame40',\n             'recTeamPosition_frame50',\n             'gunnerPenetration_frame30',\n             'gunnerPenetration_frame40',\n             'gunnerPenetration_frame50',\n             'max_spread_frame30',\n             'max_spread_frame40',\n             'max_spread_frame50',\n             'gapStdDev_frame30',\n             'gapStdDev_frame40',\n             'gapStdDev_frame50',\n             'minDist_frame30',\n             'minDist_frame40',\n             'minDist_frame50',\n             'meanDist_frame30',\n             'meanDist_frame40',\n             'meanDist_frame50']\n\npunt_data_combined = punt_data_combined[new_order]","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"## Explore Correlation and Collinearity","metadata":{}},{"cell_type":"code","source":"# Change NaN to zero for kickReturnYardage\npunt_data_combined['kickReturnYardage'].fillna(0, inplace=True)","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# Display correlation matrix\ncorr = punt_data_combined[new_order[9:]].corr()\n\nplt.figure(figsize=[20,15])\nax = plt.axes()\nplt.rcParams.update({'font.size': 12})\nax.set_title('Punt Data Correlation Matrix', fontsize=22, pad=15)\nsns.heatmap(corr, xticklabels=corr.columns, yticklabels=corr.columns, cmap='RdBu', annot=True, fmt='.2f', vmin=-1.0)\nplt.show();","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# Drop output variables to perform for VIF calculation\noutput_var = ['kickReturnYardage', 'playResult']\nlinear_reg_vif = punt_data_combined[new_order[12:]].dropna()\n\n# Create constant and run VIF calculation\nX = sm.tools.add_constant(linear_reg_vif)\nvif = pd.Series([variance_inflation_factor(X.values, i) for i in range(X.shape[1])], index=X.columns)\ndisplay(vif)","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# New run of VIF, using frame 30 only\nlinear_reg_vif = punt_data_combined[['operationTime', 'hangTime', 'kickTeamPosition_frame30', 'recTeamPosition_frame30',\n                                    'gunnerPenetration_frame30', 'max_spread_frame30', 'gapStdDev_frame30', 'minDist_frame30',\n                                    'meanDist_frame30']].dropna()\n\nX = sm.tools.add_constant(linear_reg_vif)\nvif = pd.Series([variance_inflation_factor(X.values, i) for i in range(X.shape[1])], index=X.columns)\ndisplay(vif)","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# New run of VIF, frame 40 only\nlinear_reg_vif = punt_data_combined[['operationTime', 'hangTime', 'kickTeamPosition_frame40', 'recTeamPosition_frame40',\n                                    'gunnerPenetration_frame40', 'max_spread_frame40', 'gapStdDev_frame40', 'minDist_frame40',\n                                    'meanDist_frame40']].dropna()\n\nX = sm.tools.add_constant(linear_reg_vif)\nvif = pd.Series([variance_inflation_factor(X.values, i) for i in range(X.shape[1])], index=X.columns)\ndisplay(vif)","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# New run of VIF, frame 50 only\nlinear_reg_vif = punt_data_combined[['operationTime', 'hangTime', 'kickTeamPosition_frame50', 'recTeamPosition_frame50',\n                                    'gunnerPenetration_frame50', 'max_spread_frame50', 'gapStdDev_frame50', 'minDist_frame50',\n                                    'meanDist_frame50']].dropna()\n\nX = sm.tools.add_constant(linear_reg_vif)\nvif = pd.Series([variance_inflation_factor(X.values, i) for i in range(X.shape[1])], index=X.columns)\ndisplay(vif)","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"## Run Linear Regression","metadata":{}},{"cell_type":"code","source":"# Set feature and target columns and create separate dataframes (drop NA)\nx_cols = ['kickLength', 'operationTime', 'hangTime', 'kickTeamPosition_frame40', 'recTeamPosition_frame40',\n          'gunnerPenetration_frame40', 'max_spread_frame40', 'gapStdDev_frame40', 'minDist_frame40',\n          'meanDist_frame40']\n\ny_cols = ['kickReturnYardage']\n\npunt_reg_data = punt_data_combined[x_cols + y_cols].dropna()\n\nX = punt_reg_data[x_cols]\nY = punt_reg_data[y_cols]","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# Train test split\nx_train, x_test, y_train, y_test = train_test_split(X, Y, test_size=0.20, random_state=1)","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# Initialize, train, and score Random Forest Regressor\nrf_regressor = RandomForestRegressor(random_state=13)\nrf_regressor.fit(x_train, y_train)\n\nscore = rf_regressor.score(x_test, y_test)\n\nprint('Random Forest Regressor R-Squared:  ' + str(format(score, '.4f')))","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# Perform grid search for optimal Random Forest Regressor parameters\nn_estimators = [int(x) for x in np.linspace(start = 200, stop = 2000, num = 10)]\nmax_features = ['auto', 'sqrt']\nmax_depth = [int(x) for x in np.linspace(10, 110, num = 11)]\nmax_depth.append(None)\nmin_samples_split = [2, 5, 10]\nmin_samples_leaf = [1, 2, 4]\nbootstrap = [True, False]\n\nrandom_grid = {'n_estimators': n_estimators,\n               'max_features': max_features,\n               'max_depth': max_depth,\n               'min_samples_split': min_samples_split,\n               'min_samples_leaf': min_samples_leaf,\n               'bootstrap': bootstrap}\n\nrf_regressor = RandomForestRegressor(random_state=13)\nrf_random = RandomizedSearchCV(estimator=rf_regressor,\n                               param_distributions=random_grid,\n                               n_iter=100,\n                               cv=3,\n                               verbose=2,\n                               random_state=13,\n                               n_jobs = -1)","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# Train and score best estimator\nrf_random.fit(x_train, y_train)\nrf_random.best_params_\nrf_random_score = rf_random.best_estimator_.score(x_test, y_test)\n\nprint('Random Forest Regressor R-Squared:  ' + str(format(rf_random_score, '.4f')))","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# Display feature importances for best scoring estimator\nbest_rf_regressor = rf_random.best_estimator_\npd.Series(best_rf_regressor.feature_importances_, index=x_train.columns).sort_values(ascending=False)","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"## Classification Model","metadata":{}},{"cell_type":"code","source":"def sequential_backward(model_data, target, classifier):\n    \"\"\"\n    Accept a classification model and the full train/test data. Remove features one by one\n    by determining the feature whose removal results in the highest accuracy score. Return\n    a list of features in order of removal and a dataframe with all feature combinations\n    and their accuracy scores, sorted by highest accuracy.\n    \"\"\"\n    test_columns = list(model_data.columns)\n    test_columns.remove(target)\n    \n    feature_combos = pd.DataFrame()\n\n    backward_order = []\n\n    while test_columns:\n        cols = test_columns + [target]\n        data = model_data[cols]\n\n        X = data.drop(columns=target)\n        Y = data[target]\n        \n        x_train, x_test, y_train, y_test = train_test_split(X, Y, test_size=0.20, random_state=13)\n\n        classifier.fit(x_train, y_train)\n        y_pred = classifier.predict(x_test)\n        score = accuracy_score(y_test, y_pred, normalize = True)\n\n        test_instance = {'combo' : ', '.join(test_columns), 'score' : score, 'length' : len(test_columns)}\n        feature_combos = feature_combos.append(test_instance, ignore_index=True)\n\n        feature_importance = pd.Series(classifier.feature_importances_, index=x_train.columns).sort_values(ascending=False)\n        remove = feature_importance.index[-1]\n\n        test_columns.remove(remove)\n        backward_order.extend([remove])\n\n    sorted_combos = feature_combos.sort_values('score', ascending=False).reset_index()\n    \n    return backward_order, sorted_combos","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"def sequential_forward(model_data, target, classifier):\n    \"\"\"\n    Accept a classification model and the full train/test data. Find the single feature\n    that provides the best classification on its own. Then add featurues one by one\n    by determining the feature whose addition results in the highest accuracy score. Return\n    a list of features in order of addition and a dataframe with all feature combinations\n    and their accuracy scores, sorted by highest accuracy.\n    \"\"\"\n    test_columns = list(model_data.columns)\n    test_columns.remove(target)\n    \n    feature_combos = pd.DataFrame()\n\n    forward_order = []\n    i = 1\n\n    while test_columns:\n        for col in test_columns:\n            cols = [target] + forward_order + [col]\n            data = model_data[cols].copy()\n            \n            X = data.drop(columns=target)\n            Y = data[target]\n            \n            x_train, x_test, y_train, y_test = train_test_split(X, Y, test_size=0.20, random_state=13)\n\n            classifier.fit(x_train, y_train)\n            y_pred = classifier.predict(x_test)\n\n            score = accuracy_score(y_test, y_pred, normalize = True)\n\n            test_instance = {'combo' : ', '.join(cols), 'score' : score, 'length' : i}\n            feature_combos = feature_combos.append(test_instance, ignore_index=True)\n\n        remove = feature_combos[feature_combos['length']==i].sort_values('score', ascending=False).reset_index().loc[0,'combo'].split(', ')[-1]\n\n        test_columns.remove(remove)\n        forward_order.extend([remove])\n        i += 1\n\n    sorted_combos = feature_combos.sort_values('score', ascending=False).reset_index()\n    sorted_combos = sorted_combos.drop_duplicates(subset='length', keep='first')\n    \n    return forward_order, sorted_combos","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"def balance_target(df, target):\n    \"\"\"\n    Reduce all target classes to the level of the minority class.\n    \"\"\"\n    \n    limit = df[target].value_counts()[-1]\n    \n    balanced_df = pd.DataFrame()\n    \n    # Loop through each of the possible classes\n    for value in df[target].value_counts().index:\n        subset = df[df[target] == value]\n        subset = subset.sample(limit, random_state=13)\n        balanced_df = pd.concat([balanced_df, subset])\n    \n    # Return the final dataframe\n    return balanced_df","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# Make copy of punt data for classification, remove muffed punts and plays with penalties\npunt_data_classification = punt_data_combined.copy()\n\npunt_data_classification['penaltyCodes'] = punt_data_classification['penaltyCodes'].astype(str)\n\npunt_data_classification = punt_data_classification[(punt_data_classification['specialTeamsResult'] != 'Muffed') &\n                                                    (punt_data_classification['penaltyCodes'] == 'nan')]","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# Add target bucket column for kick return yardage\nconditions = [punt_data_classification['kickReturnYardage'].le(5), \n              punt_data_classification['kickReturnYardage'].gt(5) & punt_data_classification['kickReturnYardage'].le(15),\n              punt_data_classification['kickReturnYardage'].gt(15)\n             ]\n\nchoices = ['<5', '5-15', '15+']\n\npunt_data_classification['kickReturnYardageBucket'] = np.select(conditions, choices)","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# Convert kick direction to float (Center is 0, L/R is 1)\npunt_data_classification['kickDirectionActual'] = np.where(punt_data_classification['kickDirectionActual']=='C', 0, 1)","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# Mark fair catches in the target column\npunt_data_classification['kickReturnYardageBucket'] = np.where(punt_data_classification['specialTeamsResult']=='Fair Catch',\n                                                               'Fair Catch',\n                                                               punt_data_classification['kickReturnYardageBucket'])","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# Balance the classes for the modeling data\npunt_classification_balance = balance_target(punt_data_classification, 'kickReturnYardageBucket')","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"### Baseline Model","metadata":{}},{"cell_type":"code","source":"# Establish feature and target columns and set the dataframes for modeling\nx_cols = ['kickLength', 'operationTime', 'hangTime',\n          'kickDirectionActual',\n          'kickTeamPosition_frame30', 'recTeamPosition_frame30',\n          'kickTeamPosition_frame40', 'recTeamPosition_frame40',\n          'kickTeamPosition_frame50', 'recTeamPosition_frame50',\n          'gunnerPenetration_frame30', 'gunnerPenetration_frame40', 'gunnerPenetration_frame50',\n          'max_spread_frame30', 'max_spread_frame40', 'max_spread_frame50',\n          'gapStdDev_frame30', 'gapStdDev_frame40', 'gapStdDev_frame50', \n          'minDist_frame30', 'minDist_frame40', 'minDist_frame50',\n          'meanDist_frame40', 'meanDist_frame40', 'meanDist_frame50']\n\ny_cols = ['kickReturnYardageBucket']\n\nX = punt_classification_balance[x_cols]\nY = punt_classification_balance[y_cols]","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# Train test split\nx_train, x_test, y_train, y_test = train_test_split(X, Y, test_size=0.20, random_state=13)","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# Initialize, train, and score the Random Forest Classifier model\nrf_classifier = RandomForestClassifier(random_state=13)\n\nrf_classifier.fit(x_train, y_train)\n\ny_pred_train = rf_classifier.predict(x_train)\ny_pred_test = rf_classifier.predict(x_test)\n\nrf_train_score = accuracy_score(y_train, y_pred_train, normalize = True)\nrf_test_score = accuracy_score(y_test, y_pred_test, normalize = True)\n\nprint('Training Score:   ' + str(rf_train_score))\nprint('Test Score: ' + str(rf_test_score))","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# Display the feature performances\npd.Series(rf_classifier.feature_importances_, index=x_train.columns).sort_values(ascending=False)","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"### Find Optimal Feature Combinations","metadata":{}},{"cell_type":"code","source":"# Establish the initial dataframe with all possible features\npunt_classification_balance = punt_classification_balance[x_cols + y_cols]","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# Run forward and backward feature selection\nbackward_order, sorted_combos_backward = sequential_backward(punt_classification_balance, 'kickReturnYardageBucket', rf_classifier)\nforward_order, sorted_combos_forward = sequential_forward(punt_classification_balance, 'kickReturnYardageBucket', rf_classifier)","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# Display feature order from backward selection\nbackward_order[::-1]","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# Show top combinations from backward selection\nsorted_combos_backward[0:5]","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# Display feature order from forward selection\nforward_order","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# Show top combinations from forward selection\nsorted_combos_forward[0:5]","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# Set features as top feature combination from forward and backward selection\nx_cols = sorted_combos_forward.combo[0].split(', ')[1:]\n\nX = punt_classification_balance[x_cols]\nY = punt_classification_balance[y_cols]","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# Train test split\nx_train, x_test, y_train, y_test = train_test_split(X, Y, test_size=0.20, random_state=13)","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# Initialize, train, and score the Random Forest Classifier\nrf_classifier = RandomForestClassifier(random_state=13)\n\nrf_classifier.fit(x_train, y_train)\n\ny_pred_train = rf_classifier.predict(x_train)\ny_pred_test = rf_classifier.predict(x_test)\n\nrf_train_score = accuracy_score(y_train, y_pred_train, normalize = True)\nrf_test_score = accuracy_score(y_test, y_pred_test, normalize = True)\n\nprint('Training Score:   ' + str(rf_train_score))\nprint('Test Score: ' + str(rf_test_score))","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# Display feature importances for Random Forest Classifier\npd.Series(rf_classifier.feature_importances_, index=x_train.columns).sort_values(ascending=False)","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"### Hyperparameter Tuning on Top Classifier Model","metadata":{}},{"cell_type":"code","source":"# Run grid search for Random Forest Classifier\nn_estimators = [int(x) for x in np.linspace(start = 200, stop = 2000, num = 10)]\nmax_features = ['auto', 'sqrt']\nmax_depth = [int(x) for x in np.linspace(10, 110, num = 11)]\nmax_depth.append(None)\nmin_samples_split = [2, 5, 10]\nmin_samples_leaf = [1, 2, 4]\nbootstrap = [True, False]\n\nrandom_grid = {'n_estimators': n_estimators,\n               'max_features': max_features,\n               'max_depth': max_depth,\n               'min_samples_split': min_samples_split,\n               'min_samples_leaf': min_samples_leaf,\n               'bootstrap': bootstrap}\n\nrf_classifier = RandomForestClassifier(random_state=13)\nrf_random = RandomizedSearchCV(estimator=rf_classifier,\n                               param_distributions=random_grid,\n                               n_iter=100,\n                               cv=3,\n                               verbose=2,\n                               random_state=13,\n                               n_jobs = -1)\n\nrf_random.fit(x_train, y_train)","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# Display optimal Random Forest Classifier parameters\nrf_random.best_params_","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# Score the best of the Random Forest Classifier models\nrf_random_score = rf_random.best_estimator_.score(x_test, y_test)\n\nprint('Random Forest Classification Accuracy:  ' + str(format(rf_random_score, '.4f')))","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# Display feature importances for the best Random Forest Classifier models\nbest_rf_classifier = rf_random.best_estimator_\n\npd.Series(best_rf_classifier.feature_importances_, index=x_train.columns).sort_values(ascending=False)","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"## Confusion Matrices","metadata":{}},{"cell_type":"code","source":"def confusion(classifier, x_test, y_test, y_pred, model, cmap):\n    \"\"\"\n    Plot a confusion matrix for a given classifier and its test data.\n    \"\"\"\n    fig, axes = plt.subplots(nrows=1, ncols=2, figsize=(18,8))\n    fig.patch.set_alpha(0)\n\n    norm_list = ['true', 'pred']\n    titles = ['Normalized by Actual', 'Normalized by Prediction']\n    labels = ['15+', '5-15', '<5', 'Fair Catch']\n    \n    fig.suptitle(model + ' Confusion Matrices', fontsize=24, fontweight='bold')\n\n    for ax, norm, title in zip(axes.flatten(), norm_list, titles):\n        plot_confusion_matrix(classifier, \n                              x_test, \n                              y_test, \n                              ax=ax, \n                              cmap=cmap,\n                              normalize=norm,\n                              values_format='.1%',\n                              labels=labels\n                             )\n        ax.set_title(title, pad=15, fontsize=20, fontweight='bold')\n        ax.set_xlabel('Predicted Classification', labelpad=20, fontsize=20, fontweight='bold')\n        ax.set_xticklabels(labels, rotation=45, ha='right', fontsize=18)\n        ax.set_yticklabels(labels, ha='right', fontsize=18)\n        ax.set_ylabel('True Classification', labelpad=20, fontsize=20, fontweight='bold')\n        ax.grid(None)\n    \n    plt.tight_layout(pad=2.2)  \n    plt.show();","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# Make predictions using the best Random Forest Classifier\ny_pred = best_rf_classifier.predict(x_test)","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# Create custom colormap for use in heatmaps\ncolors = [(1, 1, 1), ((106/256, 235/256, 245/256)), ((22/256, 159/256, 169/256)), ((14/256, 95/256, 101/256))] # first color is black, last is red\ncm = LinearSegmentedColormap.from_list(\"Custom\", colors, N=30)","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# Display confusion matrices\nsns.set(font_scale=2.0)\nplt.rcParams.update({'font.size': 18})\n\nconfusion(classifier=best_rf_classifier, x_test=x_test, y_test=y_test, y_pred=y_pred, model='Punt Return - Random Forest', cmap=cm)","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"## Coverage Throughout the Return","metadata":{}},{"cell_type":"code","source":"# Determine plays where the punt was received and returned, exclude muffed punts, fair catches, and penalty plays\npunt_received = punt_tracking[(punt_tracking['event']=='punt_received')][['gameId', 'playId']]\npunt_received = punt_received.merge(plays[['gameId', 'playId', 'specialTeamsResult', 'possessionTeam', 'penaltyJerseyNumbers']], on=['gameId', 'playId'])\n\npunt_received['penaltyTeam'] = np.where(punt_received.apply(lambda x: str(x['possessionTeam']) in str(x['penaltyJerseyNumbers']), axis=1),\n                                        np.where(punt_received.apply(lambda x: ';' in str(x[\"penaltyJerseyNumbers\"]), axis=1), 'Both', 'Kicking'),\n                                        np.where(punt_received['penaltyJerseyNumbers'].astype(str)=='nan', 'None', 'Receiving')\n                                       )\n\npunt_returned = punt_received[(~(punt_received['specialTeamsResult'].isin(['Muffed', 'Fair Catch']))) &\n                              (~(punt_received['penaltyTeam'].isin(['Both', 'Receiving'])))\n                             ]","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# Create columns displaying the frameVsSnap for start and end of the return\nstarting_cols = ['gameId', 'playId', 'specialTeamsResult', 'possessionTeam', 'penaltyJerseyNumbers', 'penaltyTeam'] \nmerge_cols = ['gameId', 'playId', 'frameVsSnap']\n\npunt_returned = punt_returned.merge(punt_tracking[punt_tracking['event']=='punt_received'][merge_cols],\n                                    on=['gameId', 'playId'], how='left')\n\npunt_returned = punt_returned.merge(punt_tracking[punt_tracking['event']=='out_of_bounds'][merge_cols],\n                                    on=['gameId', 'playId'], how='left', suffixes=('_rec', '_oob'))\n\npunt_returned = punt_returned.merge(punt_tracking[punt_tracking['event']=='tackle'][merge_cols],\n                                    on=['gameId', 'playId'], how='left', suffixes=('_oob', '_tackle'))\n\npunt_returned = punt_returned.merge(punt_tracking[punt_tracking['event']=='touchdown'][merge_cols],\n                                    on=['gameId', 'playId'], how='left', suffixes=('_tackle', '_td'))\n\npunt_returned = punt_returned.merge(punt_tracking[punt_tracking['event']=='fumble'][merge_cols],\n                                    on=['gameId', 'playId'], how='left', suffixes=('_td', '_fumble'))\n\n\npunt_returned.columns = starting_cols + ['returnStart', 'outOfBounds', 'tackle', 'touchdown', 'fumble']\n\npunt_returned = punt_returned.drop_duplicates()\n\npunt_returned['returnEnd'] = punt_returned[['outOfBounds','tackle', 'touchdown']].min(axis=1)\npunt_returned.dropna(subset=['returnEnd'], inplace=True)","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# Exclude all plays with fumbles\npunt_returned = punt_returned[punt_returned['fumble'].astype(str)=='nan']","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# Calculate the distance between each defender and the football, as well as average and minimum by frame\nfootball_position = punt_tracking[punt_tracking['rolePlayer']=='football'][['gameId', 'playId', 'frameVsSnap', 'x', 'y']]\nfootball_position.columns = ['gameId', 'playId', 'frameVsSnap', 'x_football', 'y_football']\n\npunt_tracking = punt_tracking.merge(football_position, on=['gameId', 'playId', 'frameVsSnap'])\n\npunt_tracking['dist_to_football'] = np.sqrt((punt_tracking['x_football'] - punt_tracking['x'])**2 + (punt_tracking['y_football'] - punt_tracking['y'])**2)\n\ndist_to_football = punt_tracking[(punt_tracking['kickingTeam'])].groupby(['gameId', 'playId', 'frameVsSnap'])['dist_to_football']\\\n                                                                .agg({np.min,np.mean})\\\n                                                                .reset_index()\n\ndist_to_football.columns = ['gameId', 'playId', 'frameVsSnap', 'minDist_football', 'meanDist_football']","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# Filter tracking data to show frames that occurred during the return\npunt_tracking_during_return = punt_tracking.merge(punt_returned[['gameId', 'playId', 'returnStart', 'returnEnd']], on=['gameId', 'playId'])\n\npunt_tracking_during_return.dropna(subset=['returnStart', 'returnEnd'], inplace=True)\n\npunt_tracking_during_return = punt_tracking_during_return[(punt_tracking_during_return['frameVsSnap']>=punt_tracking_during_return['returnStart']) &\n                                                          (punt_tracking_during_return['frameVsSnap']<=punt_tracking_during_return['returnEnd'])\n                                                         ]","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# Calculate whether each defender was within two yards of the football during the return\npunt_tracking_during_return['defender_within_two_yds'] = np.where((punt_tracking_during_return['dist_to_football'] <= 2.0) &\n                                                                  (punt_tracking_during_return['kickingTeam']) &\n                                                                  (punt_tracking_during_return['rolePlayer']!='football'), 1, 0)","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# Show punt return touchdowns sorted by number of defenders within two yards throughout the play\nclose_defenders = punt_tracking_during_return.groupby(['gameId', 'playId'])['defender_within_two_yds'].sum().reset_index().sort_values('defender_within_two_yds', ascending=False)","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# Determine the length of the return in frames (time)\npunt_tracking_during_return['returnTime'] = punt_tracking_during_return['returnEnd'] - punt_tracking_during_return['returnStart']","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# Calculate movement of the football during the return\nreturn_length = punt_tracking_during_return[['gameId', 'playId', 'frameVsSnap', 'x_football', 'y_football']].drop_duplicates()\n\nreturn_length['x_change'] = return_length['x_football'].diff()\nreturn_length['y_change'] = return_length['y_football'].diff()\nreturn_length['football_movement'] = np.sqrt(return_length['x_change']**2 + return_length['y_change']**2)\n\n\nmask = ((return_length['gameId']==return_length['gameId'].shift(1)) &\n        (return_length['playId']==return_length['playId'].shift(1))\n       )\n\nreturn_length['football_movement'] = np.where(mask!=True, np.nan, return_length['football_movement'])\n\nreturn_length = return_length.groupby(['gameId', 'playId'])['football_movement'].sum().reset_index()","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# Merge tables to create return difficulty table\nreturn_difficulty = return_length.merge(close_defenders, on=['gameId', 'playId'], how='left')\n\nreturn_difficulty = return_difficulty.merge(punt_tracking_during_return[['gameId', 'playId', 'returnTime']].drop_duplicates(),\n                                            on=['gameId', 'playId'], how='left'\n                                           )","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# Add broken tackles to the return difficulty table\npff['brokenTackles'] = pff['missedTackler'].str.count(';') + 1\n\nreturn_difficulty = return_difficulty.merge(pff[['gameId', 'playId', 'brokenTackles']],\n                                            on=['gameId', 'playId'], how='left'\n                                           )\n\nreturn_difficulty['brokenTackles'].fillna(0, inplace=True)","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# Convert return difficulty metrics to percentile ranking and calculate the average of all metrics\nreturn_difficulty_perc = return_difficulty.copy()\n\nfor col in ['football_movement', 'defender_within_two_yds', 'returnTime', 'brokenTackles']:\n    return_difficulty_perc[col] = return_difficulty_perc[col].rank(pct=True)\n\nreturn_difficulty_perc['difficulty'] = 0.25 * (return_difficulty_perc['football_movement'] +\n                                               return_difficulty_perc['defender_within_two_yds'] +\n                                               return_difficulty_perc['returnTime'] +\n                                               return_difficulty_perc['brokenTackles']\n                                              )","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# Merge game and player identification information to the return difficulty table for presentation\nreturn_difficulty_perc = return_difficulty_perc.merge(games[['gameId', 'season', 'week', 'homeTeamAbbr', 'visitorTeamAbbr']],\n                                                      on=['gameId'])\n\nreturn_difficulty_perc = return_difficulty_perc.merge(punt_tracking[['gameId', 'playId', 'possessionTeam', 'returnerId']].drop_duplicates(),\n                                                      on=['gameId', 'playId'])\n\nreturn_difficulty_perc = return_difficulty_perc.merge(players[['nflId', 'displayName']], left_on=['returnerId'], right_on=['nflId'])\n\nreturn_difficulty_perc['returnTeam'] = np.where(return_difficulty_perc['possessionTeam']==return_difficulty_perc['homeTeamAbbr'],\n                                                return_difficulty_perc['visitorTeamAbbr'],\n                                                return_difficulty_perc['homeTeamAbbr']\n                                               )\n\nreturn_difficulty_perc = return_difficulty_perc.merge(punt_returned[['gameId', 'playId', 'touchdown']], on=['gameId', 'playId'])\n\n# Add touchdown column (yes/no) \nreturn_difficulty_perc['touchdown'] = np.where(return_difficulty_perc['touchdown'] > 0, 'Yes', 'No')","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# Prepare return difficulty table for presentation\npresentation_order = ['season', 'week', 'homeTeamAbbr', 'visitorTeamAbbr', 'possessionTeam', 'returnTeam',\n                      'displayName', 'touchdown', 'difficulty', 'defender_within_two_yds', 'football_movement', \n                      'returnTime', 'brokenTackles']\n\nnew_names = ['Season', 'Week', 'Home', 'Away', 'Kick Team', 'Return Team', 'Returner', 'Touchdown', 'Difficulty', 'Defender Proximity',\n             'Total Movement', 'Return Time', 'Broken Tackles']\n\nreturn_difficulty_presentation = return_difficulty_perc[presentation_order]\n\nreturn_difficulty_presentation.columns = new_names","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"### Punt Return Touchdowns - Degree of Difficulty","metadata":{}},{"cell_type":"code","source":"# Display punt return TD's sorted by difficulty\ncm = sns.color_palette('RdYlGn', as_cmap=True)\nheatmap_cols = ['Difficulty', 'Defender Proximity', 'Total Movement', 'Return Time', 'Broken Tackles']\n\nreturn_difficulty_presentation[return_difficulty_presentation['Touchdown']=='Yes'].sort_values('Difficulty', ascending=False)\\\n                                                                                  .style.hide_index()\\\n                                                                                  .background_gradient(cmap=cm, subset=heatmap_cols)\n","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"### All Punt Returns - Degree of Difficulty","metadata":{}},{"cell_type":"code","source":"return_difficulty_presentation.sort_values('Difficulty', ascending=False)\\\n                              .style.hide_index()\\\n                              .background_gradient(cmap=cm, subset=heatmap_cols)\n","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"## Gunner Penetration and Vise Protection","metadata":{}},{"cell_type":"markdown","source":"#### Filter for Punt Plays of 40+ Yards with No Penalties","metadata":{}},{"cell_type":"code","source":"# Determine list of plays that meet criteria:  no penalties, 40+ yard punt\npunt_play_subset = plays.copy()\npunt_play_subset['penaltyCodes'] = punt_play_subset['penaltyCodes'].astype(str)\n\npunt_play_subset = punt_play_subset[(punt_play_subset['kickLength'] >= 40) &\n                                    (punt_play_subset['penaltyCodes'] == 'nan')]\n\n# Filter gunner and vise tracking data for punt plays meeting the above criteria\npunt_play_subset['game_play'] = punt_play_subset['gameId'].astype(str) + '-' + punt_play_subset['playId'].astype(str)\ngunners_and_vises_tracking = punt_tracking[punt_tracking['rolePlayer'].isin(['gunner', 'vise'])].copy()\ngunners_and_vises_tracking['game_play'] = gunners_and_vises_tracking['gameId'].astype(str) + '-' + gunners_and_vises_tracking['playId'].astype(str)\n\ngunners_and_vises_tracking = gunners_and_vises_tracking[gunners_and_vises_tracking['game_play'].isin(punt_play_subset['game_play'])]","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"#### Match Each Gunner with a Vise and Combine Their Tracking Data to One Row Per Frame","metadata":{}},{"cell_type":"code","source":"# Filter for gunner and vise positions at the snap\ngunners_and_vises_at_snap = gunners_and_vises_tracking[gunners_and_vises_tracking['frameVsSnap']==0].copy()\n\n# Count number of gunners and vises per play and only keep plays with even numbers of gunners and vises (one on one)\ngunner_vise_count = gunners_and_vises_at_snap.groupby(['gameId', 'playId', 'rolePlayer']).size().unstack(fill_value=0).reset_index()\none_on_one_punts = gunner_vise_count[gunner_vise_count['gunner']==gunner_vise_count['vise']]\n\none_on_one_at_snap = gunners_and_vises_at_snap.merge(one_on_one_punts, \n                                                     on=['gameId', 'playId'],\n                                                     how='right')[['gameId', 'playId', 'nflId', 'rolePlayer', 'x', 'y']]","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# Separate gunners and vises and then concatenate horizontally, matching each gunner with the vise lined up against him\ngunners_at_snap = one_on_one_at_snap[one_on_one_at_snap['rolePlayer']=='gunner'].sort_values(['gameId', 'playId', 'y']).reset_index(drop=True)\nvises_at_snap = one_on_one_at_snap[one_on_one_at_snap['rolePlayer']=='vise'].sort_values(['gameId', 'playId', 'y']).reset_index(drop=True)\n\ngunners_vs_vises = pd.concat([gunners_at_snap[['gameId', 'playId', 'nflId']], vises_at_snap[['nflId']]], axis=1).reset_index(drop=True)\n\ngunners_vs_vises.columns = ['gameId', 'playId', 'gunnerId', 'viseId']","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# Separate tracking data for gunners and vises and concatenate horizonally, matching each gunner with the vise lined up against him\ngunner_tracking = gunners_and_vises_tracking[gunners_and_vises_tracking['rolePlayer']=='gunner'][['gameId', 'playId', 'nflId', 'frameVsSnap', 'x', 'y']]\nvise_tracking = gunners_and_vises_tracking[gunners_and_vises_tracking['rolePlayer']=='vise'][['gameId', 'playId', 'nflId', 'frameVsSnap', 'x', 'y']]\n\ngunner_cols = ['gameId', 'playId', 'gunnerId', 'frameVsSnap', 'x_gunner', 'y_gunner']\nvise_cols = ['gameId', 'playId', 'viseId', 'frameVsSnap', 'x_vise', 'y_vise']\n\ngunner_tracking.columns = gunner_cols\nvise_tracking.columns = vise_cols\n\ngunner_vs_vise_tracking = gunner_tracking.merge(gunners_vs_vises, on=['gameId', 'playId', 'gunnerId'])\n\ngunner_vs_vise_tracking = gunner_vs_vise_tracking.merge(vise_tracking, on=['gameId', 'playId', 'frameVsSnap', 'viseId'])","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# Display the resulting dataframe, showing gunner and matching vise tracking data by frame\ngunner_vs_vise_tracking.head()","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"#### Calculate Gunner Penetration by Frame (Absolute and Relative to Vise Position)","metadata":{}},{"cell_type":"code","source":"# Determine gunner and vise positions at the snap and add to tracking data\npositions_at_snap = gunner_vs_vise_tracking[gunner_vs_vise_tracking['frameVsSnap']==0]\npositions_at_snap.columns = ['gameId', 'playId', 'gunnerId', 'frameVsSnap', 'x_gunner_snap', 'y_gunner_snap',\n                             'viseId', 'x_vise_snap', 'y_vise_snap']\n\ngunner_vs_vise_tracking = gunner_vs_vise_tracking.merge(positions_at_snap[['gameId', 'playId', 'gunnerId', 'x_gunner_snap', \n                                                                           'y_gunner_snap', 'x_vise_snap', 'y_vise_snap']],\n                                                        on=['gameId', 'playId', 'gunnerId'])","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# Calculate gunner penetration (yards downfield) in absolute terms and verus the vise position\ngunner_vs_vise_tracking['gunner_penetration'] = gunner_vs_vise_tracking['x_gunner'] - gunner_vs_vise_tracking['x_gunner_snap']\ngunner_vs_vise_tracking['gunner_penetration_vs_vise'] = gunner_vs_vise_tracking['x_gunner'] - gunner_vs_vise_tracking['x_vise']","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# Display the resulting dataframe, showing gunner and matching vise tracking data by frame\ngunner_vs_vise_tracking.head()","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"#### Rank Gunners and Vises by Penetration Metrics","metadata":{}},{"cell_type":"code","source":"def top_gunner_vise(df, players, metric, frame, cutoff, id_field, ascend=False):\n    \"\"\"\n    Return df with top gunners/vises for given metric and frame of play. Only return if number of punts cutoff is met.\n    \"\"\"\n    \n    # Calculate average of metric for given frame\n    best_by_frame = df[df['frameVsSnap']==frame].groupby(id_field)[[metric]]\\\n                                                .mean()\\\n                                                .reset_index()\\\n                                                .sort_values(metric, ascending=ascend)\n    \n    # Determine number of qualifying punt plays for each gunner/vise\n    punt_count = df[df['frameVsSnap']==frame].groupby(id_field)[[metric]].count().reset_index().sort_values(metric, ascending=ascend)\n\n    # Merge punt play count to average by player and exclude players who do not meet the cutoff\n    best_by_frame = best_by_frame.merge(punt_count, on=id_field).sort_values(metric + '_y', ascending=ascend)\\\n                                 .merge(players[['nflId', 'displayName']], left_on=id_field, right_on='nflId').drop(columns='nflId')\n    \n    best_by_frame.columns = [id_field, 'avg_' + metric, 'punt_plays', 'name']\n    \n    best_by_frame = best_by_frame[best_by_frame['punt_plays']>cutoff][['name', id_field, 'avg_' + metric, 'punt_plays']]\\\n                        .sort_values('avg_' + metric, ascending=ascend)\n    \n    # Add ranking column and return dataframe with ranking\n    best_by_frame['rank'] = best_by_frame['avg_' + metric].rank(ascending=ascend).astype(int)\n    \n    return best_by_frame[['rank', id_field, 'name', 'avg_' + metric, 'punt_plays']].reset_index(drop=True)","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# Establish number of punt plays required to be ranked\ncutoff = 15","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# Calculate absolute and relative penetration for gunners 3, 4, and 5 seconds into the play\ngunner_penetration_frame30 = top_gunner_vise(df=gunner_vs_vise_tracking, \n                                             players=players,\n                                             metric='gunner_penetration', \n                                             frame=30, \n                                             cutoff=cutoff,\n                                             id_field='gunnerId',\n                                             ascend=False)\n\ngunner_penetration_frame40 = top_gunner_vise(df=gunner_vs_vise_tracking, \n                                             players=players,\n                                             metric='gunner_penetration', \n                                             frame=40, \n                                             cutoff=cutoff,\n                                             id_field='gunnerId',\n                                             ascend=False)\n\ngunner_penetration_frame50 = top_gunner_vise(df=gunner_vs_vise_tracking, \n                                             players=players,\n                                             metric='gunner_penetration', \n                                             frame=50, \n                                             cutoff=cutoff,\n                                             id_field='gunnerId',\n                                             ascend=False)\n\ngunner_penetration_vs_vise_frame30 = top_gunner_vise(df=gunner_vs_vise_tracking, \n                                                     players=players,\n                                                     metric='gunner_penetration_vs_vise', \n                                                     frame=30, \n                                                     cutoff=cutoff,\n                                                     id_field='gunnerId',\n                                                     ascend=False)\n\ngunner_penetration_vs_vise_frame40 = top_gunner_vise(df=gunner_vs_vise_tracking, \n                                                     players=players,\n                                                     metric='gunner_penetration_vs_vise', \n                                                     frame=40, \n                                                     cutoff=cutoff,\n                                                     id_field='gunnerId',\n                                                     ascend=False)\n\ngunner_penetration_vs_vise_frame50 = top_gunner_vise(df=gunner_vs_vise_tracking, \n                                                     players=players,\n                                                     metric='gunner_penetration_vs_vise', \n                                                     frame=50, \n                                                     cutoff=cutoff,\n                                                     id_field='gunnerId',\n                                                     ascend=False)","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# Merge gunner rankings into a single dataframe and organize for presentation\noriginal_cols = ['gunnerId', 'name', 'punt_plays', 'rank']\nmerge_cols = ['gunnerId', 'rank']\n\ngunner_merged = gunner_penetration_frame30[original_cols].merge(gunner_penetration_frame40[merge_cols], on='gunnerId')\n\ngunner_merged = gunner_merged.merge(gunner_penetration_frame50[merge_cols], on='gunnerId')\n\ngunner_merged = gunner_merged.merge(gunner_penetration_vs_vise_frame30[merge_cols], on='gunnerId')\n\ngunner_merged = gunner_merged.merge(gunner_penetration_vs_vise_frame40[merge_cols], on='gunnerId')\n\ngunner_merged = gunner_merged.merge(gunner_penetration_vs_vise_frame50[merge_cols], on='gunnerId')\n\ngunner_merged.columns = ['gunnerId', 'Name', 'Punt Plays', 'Penetration (3 Sec)', 'Penetration (4 Sec)', 'Penetration (5 Sec)',\n                         'Vs Vise (3 Sec)', 'Vs Vise (4 Sec)', 'Vs Vise (5 Sec)']","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# Calculate absolute and relative penetration vs vises 3, 4, and 5 seconds into the play\nvise_penetration_frame30 = top_gunner_vise(df=gunner_vs_vise_tracking, \n                                           players=players,\n                                           metric='gunner_penetration', \n                                           frame=30, \n                                           cutoff=cutoff,\n                                           id_field='viseId',\n                                           ascend=True)\n\nvise_penetration_frame40 = top_gunner_vise(df=gunner_vs_vise_tracking, \n                                           players=players,\n                                           metric='gunner_penetration', \n                                           frame=40, \n                                           cutoff=cutoff,\n                                           id_field='viseId',\n                                           ascend=True)\n\nvise_penetration_frame50 = top_gunner_vise(df=gunner_vs_vise_tracking, \n                                           players=players,\n                                           metric='gunner_penetration', \n                                           frame=50, \n                                           cutoff=cutoff,\n                                           id_field='viseId',\n                                           ascend=True)\n\nvise_penetration_vs_vise_frame30 = top_gunner_vise(df=gunner_vs_vise_tracking, \n                                                   players=players,\n                                                   metric='gunner_penetration_vs_vise', \n                                                   frame=30, \n                                                   cutoff=cutoff,\n                                                   id_field='viseId',\n                                                   ascend=True)\n\nvise_penetration_vs_vise_frame40 = top_gunner_vise(df=gunner_vs_vise_tracking, \n                                                   players=players,\n                                                   metric='gunner_penetration_vs_vise', \n                                                   frame=40, \n                                                   cutoff=cutoff,\n                                                   id_field='viseId',\n                                                   ascend=True)\n\nvise_penetration_vs_vise_frame50 = top_gunner_vise(df=gunner_vs_vise_tracking, \n                                                   players=players,\n                                                   metric='gunner_penetration_vs_vise', \n                                                   frame=50, \n                                                   cutoff=cutoff,\n                                                   id_field='viseId',\n                                                   ascend=True)","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# Merge gunner rankings into a single dataframe and organize for presentation\noriginal_cols = ['viseId', 'name', 'punt_plays', 'rank']\nmerge_cols = ['viseId', 'rank']\n\nvise_merged = vise_penetration_frame30[original_cols].merge(vise_penetration_frame40[merge_cols], on='viseId')\n\nvise_merged = vise_merged.merge(vise_penetration_frame50[merge_cols], on='viseId')\n\nvise_merged = vise_merged.merge(vise_penetration_vs_vise_frame30[merge_cols], on='viseId')\n\nvise_merged = vise_merged.merge(vise_penetration_vs_vise_frame40[merge_cols], on='viseId')\n\nvise_merged = vise_merged.merge(vise_penetration_vs_vise_frame50[merge_cols], on='viseId')\n\nvise_merged.columns = ['viseId', 'Name', 'Punt Plays', 'Penetration (3 Sec)', 'Penetration (4 Sec)', 'Penetration (5 Sec)',\n                         'Vs Vise (3 Sec)', 'Vs Vise (4 Sec)', 'Vs Vise (5 Sec)']","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"#### Gunner Rankings","metadata":{}},{"cell_type":"code","source":"# Add \"Average Rank\" column, sort, add heatmap, and present\ngunner_merged['Average Rank'] = gunner_merged[['Penetration (3 Sec)', 'Penetration (4 Sec)', 'Penetration (5 Sec)',\n                                               'Vs Vise (3 Sec)', 'Vs Vise (4 Sec)', 'Vs Vise (5 Sec)']].mean(axis=1)\n\ncm = sns.color_palette('RdYlGn_r', as_cmap=True)\n\nno_heatmap_cols = ['Name', 'Punt Plays']\nheatmap_cols = ['Average Rank', 'Penetration (3 Sec)', 'Penetration (4 Sec)', 'Penetration (5 Sec)',\n                'Vs Vise (3 Sec)', 'Vs Vise (4 Sec)', 'Vs Vise (5 Sec)']\n\ngunner_merged.sort_values('Average Rank')[no_heatmap_cols + heatmap_cols].style.hide_index()\\\n                                                                               .background_gradient(cmap=cm,subset=heatmap_cols)","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"#### Vise Rankings","metadata":{}},{"cell_type":"code","source":"# Add \"Average Rank\" column, sort, add heatmap, and present\nvise_merged['Average Rank'] = vise_merged[['Penetration (3 Sec)', 'Penetration (4 Sec)', 'Penetration (5 Sec)',\n                                             'Vs Vise (3 Sec)', 'Vs Vise (4 Sec)', 'Vs Vise (5 Sec)']].mean(axis=1)\n\nvise_merged.sort_values('Average Rank')[no_heatmap_cols + heatmap_cols].style.hide_index()\\\n                                                                             .background_gradient(cmap=cm,subset=heatmap_cols)","metadata":{"trusted":true},"execution_count":null,"outputs":[]}]}