{"cells":[{"metadata":{"_uuid":"3350166502d0f39f4cdf137c5036737092446eff"},"cell_type":"markdown","source":"## Abstract\n\nThis kernel produces my analysis for Kaggle's NFL Punt Analytics Competition.  My goal is to reduce the number of concussions on punt plays, through rule changes that are supported by the given data.  Two important aspects of the potential rule changes are that they cannot drastically alter the fabric of the game and that they must have the ability to be easily implemented.  I believe both of my proposed rule changes satisfy these requirements and will have a positive impact on punt safety in the NFL.\n\n## Executive Summary\nMy analysis concludes with two proposes rules changes regarding punt plays:\n1. **Gunner Blockers** - When Team A presents a punt formation, Team B may have at most 1 blocker aligned opposite each of Team A’s end men on the line of scrimmage (gunners) at the snap of the ball. \n\n2. **Touchbacks** - If the result of the punt play is a touchback,  the dead-ball spot for the following snap will be from the 25-yard line.\n\nThe code below will comprehensively take you through my thought process; starting with exploring the data to the conclusions and proposals.  For each of the rule change, the code will:\n* Describe & visualize the rule changes\n* Examine the evidence and reasoning supported by the data\n* Forecast the benefits, should the rules be implemented\n* Explore potential unintended consequences\n\nThe two rule changes are independent of one another, however they are based on the notion that decreasing punt ***returns*** will decrease injuries."},{"metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"scrolled":true},"cell_type":"code","source":"#Import packages\nimport pandas as pd\nimport numpy as np\nimport matplotlib.pyplot as plt\nfrom matplotlib.colors import ListedColormap\n%matplotlib inline\nimport re\nimport warnings\nwarnings.filterwarnings('ignore')\nfrom plotly.offline import init_notebook_mode, iplot\nfrom plotly import tools\nimport plotly.graph_objs as go\ninit_notebook_mode(connected=True)\nfrom IPython.display import display, HTML","execution_count":null,"outputs":[]},{"metadata":{"_uuid":"73d35be5b44718c0ff8152137b6a247731044ae2"},"cell_type":"markdown","source":"I use the below function to load the field layout. Special thanks to Chris Crawford for providing the code in his starter kernel."},{"metadata":{"_kg_hide-input":true,"_kg_hide-output":true,"trusted":true,"_uuid":"f8f6f2b0f6644a9413e63d3e3ca16b8d280798e8"},"cell_type":"code","source":"def load_field():\n    field = dict(\n        title = \"Player Activity\",\n        plot_bgcolor='darkseagreen',\n        showlegend=True,\n        xaxis=dict(\n            autorange=False,\n            range=[0, 120],\n            showgrid=False,\n            zeroline=False,\n            showline=True,\n            linecolor='black',\n            linewidth=1,\n            mirror=True,\n            ticks='',\n            tickmode='array',\n            tickvals=[10,20, 30, 40, 50, 60, 70, 80, 90, 100, 110],\n            ticktext=['Goal', 10, 20, 30, 40, 50, 40, 30, 20, 10, 'Goal'],\n            showticklabels=True\n        ),\n        yaxis=dict(\n            title='',\n            autorange=False,\n            range=[-3.3,56.3],\n            showgrid=False,\n            zeroline=False,\n            showline=True,\n            linecolor='black',\n            linewidth=1,\n            mirror=True,\n            ticks='',\n            showticklabels=False\n         ),\n        shapes=[\n            dict(\n                type='line',\n                layer='below',\n                x0=0,\n                y0=0,\n                x1=120,\n                y1=0,\n                line=dict(\n                    color='white',\n                    width=2\n                )\n            ),\n            dict(\n                type='line',\n                layer='below',\n                x0=0,\n                y0=53.3,\n                x1=120,\n                y1=53.3,\n                line=dict(\n                    color='white',\n                    width=2\n                )\n            ),\n            dict(\n                type='line',\n                layer='below',\n                x0=10,\n                y0=0,\n                x1=10,\n                y1=53.3,\n                line=dict(\n                    color='white',\n                    width=10\n                )\n            ),\n            dict(\n                type='line',\n                layer='below',\n                x0=20,\n                y0=0,\n                x1=20,\n                y1=53.3,\n                line=dict(\n                    color='white'\n                )\n            ),\n            dict(\n                type='line',\n                layer='below',\n                x0=30,\n                y0=0,\n                x1=30,\n                y1=53.3,\n                line=dict(\n                    color='white'\n                )\n            ),\n            dict(\n                type='line',\n                layer='below',\n                x0=40,\n                y0=0,\n                x1=40,\n                y1=53.3,\n                line=dict(\n                    color='white'\n                )\n            ),\n            dict(\n                type='line',\n                layer='below',\n                x0=50,\n                y0=0,\n                x1=50,\n                y1=53.3,\n                line=dict(\n                    color='white'\n                )\n            ),\n            dict(\n                type='line',\n                layer='below',\n                x0=60,\n                y0=0,\n                x1=60,\n                y1=53.3,\n                line=dict(\n                    color='white'\n                )\n            ),dict(\n                type='line',\n                layer='below',\n                x0=70,\n                y0=0,\n                x1=70,\n                y1=53.3,\n                line=dict(\n                    color='white'\n                )\n            ),dict(\n                type='line',\n                layer='below',\n                x0=80,\n                y0=0,\n                x1=80,\n                y1=53.3,\n                line=dict(\n                    color='white'\n                )\n            ),\n            dict(\n                type='line',\n                layer='below',\n                x0=90,\n                y0=0,\n                x1=90,\n                y1=53.3,\n                line=dict(\n                    color='white'\n                )\n            ),dict(\n                type='line',\n                layer='below',\n                x0=100,\n                y0=0,\n                x1=100,\n                y1=53.3,\n                line=dict(\n                    color='white'\n                )\n            ),\n            dict(\n                type='line',\n                layer='below',\n                x0=110,\n                y0=0,\n                x1=110,\n                y1=53.3,\n                line=dict(\n                    color='white',\n                    width=10\n                )\n            )\n        ]\n    )\n    \n    return field","execution_count":null,"outputs":[]},{"metadata":{"_cell_guid":"79c7e3d0-c299-4dcb-8224-4455121ee9b0","_uuid":"d629ff2d2480ee46fbb7e2d37f6b5fab8052498a","trusted":true},"cell_type":"code","source":"#Read in Data\ngame_data = pd.read_csv('../input/game_data.csv')\nplay_info = pd.read_csv('../input/play_information.csv')\nplay_player_role = pd.read_csv('../input/play_player_role_data.csv')\nplayer_punt = pd.read_csv('../input/player_punt_data.csv')\nvideo_footage_control = pd.read_csv('../input/video_footage-control.csv')\nvideo_footage_injury = pd.read_csv('../input/video_footage-injury.csv')\nvideo_review = pd.read_csv('../input/video_review.csv')","execution_count":null,"outputs":[]},{"metadata":{"_uuid":"ad65d4bc24d731fd88fa09644754f4bc508e86df"},"cell_type":"markdown","source":"I'll create a function to read in all of the Next Gen Stats data.  I'll then concatenate the individual files into one large file to make the NGS data easier to work with.\n"},{"metadata":{"trusted":true,"_uuid":"0f93ab2e409049c23916e6478ff57dfd8b6fe44a"},"cell_type":"code","source":"def read_NGS_data(file_lst):\n    for i, file in enumerate(file_lst):\n        print('Reading in {}'.format(file))\n        data = pd.read_csv('../input/' + file)\n        if i == 0:\n            NGS_df = data\n            del data\n        else:\n            NGS_df = pd.concat([NGS_df, data])\n            del data\n    return NGS_df\n\nfile_lst = ['NGS-2016-pre.csv','NGS-2016-reg-wk1-6.csv','NGS-2016-reg-wk7-12.csv','NGS-2016-reg-wk13-17.csv','NGS-2016-post.csv','NGS-2017-pre.csv','NGS-2017-reg-wk1-6.csv','NGS-2017-reg-wk7-12.csv','NGS-2017-reg-wk13-17.csv','NGS-2017-post.csv']\n\nNGS_df = read_NGS_data(file_lst)","execution_count":null,"outputs":[]},{"metadata":{"_uuid":"e6e87f86fa8170bc2e4eec9fdff8eb93490aefb4"},"cell_type":"markdown","source":"## Exploratory Data Analysis"},{"metadata":{"trusted":true,"_uuid":"e6d2d5adf0e8f87235f56e464aca80c8f4faa8b9","_kg_hide-input":true},"cell_type":"code","source":"print('There are concussion injuries on ' \\\n      + str(round(len(video_review) / float(len(play_info)) * 100, 2)) \\\n      + '% of ' + 'punt plays')","execution_count":null,"outputs":[]},{"metadata":{"_uuid":"0f0e508adf74d06300f594cbb6dcadb38ed302ae"},"cell_type":"markdown","source":"My initial reaction is that head injuries are already rare on punt plays (<1%).\nHowever, given the severity of these injuries, any reduction to concussions would be beneficial for the NFL.\n### How are players being injured?\nExplore:\n* Primary player acitivities\n* Impact types (ex. Helmet-to-Helmet)\n* Friendly Fire\n* Partner activities\n\nI create bar graphs with Plotly to show the counts for each of the bullet points."},{"metadata":{"trusted":true,"_uuid":"bc75f3a53efb454eb60cd1b4955a246f1de97d58","scrolled":true,"_kg_hide-input":true},"cell_type":"code","source":"trace1 = go.Bar(\n        x=video_review.groupby(['Player_Activity_Derived'], \\\n                               as_index=False)['PlayID'].count()['Player_Activity_Derived'],\n        y=video_review.groupby(['Player_Activity_Derived'],\\\n                               as_index=False)['PlayID'].count()['PlayID']\n    )\ntrace2 = go.Bar(\n        x=video_review.groupby(['Primary_Impact_Type'], \\\n                               as_index=False)['PlayID'].count()['Primary_Impact_Type'],\n        y=video_review.groupby(['Primary_Impact_Type'], \\\n                               as_index=False)['PlayID'].count()['PlayID'],\n    )\ntrace3 = go.Bar(\n        x=video_review.groupby(['Friendly_Fire'], \\\n                               as_index=False)['PlayID'].count()['Friendly_Fire'],\n        y=video_review.groupby(['Friendly_Fire'], \\\n                               as_index=False)['PlayID'].count()['PlayID'],\n    )\ntrace4 = go.Bar(\n        x=video_review.groupby(['Primary_Partner_Activity_Derived'], \\\n                               as_index=False)['PlayID'].count()['Primary_Partner_Activity_Derived'],\n        y=video_review.groupby(['Primary_Partner_Activity_Derived'], \\\n                               as_index=False)['PlayID'].count()['PlayID'],\n    )\n\nfig = tools.make_subplots(rows=2,\n                          cols=2, \n                          subplot_titles=('Player Activity Derived', \n                                          'Primary Impact Type',\n                                          'Friendly Fire', \n                                          'Primary Partner Activity Derived')\n                         )\n\nfig.append_trace(trace1, 1, 1)\nfig.append_trace(trace2, 1, 2)\nfig.append_trace(trace3, 2, 1)\nfig.append_trace(trace4, 2, 2)\n\nfig['layout'].update(showlegend=False)\n\niplot(fig, filename='injury-eda')","execution_count":null,"outputs":[]},{"metadata":{"trusted":true,"_uuid":"a065ee4d500839099625afed418897579294fa00"},"cell_type":"markdown","source":"#### Quick Conclusions\n* No primary player activity significantly stands out\n* Surprisingly, helmet-to-helmet and helmet-to-body are involved in the same amount of injuries\n* Friendly fire is only clear 16% of the time\n* No partner activity Stands Out\n\n### Who's getting hurt?\nExplore:\n* The side of the ball (coverage vs return)\n* The punt role of the players involved\n\nBelow, I categorize player punt roles into sides of the ball and create bar graphs to visualize the injuries on each side of the ball for primary and partner."},{"metadata":{"trusted":true,"_uuid":"51d185830c094423afae74db5e69247be0d13a88","_kg_hide-input":true},"cell_type":"code","source":"return_roles = ['PDL1','PDL2','PDL3','PDL4','PDL5','PDL6','PDM','PDR1','PDR2','PDR3','PDR4','PDR5','PDR6'\n                ,'PFB','PLL','PLL1','PLL2','PLL3','PLM','PLM1','PLR','PLR1','PLR2','PLR3','PR','VL','VLi'\n                ,'VLo','VR','VRi','VRo']\n\ncoverage_roles = ['GL','GLi','GLo','GR','GRi','GRo','P','PC','PLG','PLS','PLT','PLW','PPL','PPLi','PPLo'\n                 ,'PPR','PPRi','PPRo','PRG','PRT','PRW']\n\n#Merge the player role data with the injury review data on the primary player\ninj_players = video_review.merge(play_player_role, how='inner', on=['Season_Year', 'GameKey', 'PlayID', 'GSISID'])\ninj_players.rename(columns={'Role':'inj_role'}, inplace=True)\n\n#Create a column to determine side of the ball\ninj_players['inj_side_of_ball'] = np.where(inj_players.inj_role.isin(return_roles), 'return',\n                                          np.where(inj_players.inj_role.isin(coverage_roles), 'coverage', ''))\n\n#Merge the player role data with the injury review data on the partner player\npartner_players = video_review[['Season_Year', 'GameKey', 'PlayID', 'Primary_Partner_GSISID']]\npartner_players['Primary_Partner_GSISID'] = partner_players.loc[:,'Primary_Partner_GSISID'] \\\n                                                .replace('Unclear','0').fillna(0).astype(int)\npartner_players = partner_players.merge(play_player_role, how='left', \\\n                                        left_on=['Season_Year', 'GameKey', 'PlayID', 'Primary_Partner_GSISID'],\\\n                                       right_on =['Season_Year', 'GameKey', 'PlayID', 'GSISID'])\npartner_players = partner_players.drop('GSISID', axis=1)\npartner_players.rename(columns={'Role':'partner_role'}, inplace=True)\n\n#Create a column to determine side of the ball\npartner_players['partner_side_of_ball'] = np.where(partner_players.partner_role.isin(return_roles), 'return',\n                                          np.where(partner_players.partner_role.isin(coverage_roles), 'coverage', ''))\n\n#Concatenate the primary and partner dataframes\ninj_partner_df = pd.concat([inj_players,partner_players[['partner_role','partner_side_of_ball']]], axis = 1)","execution_count":null,"outputs":[]},{"metadata":{"trusted":true,"_uuid":"5f826923a5c76c1e0f4260ae80e9ff652081faec","_kg_hide-input":true,"scrolled":true},"cell_type":"code","source":"trace1 = go.Bar(\n        x=inj_partner_df.groupby(['inj_side_of_ball'], as_index=False)['PlayID'].count()['inj_side_of_ball'],\n        y=inj_partner_df.groupby(['inj_side_of_ball'], as_index=False)['PlayID'].count()['PlayID']\n    )\n\ntrace2 = go.Bar(\n        x=inj_partner_df.groupby(['partner_side_of_ball'], as_index=False)['PlayID'].count()['partner_side_of_ball'],\n        y=inj_partner_df.groupby(['partner_side_of_ball'], as_index=False)['PlayID'].count()['PlayID']\n    )\n\nfig = tools.make_subplots(rows=1, cols=2, subplot_titles=('Injured Side of Ball', 'Partner Side of Ball'))\n\nfig.append_trace(trace1, 1, 1)\nfig.append_trace(trace2, 1, 2)\n\nfig['layout'].update(showlegend=False)\n\niplot(fig, filename='side-of-ball')","execution_count":null,"outputs":[]},{"metadata":{"_uuid":"3dbd5320f91e0c71d95d3612be6dc5aac486a756"},"cell_type":"markdown","source":"Pivot the injury df to see count of injuries by primary and partner combination"},{"metadata":{"trusted":true,"_uuid":"bb31c6596303c21d9c6d64883f530f3c6e3ccb67","_kg_hide-input":true},"cell_type":"code","source":"pd.pivot_table(inj_partner_df, index=['inj_role'], columns=['partner_role'],\n              values='GSISID', aggfunc='count', margins=True).fillna('-')","execution_count":null,"outputs":[]},{"metadata":{"_uuid":"cf5141bb292f600a18741efa02dc3d1d087b1504"},"cell_type":"markdown","source":"#### Quick Conclusions\n* The primary player injured is a member of the coverage team 73% of the time\n* There is less of a chasm between the side of the ball for partner players\n    * 4 instances with no partner player\n* Looking at specific roles:\n    * PR is involved in 13 total injuries - 8 of which are as a partner\n    * The top coverage positions as primary injuries are PRG, PLW, PLG, GL\n    * There is only 1 combination with more than 1 injury - PRG & PR\n\n### On what types of plays are players getting hurt?\n\nExplore:\n* What are the outcomes of punt plays?\n* What are the frequencies of each outcome?\n* What are the proportions of injuries for each outcome?\n\nI start by adding a column in the play_info dataframe to label the general outcome of play.  I'll then merge the play_info dataframe with the play_info dataframe in order to see the play outcomes with injuries.  By grouping and aggregating on the outcome column, I can compare the injury rates by outcome."},{"metadata":{"trusted":true,"_uuid":"16ac98571e9d123006e528432cc56cf0c928edf0","_kg_hide-input":true},"cell_type":"code","source":"play_info['outcome'] =  np.where(play_info['PlayDescription'].str.contains('aborted|Fumbled snap|FUMBLES, and recovers', flags=re.IGNORECASE, regex=True), 'aborted',\n                        np.where(play_info['PlayDescription'].str.contains('fake|pass|right end|left end|up the middle|Direct snap|right guard', flags=re.IGNORECASE, regex=True), 'fake',\n                        np.where(play_info['PlayDescription'].str.contains('muffs', flags=re.IGNORECASE, regex=True), 'muff',         \n                        np.where(play_info['PlayDescription'].str.contains('fair catch by', flags=re.IGNORECASE, regex=True), 'fair_catch',\n                        np.where(play_info['PlayDescription'].str.contains('touchback', flags=re.IGNORECASE, regex=True), 'touchback',\n                        np.where(play_info['PlayDescription'].str.contains('blocked|deflected', flags=re.IGNORECASE, regex=True), 'blocked',\n                        np.where(play_info['PlayDescription'].str.contains('out of bounds.', flags=re.IGNORECASE, regex=False), 'oob',\n                        np.where(play_info['PlayDescription'].str.contains('downed', flags=re.IGNORECASE, regex=True), 'downed', \n                        np.where(play_info['PlayDescription'].str.contains('safety', flags=re.IGNORECASE, regex=True), 'safety',\n                        np.where(play_info['PlayDescription'].str.contains('[0-9]+ for [-+]?[0-9]+ yards?|for no gain|touchdown|(to [A-Z]+ [0-9]+ for [-+]?[0-9]+ yards?)|(to [0-9]+ for [-+]?[0-9]+ yards?)', flags=re.IGNORECASE, regex=True), 'return',         \n                        np.where(play_info['PlayDescription'].str.contains('- no play|delay of game|false start, declined|penalty enforced', flags=re.IGNORECASE, regex=True), 'no_play', ' ')))))))))))\n\n#Merge the play info df with the injury df\npi = play_info.merge(video_review[['Season_Year', 'GameKey', 'PlayID','GSISID']], how='left', on =['Season_Year', 'GameKey', 'PlayID'])\npi['injury'] = np.where(pi.GSISID.notnull(), 1, 0).astype(int)\npi.drop('GSISID', axis = 1, inplace=True)\npi_inj_grouped = pi.groupby(['outcome'], as_index=False)['injury'] \\\n    .agg({'total_plays':'count','injuries':sum}) \\\n    .sort_values('total_plays', ascending = False) \\\n    .reset_index(drop=True)\n\npi_inj_grouped['injury_rate'] = round(pi_inj_grouped['injuries'] / pi_inj_grouped['total_plays'] * 100, 1).astype(str) + '%'\npi_inj_grouped","execution_count":null,"outputs":[]},{"metadata":{"_uuid":"a7d3ca5eb264b44d1ed1d0ac0788505f115a5518"},"cell_type":"markdown","source":"#### Quick Conclusions: \n* Returns are the most frequent outcome (41%)\n* Over 1% of punt plays with a return have an injury\n* This is 10x higher than when a fair catch is called\n* The next 3 most frequent outcomes have lower than average injury rates:\n    * downed 0.4%\n    * out of bounds 0%\n    * touchback 0%\n    \nSince the majority of injuries are occuring when there is a return, I'll look to find the differences between returns and other outcomes, starting with the time the distance between the returner and the coverage team.\n\n### Returner and Coverage Team Metrics During Punt\nExplore:\n* Distribution of punt hang times\n    * will need a general sense of how long the ball is in the air\n* For each second from the time the ball is punted to the time it is received:\n    * calculate distance from each coverage man to the returner\n    * calculate the speed (meters per sec) for each player\n\n#### Punt Hang Times\nI create a function to help calculate the time in secs from punt to land.  Then counts are visualized in a histogram and summary statistics are presented.  Since we don't have event data on the location of the football, I limit the events to punts that are either received or fair caught."},{"metadata":{"trusted":true,"_uuid":"d6c76e25b95b5e42ea1f5caefb94551382c0e82b","_kg_hide-input":true},"cell_type":"code","source":"def get_hang_time(ngs_df, start_event='punt', *stop_events):\n    punt_event = ngs_df.loc[ngs_df.Event==start_event] \\\n        .groupby(['Season_Year', 'GameKey','PlayID'], as_index = False)['Time'].min()\n    punt_event.rename(columns = {'Time':'punt_time'}, inplace=True)\n    punt_event['punt_time'] = pd.to_datetime(punt_event['punt_time'],\\\n                                             format='%Y-%m-%d %H:%M:%S.%f')\n    \n    receiving_event = ngs_df.loc[ngs_df.Event.isin(stop_events)] \\\n        .groupby(['Season_Year', 'GameKey','PlayID'], as_index = False)['Time'].min()\n    receiving_event.rename(columns = {'Time':'receiving_time'}, inplace=True)\n    receiving_event['receiving_time'] = pd.to_datetime(receiving_event['receiving_time'],\\\n                                             format='%Y-%m-%d %H:%M:%S.%f')\n    \n    punt_df = punt_event.merge(receiving_event, how='inner', on = ['Season_Year','GameKey','PlayID']) \\\n                .reset_index(drop=True)\n    \n    punt_df['hang_time'] = (punt_df['receiving_time'] - punt_df['punt_time']).dt.total_seconds()\n    \n    return punt_df\n\npunt_df = get_hang_time(NGS_df, 'punt', 'punt_received', 'fair_catch')\n\n#Visualize the hang times with a histogram\ndata = [go.Histogram(x=punt_df.hang_time)]\n\nlayout = go.Layout(\n    title='Hang Time Histogram',\n    xaxis=dict(\n        title='Seconds'\n    ),\n    yaxis=dict(\n        title='Punt Count'\n    )\n)\n\nfig = go.Figure(data=data, layout=layout)\niplot(fig, filename='hang-hist')","execution_count":null,"outputs":[]},{"metadata":{"trusted":true,"_uuid":"f38b625e35f8cc2782298514bbf7af241595600f","_kg_hide-input":true},"cell_type":"code","source":"#Print a few summary statistics\nprint('The mean hang time is ' + str(round(punt_df['hang_time'].mean(), 1)))\nprint('The median hang time is ' + str(round(punt_df['hang_time'].median(), 1)))\nprint('The standard deviation ' + str(round(punt_df['hang_time'].std(), 1)))\nprint(str(round(len(punt_df.loc[punt_df.hang_time < 6]) / len(punt_df) * 100, 1))\\\n      + '% of hang times are less than 5 1/2 seconds')","execution_count":null,"outputs":[]},{"metadata":{"_uuid":"5d5e988a45e660b4e34729705e0b621e4bf1034b"},"cell_type":"markdown","source":"### Space, Speed & Blocker Counts\nIn order to analyze the distance between every coverage player and the returner, I created the below function.  Along with distance (in yards) it also performs a speed calculation (in meters per second) on 1 second intervals starting at the time of the ball being punted. Another aspect of this function is that it counts the number of gunner blockers for each play.\n\n*Note the coverage_returner_space function may take 30 mins to an hour to complete, but it is a key component in constructing the dataframe for the rest of the analysis"},{"metadata":{"_kg_hide-output":true,"trusted":true,"_uuid":"825722d9f1763e8f628cf6b68581acd95cfca326","scrolled":true},"cell_type":"code","source":"def coverage_returner_space(play_df, ngs_df):\n    cov_ret_lst = []\n    gunner_blockers = ['VL','VLi','VLo','VR','VRi','VRo']\n    for i in range(0, len(play_df)):\n        season_key = play_df['Season_Year'][i]\n        game_key = play_df['GameKey'][i]\n        play_id = play_df['PlayID'][i]\n        outcome = play_df['outcome'][i]\n        injury = play_df['injury'][i]\n        if i % 100 == 0:\n            print('{} / {}'.format(i, len(play_df)))\n        if game_key in ngs_df.GameKey and play_id in ngs_df.PlayID:\n            filtered_play = ngs_df.loc[(ngs_df.GameKey == game_key) \\\n                                       & (ngs_df.PlayID == play_id)].sort_values('Time').reset_index(drop=True)\n            filtered_play = filtered_play.merge(play_player_role, \\\n                                                how='inner', on = ['Season_Year','GameKey','PlayID','GSISID'])\n            if len(filtered_play) > 0:\n                filtered_play['Time'] = pd.to_datetime(filtered_play['Time'], \\\n                                                       format='%Y-%m-%d %H:%M:%S.%f')\n                punt_event_time = filtered_play.loc[filtered_play.Event == 'punt'].Time.min()\n                receiving_event_time = filtered_play.loc[(filtered_play.Event == 'punt_received') | \\\n                                                         (filtered_play.Event == 'fair_catch')].Time.min()\n                gunner_blocker_count = len(filtered_play.loc[filtered_play['Role'].isin(gunner_blockers)]['Role'].unique())\n                filtered_play = filtered_play.loc[(filtered_play.Time >= punt_event_time) & \\\n                                                  (filtered_play.Time <= receiving_event_time)]\n                coverage_df = filtered_play.loc[filtered_play['Role'].isin(coverage_roles)].sort_values('Time')\n                coverage_df.rename(columns={'x':'cov_x', \n                                          'y': 'cov_y',\n                                          'GSISID': 'cov_GSISID',\n                                          'dis': 'cov_dis',\n                                          'o': 'cov_o',\n                                          'dir': 'cov_dir',\n                                          'Role': 'cov_Role'\n                                         }, inplace=True)\n                \n                returner_df = filtered_play.loc[filtered_play['Role'] == 'PR'].sort_values('Time')\n                returner_df.rename(columns={'x':'ret_x', \n                                          'y': 'ret_y',\n                                          'GSISID': 'ret_GSISID',\n                                          'dis': 'ret_dis',\n                                          'o': 'ret_o',\n                                          'dir': 'ret_dir',\n                                          'Role': 'ret_Role'\n                                         }, inplace=True)\n                returner_df = returner_df.drop('Event', axis = 1)\n                \n                cov_ret_df = coverage_df.merge(returner_df, how ='inner', on = ['Season_Year','GameKey','PlayID','Time'])\n                cov_ret_df['dis_from_ret'] = ((cov_ret_df['cov_x'] -  cov_ret_df['ret_x']) ** 2 \\\n                                           + (cov_ret_df['cov_y'] -  cov_ret_df['ret_y']) ** 2).apply(np.sqrt)\n                cov_ret_df['time_since_punt'] = cov_ret_df['Time'] - punt_event_time\n                times_to_capture = [punt_event_time + pd.Timedelta(seconds=i) for i in range(0, 7)]\n                cov_ret_df = cov_ret_df.loc[cov_ret_df['Time'].isin(times_to_capture)]\n                cov_ret_df['gunner_blockers'] = gunner_blocker_count\n                cov_ret_df['outcome'] = outcome\n                cov_ret_df['injury'] = injury\n                cov_ret_df['cov_speed'] = convert_to_meters_per_sec(cov_ret_df.cov_dis, 9.1)\n                cov_ret_df['ret_speed'] = convert_to_meters_per_sec(cov_ret_df.ret_dis, 9.1)\n                if len(cov_ret_df) > 0:\n                    cov_ret_lst.append(cov_ret_df)\n                    \n    cov_ret_df = pd.concat(cov_ret_lst).reset_index(drop=True)          \n    return cov_ret_df\n\ndef convert_to_meters_per_sec(dis_vector, converter):\n    mps_vector = dis_vector * converter\n    return mps_vector\n\ncov_ret_df = coverage_returner_space(pi, NGS_df)","execution_count":null,"outputs":[]},{"metadata":{"trusted":true,"_uuid":"d75504ff0a002862628cbd83a0fcf5e3cc2c3395"},"cell_type":"markdown","source":"#### Injury Play Animation and Distance Calculation\n- For an added visualization, below is animation of an example play\n- It tracks the paths of an injured punt returner and the cover man involved in the tackle\n- The animation also shows the distance between the two players during the play\n- Begin by pressing the \"Play\" button"},{"metadata":{"trusted":true,"_uuid":"bf33f38075667f0fd9c507b6cd7337de25d0148d","_kg_hide-input":true},"cell_type":"code","source":"#Selecting an exmple injury play for returner and coverman\nex_play = NGS_df.loc[(NGS_df.Season_Year==2016) & (NGS_df.GameKey== 234) \\\n                     & (NGS_df.PlayID== 3278) & (NGS_df.GSISID== 28620)\n                    ].sort_values('Time')\n\nret_play = NGS_df.loc[(NGS_df.Season_Year==2016) & (NGS_df.GameKey== 234) \\\n                     & (NGS_df.PlayID== 3278) & (NGS_df.GSISID== 27860)].sort_values('Time')\n\n#Removing time prior to the ball being snapped\nball_snap_time = ex_play.loc[ex_play.Event == 'ball_snap'].Time.min()\nex_play = ex_play.loc[ex_play.Time >= ball_snap_time].reset_index(drop=True)\nret_play = ret_play.loc[ret_play.Time >= ball_snap_time].reset_index(drop=True)\n\n#Filling NA's in the event column with the prior event\nex_play['Event'] = ex_play['Event'].fillna(method='ffill')\n\n#Data for Animation\nx = np.array(ex_play.x)\ny = np.array(ex_play.y)\nxx = np.array(ex_play.x)\nyy = np.array(ex_play.y)\n\nx1 = np.array(ret_play.x)\ny1 = np.array(ret_play.y)\nxx1 = np.array(ret_play.x)\nyy1 = np.array(ret_play.y)\n\nN = len(x)\n\ndata=[dict(x=x, y=y, \n            name='Distance',\n            mode='lines',\n            textposition='bottom center',\n            line=dict(width=2, color=None)\n          ),\n      dict(x=x, y=y, \n            name='Injured Player',\n            mode='markers',\n            marker=dict(color=None, size=15)\n          ),\n      dict(x=x1, y=y1, \n           name = 'Partner Player',\n           mode='markers',\n           marker=dict(color='orange', size=15)\n         )\n    ]\n\nlayout = load_field()\nlayout['hovermode'] = 'closest'\nlayout['updatemenus'] = [{'type': 'buttons',\n                           'buttons': [{'label': 'Play',\n                                        'method': 'animate',\n                                        'args': [None]}]}]\n\nframes=[dict(data=[dict(x=[x1[k]], \n                        y=[y1[k]], \n                        mode='markers', \n                        marker=dict(color='#013369', size=15),\n                        name='Partner Player'\n                        ),\n                   dict(x=[x[k]], \n                        y=[y[k]], \n                        mode='markers', \n                        marker=dict(color='orange', size=15),\n                        name='Injured Player'\n                        ), \n                   dict(x=[xx[k], xx1[k], None, xx[k], xx1[k]], \n                        y=[yy[k], yy1[k], None, yy[k], yy1[k]], \n                        mode='lines', \n                        text='Distance: {}'.format(round(np.sqrt((xx1[k] - xx[k])**2 + (yy1[k] - yy[k])**2),0)),\n                        textposition='bottom center',\n                        line=dict(color='#2c3539', width=2),\n                        name='Distance'\n                       )\n                  ], layout=dict(title=ex_play.Event[k],\n                                 annotations=[\n                                     dict(x=100,\n                                          y=5,\n                                          showarrow=False,\n                                          font=dict(\n                                              family='Courier New, monospace',\n                                              size=14,\n                                              color='#ffffff'),\n                                          align='center',\n                                          bordercolor='#c7c7c7',\n                                          borderwidth=2,\n                                          borderpad=4,\n                                          bgcolor='#2c3539',\n                                          opacity=0.8,\n                                          text='{} Yds'.format(round(np.sqrt((xx1[k] - xx[k])**2 + (yy1[k] - yy[k])**2),0)),\n                                          )\n                                 ]\n                                )\n            ) for k in range(0, N, 5)]\n          \nfigure1=dict(data=data, layout=layout, frames=frames)\niplot(figure1)","execution_count":null,"outputs":[]},{"metadata":{"_uuid":"e9eabeb5d50ffd1252b665896eb1e14fc74bf7d7"},"cell_type":"markdown","source":"### Analyzing coverage and returner data frame\n\nNow that the dataframe from the coverage_returner_space function is construted, I can dive into some serious analysis\n\nExplore:\n* Does the space between the coverage team and the returner impact the return rate?\n* How far is each coverage position from the returner during the punt?\n* When does the coverage team close the most distance?\n* Which coverage positions have the greatest impact on returns?\n    * Focusing on returns vs fair catches\n    \nI start by pivoting the data to show mean distance at each second for return vs fair catch    "},{"metadata":{"trusted":true,"_uuid":"9469bd07418e67e33bdc94d4158b0d5d604e4695","_kg_hide-input":true},"cell_type":"code","source":"dist_pivot = pd.pivot_table(cov_ret_df.loc[cov_ret_df.outcome.isin(['return', 'fair_catch'])]\n                            , index='time_since_punt'\n                            , columns='outcome'\n                            , values='dis_from_ret', aggfunc='mean').round(1)\n\ndist_pivot['delta'] = dist_pivot['return'] - dist_pivot['fair_catch']\ndist_pivot","execution_count":null,"outputs":[]},{"metadata":{"_uuid":"9889c1d7637b5957842a655ef2ae22f549b408c9"},"cell_type":"markdown","source":"I then want to see how close each position is to the returner on average."},{"metadata":{"trusted":true,"_uuid":"12b107b36c0833e48a02e3bf3cc033d01c7c9699","_kg_hide-input":true},"cell_type":"code","source":"#Group inside and outside gunners into left and right\ncov_ret_df = cov_ret_df.replace('GLi', 'GL').replace('GLo', 'GL').replace('GRi', 'GR').replace('GRo', 'GR')\n#On average, how close is each position to the returner?\ncov_ret_df.groupby(['cov_Role'], as_index=False)['dis_from_ret'] \\\n    .agg({'count':'count', 'dis_from_ret':np.mean}) \\\n    .sort_values('dis_from_ret').round(1)","execution_count":null,"outputs":[]},{"metadata":{"_uuid":"3b893e8ded0f1ee00927f1291d63c68613ccddb6"},"cell_type":"markdown","source":"A line graph to shows the average distance from the returner for each coverage position at each second of the punt"},{"metadata":{"trusted":true,"_uuid":"82bdbd5d1d6cb006148c13d383de82c0431102d7","_kg_hide-input":true},"cell_type":"code","source":"by_time_pos = cov_ret_df.groupby(['time_since_punt', 'cov_Role'], as_index=False)['dis_from_ret'].mean()\n\ntrace_lst = []\n\nfor role in by_time_pos.cov_Role.unique():\n    if role not in ['Go', 'PC', 'PPLi', 'PPLo']:\n        trace = go.Scatter(\n            x = by_time_pos.loc[by_time_pos.cov_Role == role].time_since_punt.dt.total_seconds(),\n            y = by_time_pos.loc[by_time_pos.cov_Role == role].dis_from_ret,\n            mode = 'lines',\n            name = role\n        )\n\n        trace_lst.append(trace)\n\ndata = trace_lst\n\nlayout = dict(title = 'Distance From Returner by Coverage Position ',\n              xaxis = dict(title = 'Seconds from Punt'),\n              yaxis = dict(title = 'Avg Distance (Yds)', \n                           range = [0,60]),\n              hovermode = 'closest')\n\nfig = dict(data=data, layout=layout)\n\niplot(fig, filename='line-dist')","execution_count":null,"outputs":[]},{"metadata":{"_uuid":"c2b879077b6033d354394dfb871e367cc14b6f23"},"cell_type":"markdown","source":"A grouped bar graph shows the avg distance from the returner for each coverage position by return vs fair catch."},{"metadata":{"trusted":true,"_uuid":"368cba14d99e26c972782b071d8ace47be73fa3f","_kg_hide-input":true},"cell_type":"code","source":"pos_grouped_df = cov_ret_df.groupby(['cov_Role', 'outcome'], as_index=False)['dis_from_ret'].agg({'mean_dist':np.mean,\n                                                                                                  'count': 'count'})\n                                                                                                  \npos_grouped_df = pos_grouped_df.loc[(pos_grouped_df.cov_Role != 'PPLi') & \\\n                                    (pos_grouped_df.cov_Role != 'PPLo') & \\\n                                    (pos_grouped_df.cov_Role != 'PC') & \\\n                                    (pos_grouped_df.cov_Role != 'Go')]\n\ntrace1 = go.Bar(\n        x=pos_grouped_df.loc[pos_grouped_df.outcome == 'fair_catch']['cov_Role'],\n        y=pos_grouped_df.loc[pos_grouped_df.outcome == 'fair_catch']['mean_dist'],\n        name='fair catch'\n    )\n\ntrace2 = go.Bar(\n        x=pos_grouped_df.loc[pos_grouped_df.outcome == 'return']['cov_Role'],\n        y=pos_grouped_df.loc[pos_grouped_df.outcome == 'return']['mean_dist'],\n        name='return'\n    )\n\ndata = [trace1, trace2]\nlayout = go.Layout(\n    barmode='group',\n    xaxis=dict(title='Position'),\n    yaxis= dict(title='Avg Distance From Returner (Yds)')\n)\n\nfig=go.Figure(data=data, layout=layout)\niplot(fig, filename='grouped-bar')","execution_count":null,"outputs":[]},{"metadata":{"_uuid":"6ebef38a59a80eb3dc48bc78e17674ab71503ce0"},"cell_type":"markdown","source":"From the graphs above, it's clear that on average the gunners are closest to the returner.  The below table shows at each second, how often a gunner is the closest to the returner."},{"metadata":{"trusted":true,"_uuid":"a577f38dafa28672223bfb53e370aade6c03de96","_kg_hide-input":true},"cell_type":"code","source":"min_dist_from_ret = cov_ret_df.groupby(['Season_Year', 'GameKey','PlayID','time_since_punt'],\n                                       as_index = False)['dis_from_ret'].min()\n\nclosest_df = cov_ret_df.merge(min_dist_from_ret.drop('time_since_punt', axis = 1),\n                              how = 'inner',\n                              on=['Season_Year', 'GameKey', 'PlayID', 'dis_from_ret'])\n\n\nclosest_df = closest_df.groupby(['time_since_punt','cov_Role'],\n                   as_index=False)['dis_from_ret'].agg({'# of times closest': 'count',\n                                                        'avg distance':np.mean})\\\n        .sort_values(['time_since_punt', '# of times closest'], ascending=[True, False])\\\n        .reset_index(drop=True)\n\ngunner_times_closest = []\nother_times_closest = []\n\nfor sec in closest_df.time_since_punt.unique():\n    sec_df = closest_df.loc[closest_df.time_since_punt == sec]\n    gunner_times_closest.append(sec_df.loc[sec_df.cov_Role.isin(['GR', 'GL'])]['# of times closest'].sum())\n    other_times_closest.append(sec_df.loc[-sec_df.cov_Role.isin(['GR', 'GL'])]['# of times closest'].sum())\n    \nfinal_closest_df = pd.DataFrame({'time_since_punt': closest_df.time_since_punt.unique(),\n            'gunner_closest': gunner_times_closest,\n              'other_closest': other_times_closest\n             }, columns = ['time_since_punt', 'gunner_closest', 'other_closest'])\n\nfinal_closest_df['gunner_closest_perc'] = (final_closest_df['gunner_closest'] / \\\n                                          (final_closest_df['gunner_closest'] + final_closest_df['other_closest']) \\\n                                            * 100).round(1).astype(str) + '%'\n\nfinal_closest_df[['time_since_punt','gunner_closest_perc']].set_index('time_since_punt')","execution_count":null,"outputs":[]},{"metadata":{"_uuid":"2fb2a5f865eebf38c4a0b51fd4343a79235beb8d"},"cell_type":"markdown","source":"#### Quick Conclusions: \n* It's clear gunners are the most impactful players on the coverage team\n    * They close the distance to the returner the quickest\n    * On average, they are 8 yards closer than any other position\n    * The gunners are the closest cover men to the returner 70-90% of the time\n    \nSince it is very rare that the return team has 0, 1, or 5 gunner blockers, we filter those plays out"},{"metadata":{"trusted":true,"_uuid":"83ac8f431a31d5c16103a06b81f374354dd8c71d","_kg_hide-input":true},"cell_type":"code","source":"print(round(cov_ret_df.gunner_blockers.value_counts(normalize=True), 2))\ncov_ret_df = cov_ret_df.loc[cov_ret_df.gunner_blockers.isin([2, 3, 4])]","execution_count":null,"outputs":[]},{"metadata":{"_uuid":"983afed315b06335095db507ab5070392108d83c"},"cell_type":"markdown","source":"Visualize play counts by alignment with a pie graph"},{"metadata":{"trusted":true,"_uuid":"8c72501c721d4b4a917532d34c5633bb99a67d62","_kg_hide-input":true},"cell_type":"code","source":"#\nlabels = cov_ret_df.groupby(['gunner_blockers'], \\\n                   as_index=False)['PlayID'].agg({\"play_count\": pd.Series.nunique})['gunner_blockers']\n\nvalues = cov_ret_df.groupby(['gunner_blockers'], \\\n                   as_index=False)['PlayID'].agg({\"play_count\": pd.Series.nunique})['play_count']\n\ntrace = go.Pie(labels=labels, values=values)\n\niplot([trace], filename='alignment_pie')","execution_count":null,"outputs":[]},{"metadata":{"_uuid":"7dac70db574f65dd089513b63a930e004c298bfb"},"cell_type":"markdown","source":"Create a table to show the return rates by the # of gunner blockers"},{"metadata":{"trusted":true,"_uuid":"b24bf7333314832467704c3fab8801784c045dd7","_kg_hide-input":true},"cell_type":"code","source":"cov_ret_count = cov_ret_df.groupby('gunner_blockers', \\\n                                     as_index=False)['PlayID'].agg({\"play_count\": pd.Series.nunique}) \n\ncov_ret_returns = cov_ret_df.loc[cov_ret_df.outcome == 'return'].groupby('gunner_blockers', \\\n                                     as_index=False)['PlayID'].agg({\"return_count\": pd.Series.nunique})\n\ncov_ret_merged = cov_ret_count.merge(cov_ret_returns)\n\ncov_ret_merged['return_rate'] = (cov_ret_merged['return_count'] / \n                                 cov_ret_merged['play_count'] * 100).round(1).astype(str) + '%'\n\n#Create similar table but with injury rates\ncov_ret_injury = cov_ret_df.loc[cov_ret_df.injury == 1].groupby('gunner_blockers', \\\n                                     as_index=False)['PlayID'].agg({\"injury_count\": pd.Series.nunique})\n\ncov_ret_merged = cov_ret_merged.merge(cov_ret_injury)\n\ncov_ret_merged['injury_rate'] = (cov_ret_merged['injury_count'] / \n                                 cov_ret_merged['play_count'] * 100).round(1).astype(str) + '%'\n\n#Merge the two data frames and only show the rates for easier viewing\ncov_ret_merged[['gunner_blockers', 'return_rate', 'injury_rate']]","execution_count":null,"outputs":[]},{"metadata":{"_uuid":"e4f2a4abd85ebf111bbb07470fc7b518e7e8ed68"},"cell_type":"markdown","source":"Speed (in meters per second) vs Number of Gunner Blockers at each second"},{"metadata":{"trusted":true,"_uuid":"50025f3812427161a27a59dab1444e2bfa7496e2","_kg_hide-input":true},"cell_type":"code","source":"cov_ret_gun = cov_ret_df.loc[(cov_ret_df.cov_Role == 'GR') |\n                              (cov_ret_df.cov_Role == 'GL')]\n\npd.pivot_table(cov_ret_gun, \n               index=['gunner_blockers'], values='cov_speed',\n               columns=['time_since_punt'], aggfunc=[np.mean]).round(1)","execution_count":null,"outputs":[]},{"metadata":{"_uuid":"4e445e48850f50058e1013ad10a398986db24706"},"cell_type":"markdown","source":"With a line graph, visualize the change in speed per second, grouped by the number of blockers"},{"metadata":{"trusted":true,"_uuid":"ae1701d8b93c7be8970183c2053d3986caa3e078","_kg_hide-input":true},"cell_type":"code","source":"cov_ret_gun_grouped = cov_ret_gun.groupby(['gunner_blockers', 'time_since_punt'], as_index=False)['cov_speed'].mean()\n\ntrace_lst = []\n\nfor gun in cov_ret_gun_grouped.gunner_blockers.unique():\n    trace = go.Scatter(\n        x = cov_ret_gun_grouped.loc[cov_ret_gun_grouped.gunner_blockers == gun].time_since_punt.dt.total_seconds(),\n        y = cov_ret_gun_grouped.loc[cov_ret_gun_grouped.gunner_blockers == gun].cov_speed,\n        mode = 'lines',\n        name = str(gun)\n    )\n\n    trace_lst.append(trace)\n\ndata = trace_lst\n\nlayout = dict(title = 'Gunner Speed by # of Blockers',\n              xaxis = dict(title = 'Seconds from Punt'),\n              yaxis = dict(title = 'Avg Speed (Meters per Sec)'),\n              hovermode = 'closest')\n\nfig = dict(data=data, layout=layout)\n\niplot(fig, filename='line-speed')","execution_count":null,"outputs":[]},{"metadata":{"_uuid":"84adaafd82908f34104b9b4216999f5cd68906d8"},"cell_type":"markdown","source":"#### Quick Conclusions: \n* 2 gunner blockers have the lowest return rate:\n    * 35% less than 3 gunner blockers\n    * 38% less than 4 gunner blockers\n* 2 gunner blockers have the lowest injury rate:\n    * 40% less than 3 gunner blockers\n    * 50% less than 4 gunner blockers\n*    Although 2 gunner blockers allow the gunners to reach max speed the quickest, in this alignment the gunners are able to \"throttle down\" down their speed by the time they get close to the returner\n    * This is a really interesting finding.  Gunners will actually be able to make contact with the returner at a lesser speed if they are able to reach higher speeds earlier on in the play\n    \n### Proposal #1\nWhen Team A presents a punt formation:\n\nTeam B may have at most 1 blocker aligned opposite each of Team A’s end men on the line of scrimmage (gunners) at the snap of the ball\n\n#### Show correct vs incorrect alignment    "},{"metadata":{"trusted":true,"_uuid":"60c7ddf4c6f8e9acc233a7c716f15b7826d6cdf9","_kg_hide-input":true},"cell_type":"code","source":"def visualize_alignment(next_gen_df, game_id_lst, role_df):\n    alignment_df = next_gen_df.loc[(next_gen_df.Season_Year == game_id_lst[0]) & \\\n                        (next_gen_df.GameKey == game_id_lst[1]) & \\\n                        (next_gen_df.PlayID == game_id_lst[2]) & \\\n                        (next_gen_df.Event == game_id_lst[3])].sort_values('y').reset_index(drop=True)\n    \n    align_merged = alignment_df.merge(play_player_role, how='left', \\\n                                          on =['Season_Year', 'GameKey', 'PlayID', 'GSISID'])\n    \n    align_merged['side_of_ball'] = np.where(align_merged.Role.isin(return_roles), 'return',\n                                          np.where(align_merged.Role.isin(coverage_roles), 'coverage', ''))\n    \n    trace1 = go.Scatter(\n        x = align_merged.loc[align_merged.side_of_ball == 'return'].x,\n        y = align_merged.loc[align_merged.side_of_ball == 'return'].y,\n        mode = 'markers',\n        marker = dict(color='#013369', size=10),\n        name = 'Return'\n    )\n\n    trace2 = go.Scatter(\n        x = align_merged.loc[align_merged.side_of_ball == 'coverage'].x,\n        y = align_merged.loc[align_merged.side_of_ball == 'coverage'].y,\n        mode = 'markers',\n        marker = dict(color='orange', size=10),\n        name = 'Coverage'\n    )\n    \n    #Change alignment of one of the players\n    align_merged.at[2,'y'] = 7\n\n    trace3 = go.Scatter(\n        x = align_merged.loc[align_merged.side_of_ball == 'return'].x,\n        y = align_merged.loc[align_merged.side_of_ball == 'return'].y,\n        mode = 'markers',\n        marker = dict(color='#013369', size=10),\n        name = 'Return'\n    )\n\n    trace4 = go.Scatter(\n        x = align_merged.loc[align_merged.side_of_ball == 'coverage'].x,\n        y = align_merged.loc[align_merged.side_of_ball == 'coverage'].y,\n        mode = 'markers',\n        marker = dict(color='orange', size=10),\n        name = 'Coverage'\n    )\n\n    fig = tools.make_subplots(rows=1, cols=2, subplot_titles=('Legal Alignment', 'Illegal Alignment'))\n\n    fig.append_trace(trace1, 1, 1)\n    fig.append_trace(trace2, 1, 1)\n    fig.append_trace(trace3, 1, 2)\n    fig.append_trace(trace4, 1, 2)\n    \n    fig['layout'].update(showlegend=False)\n    \n    return iplot(fig, filename='alignments')\n\nvisualize_alignment(NGS_df, [2016, 234, 3278, 'ball_snap'], play_player_role)","execution_count":null,"outputs":[]},{"metadata":{"_uuid":"86cdacef1719a4f51e4d5c57ab230cc7eadabdb5"},"cell_type":"markdown","source":"### Forecasting the benefit of Proposal #1\nThe data shows that there is conservatively a 35% reduction in returns when the return team uses 2 gunner blockers compared with 3 or 4.  Therefore the below calculation can be made to get a sense of the  approximate impact the rule change would have on injuries:"},{"metadata":{"trusted":true,"_uuid":"3c81b06c5e5633dc2eb20efbd6e388c51dea9b06"},"cell_type":"code","source":"returns_per_season = len(play_info.loc[play_info.outcome=='return'])/2\nreturn_rate1 = .3\nreturn_rate2 = .4\n#injury rates taken from earlier EDA\nreturn_inj_rate = .011\nnon_return_inj_rate = .0002\n\n\n#Lower Bound\nlower_inj = round(((returns_per_season * return_rate1) * return_inj_rate) \\\n      - ((returns_per_season * return_rate1) * non_return_inj_rate), 1)\n\nprint('With a 30% reduction in return rate, there would be a drop of approximately {} injuries'\n     .format(int(lower_inj)))\n\n#Upper Bound\nupper_inj = round(((returns_per_season * return_rate2) * return_inj_rate) \\\n      - ((returns_per_season * return_rate2) * non_return_inj_rate), 1)\n\nprint('With a 40% reduction in return rate, there would be a drop of approximately {} injuries'\n     .format(round(upper_inj),1))\n","execution_count":null,"outputs":[]},{"metadata":{"_uuid":"c1cff92af61394fc0ac5b3effd8f4a3d47d62663"},"cell_type":"markdown","source":"#### Quick Conclusions: \n* If this rule were to be implemented, I forecast that the return rate would be reduced by 30-40%, which would reduce the injury rate by a similar margin\n* This reduction in the return rate would eliminate 400-550 returns per season, which would lead to approximately 4-6 less concussion injuries per season"},{"metadata":{"_uuid":"9609008e4fa317c19e6c7468ecfef7402452dee4"},"cell_type":"markdown","source":"## Continuing EDA for 2nd Proposal\n\nUnfortunately, the dataset provided does not have event data on the football (only by player).  It would be great to know the location of the football at every 10th of a second.  However, the play information can be parsed to get a general sense of where the ball started, stopped, etc.\n\nExplore:\n* How far do punts travel?\n* What yard line do punts land?\n* How often do outcomes occur?\n\nThe function below utilizes regular expression to parse the description field of the play_info data in order to extract features for the above bullet points. I then create 3 histograms for Punt Distance, Punt To yard line, Hang Time and print summary statistics."},{"metadata":{"trusted":true,"_uuid":"b10960f7b7d4523dbd50e5576d2e5166f681f8b6","_kg_hide-input":true},"cell_type":"code","source":"def parse_play_description(df, outcome_lst):\n    parsed_df = play_info.loc[play_info.outcome.isin(outcome_lst),\\\n                             ['Season_Year','GameKey','PlayID',\\\n                              'PlayDescription','outcome']].reset_index(drop=True)\n    punt_to_lst = []\n    punt_dist_lst = []\n    return_dist_lst = []\n    punt_regex = '(punts [0-9]+ yards? to [A-Z]* [-+]?[0-9]+)| (punts [0-9]+ yards? to [-+]?[0-9]+)'\n    return_regex = '(to [A-Z]* [0-9]+ for [-+]?[0-9]+ yards?)|(to [0-9]+ for [-+]?[0-9]+ yards?)|(ob at [A-Z]* [-+]?[0-9]+ for [-+]?[0-9]+ yards?)|(ob at [0-9]+ for [-+]?[0-9]+ yards?)|(for [-+]?[0-9]+ yards?, TOUCHDOWN)'\n    \n    for i in range(0, len(parsed_df)):\n        punt_search = re.search(punt_regex, parsed_df.PlayDescription[i])\n        return_search = re.search(return_regex, parsed_df.PlayDescription[i])\n    \n        if punt_search:\n            punt_snip = re.findall(r'-?\\d+', punt_search.group(0))\n            if parsed_df.outcome[i] in ['downed','fair_catch', 'oob', 'return', 'muff']:\n                punt_to_lst.append(int(punt_snip[-1]))\n                punt_dist_lst.append(int(punt_snip[0]))\n            else:\n                if parsed_df.outcome[i] == 'touchback':\n                    punt_to_lst.append(0)\n                    punt_dist_lst.append(int(punt_snip[0]))\n                else:\n                    print('Missing Punt Outcome at Row {}'.format(i))\n        else:\n            if parsed_df.outcome[i] == 'touchback':\n                punt_to_lst.append(0)\n                punt_dist_lst.append(int(punt_snip[0]))\n            else:\n                print('Missing Punt Outcome at Row {}'.format(i))\n        \n        if return_search:\n            return_snip = re.findall(r'-?\\d+', return_search.group(0))\n            return_dist_lst.append(int(return_snip[-1]))\n        else:\n            if parsed_df.outcome[i] == 'touchback':\n                return_dist_lst.append(20)\n            elif parsed_df.outcome[i] in ['downed','fair_catch', 'oob', 'muff']:\n                return_dist_lst.append(0)\n            elif 'no gain' in parsed_df.PlayDescription[i]:\n                return_dist_lst.append(0)\n            else:\n                print('Missing Return Outcome at Row {}'.format(i))   \n                \n    parsed_df['punt_to'] = punt_to_lst\n    parsed_df['punt_dist'] = punt_dist_lst\n    parsed_df['return_dist'] = return_dist_lst\n                                   \n    return parsed_df\n\npdd = parse_play_description(play_info, ['touchback', 'fair_catch','oob', 'downed', 'return', 'muff'])\n\n#Add in hang_time feature from punt_df\npdd = pdd.merge(punt_df[['Season_Year', 'GameKey', 'PlayID', 'hang_time']], \n                how ='left', \n                on=['Season_Year', 'GameKey', 'PlayID'])","execution_count":null,"outputs":[]},{"metadata":{"trusted":true,"_uuid":"3f24d2e2889c948e1ca58ea2bfbcebf783f48bd3","_kg_hide-input":true},"cell_type":"code","source":"trace1 = go.Histogram(\n        x=pdd.punt_dist\n    )\n\ntrace2 = go.Histogram(\n        x=pdd.loc[pdd.punt_to >= 0].punt_to\n    )\n\ntrace3 = go.Histogram(\n        x=pdd.hang_time\n    )\nfig = tools.make_subplots(rows=1, cols=3, subplot_titles=('Punt Distance', 'Punt To', 'Hang Time'))\n\nfig.append_trace(trace1, 1, 1)\nfig.append_trace(trace2, 1, 2)\nfig.append_trace(trace3, 1, 3)\n\nfig['layout'].update(showlegend=False)\nfig['layout'].update(xaxis=dict(title='Yards'))\nfig['layout'].update(xaxis2=dict(title='Yard Line'))\nfig['layout'].update(xaxis3=dict(title='Seconds'))\n\niplot(fig, filename='punt_hist')","execution_count":null,"outputs":[]},{"metadata":{"trusted":true,"_uuid":"c872dc37544b038e08d1342238387af9599ee420","_kg_hide-input":true,"scrolled":true},"cell_type":"code","source":"#Print a few summary statistics\npunt_to_mean = round(np.mean(pdd.punt_to), 1)\npunt_dist_mean = round(np.mean(pdd.punt_dist), 1)\nhang_time_mean = round(np.mean(pdd.hang_time), 1)\n\nprint('The mean yard line the ball is punted to is {}'.format(punt_to_mean))\nprint('The mean distance of a punt is {} yards'.format(punt_dist_mean))\nprint('The mean hang_time is {} seconds'.format(hang_time_mean))\n\nprint('Touchbacks occur on ' + str(round(len(pdd.loc[pdd.outcome=='touchback']) \\\n                                        / len(pdd) * 100, 1)) + '% of plays')","execution_count":null,"outputs":[]},{"metadata":{"_uuid":"cb161957216d48209edbd839ade905e734bfe52c"},"cell_type":"markdown","source":"Visualize play counts by outcome with a pie graph"},{"metadata":{"trusted":true,"_uuid":"112132fee4cb4ad54dffdad01c1bd688e3112ce1"},"cell_type":"code","source":"labels = pdd.groupby(['outcome'], \\\n                   as_index=False)['PlayID'].agg({\"play_count\": 'count'})['outcome']\n\nvalues = pdd.groupby(['outcome'], \\\n                   as_index=False)['PlayID'].agg({\"play_count\": 'count'})['play_count']\n\ntrace = go.Pie(labels=labels, values=values)\n\niplot([trace], filename='outcome_pie')","execution_count":null,"outputs":[]},{"metadata":{"_uuid":"5034304a87a863f9868496ff783a721bb2510395"},"cell_type":"markdown","source":"#### Quick Conclusions: \n* Punts are kicked to yard line 0 (touchbacks) with the most frequency by far\n* However, touchbacks only make up 6.3% of standard punt plays\n* Punt distance is approximately normally distributed around a mean of 45 yds\n\n\n### How do Return Rates change by Punt Metrics?\n\nExplore:\n* Categorize the punt description data into bins\n* How do these bins correlate with return/injury rates?\n\nBinning the punt data will make it easier to construct conclusions about clusters of the data rather than looking at it as a whole.  I'll then visualize the return rates by each of these bins."},{"metadata":{"trusted":true,"_uuid":"33b8cfb3814aebe0631d539e50cf259182b7a243","_kg_hide-input":true},"cell_type":"code","source":"punt_dist_bins = [0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55, 60, 65, 70, 75, 80]\npunt_to_bins = [0, 10, 20, 30, 40, 50]\nhang_time_bins = [3, 3.5, 4, 4.5, 5, 5.5]\n\ndef bin_punt_data(pdd, col, bin_lst):\n    bin_col = col + '_bin'\n    pdd[bin_col] = pd.cut(pdd[col], bin_lst)\n    bin_count = pdd \\\n        .groupby(bin_col, as_index=False)['PlayID'] \\\n        .agg({'play_count': 'count'})\n        \n    bin_returns = pdd.loc[pdd.outcome == 'return'] \\\n        .groupby(bin_col, as_index=False)['PlayID'] \\\n        .agg({'return_count':'count'})\n        \n    merged_bins = bin_count.merge(bin_returns)\n    merged_bins['return_rate'] = (merged_bins['return_count'] / merged_bins['play_count']).round(2)\n    merged_bins[bin_col] = merged_bins[bin_col].astype(str)\n    \n    return merged_bins\n\npunt_dist_bin = bin_punt_data(pdd, 'punt_dist', punt_dist_bins)\npunt_to_bin = bin_punt_data(pdd, 'punt_to', punt_to_bins)\nhang_time_bin = bin_punt_data(pdd, 'hang_time', hang_time_bins)","execution_count":null,"outputs":[]},{"metadata":{"trusted":true,"_uuid":"0d50dad77c7c0e36a75d722a962747b95bca0631","_kg_hide-input":true},"cell_type":"code","source":"trace1 = go.Bar(\n        x=punt_dist_bin.loc[punt_dist_bin.return_count > 50]['punt_dist_bin'],\n        y=punt_dist_bin.loc[punt_dist_bin.return_count > 50]['return_rate']\n    )\n\ntrace2 = go.Bar(\n        x=punt_to_bin['punt_to_bin'],\n        y=punt_to_bin['return_rate']\n    )\n\ntrace3 = go.Bar(\n        x=hang_time_bin['hang_time_bin'],\n        y=hang_time_bin['return_rate']\n    )\n\nfig = tools.make_subplots(rows=1, cols=3, subplot_titles=('Punt Distance', 'Punt To', 'Hang Time'), shared_yaxes=True)\n\nfig.append_trace(trace1, 1, 1)\nfig.append_trace(trace2, 1, 2)\nfig.append_trace(trace3, 1, 3)\n\nfig['layout'].update(showlegend=False)\nfig['layout'].update(yaxis=dict(title='Return Rate', range=[0,1], tickformat=',.0%'))\nfig['layout'].update(xaxis=dict(title='Yards'))\nfig['layout'].update(xaxis2=dict(title='Yard Line'))\nfig['layout'].update(xaxis3=dict(title='Seconds'))\n\niplot(fig, filename='punt_bars')","execution_count":null,"outputs":[]},{"metadata":{"_uuid":"61ae19aac2fce4c53d196277efb6244b2ccdd951"},"cell_type":"markdown","source":"Compare the return rates for punts 45 yards or less"},{"metadata":{"_kg_hide-input":true,"_kg_hide-output":false,"trusted":true,"_uuid":"73bfc52965f7c4a0c756403b4f5ac24c787f0fb0"},"cell_type":"code","source":"dist_under45 = punt_dist_bin.loc[punt_dist_bin.punt_dist_bin.isin(['(0, 5]',\n                                                    '(5, 10]',\n                                                    '(10, 15]',\n                                                    '(15, 20]',\n                                                    '(20, 25]',\n                                                    '(25, 30]',\n                                                    '(30, 35]',\n                                                    '(35, 40]',\n                                                    '(40, 45]'\n                                                   ])]\ndist_over45 = punt_dist_bin.loc[punt_dist_bin.punt_dist_bin.isin(['(0, 5]',\n                                                    '(5, 10]',\n                                                    '(10, 15]',\n                                                    '(15, 20]',\n                                                    '(20, 25]',\n                                                    '(25, 30]',\n                                                    '(30, 35]',\n                                                    '(35, 40]',\n                                                    '(40, 45]'\n                                                   ])==False]\nprint('Return Rate for punts under 45 yards is {}'\\\n          .format(str(round(dist_under45.return_count.sum() / dist_under45.play_count.sum() * 100, 1))) +'%')\n\nprint('Return Rate for punts over 45 yards is {}'\\\n          .format(str(round(dist_over45.return_count.sum() / dist_over45.play_count.sum() * 100, 1))) +'%')","execution_count":null,"outputs":[]},{"metadata":{"_uuid":"8c1d04471ca781b89323b4b025deb488c8bc5267"},"cell_type":"markdown","source":"Compare the return rates for hang times 4.5 seconds or less"},{"metadata":{"_kg_hide-input":true,"trusted":true,"_uuid":"6e693dee9a29d660cb06d16aa4ffba502f0b88c0"},"cell_type":"code","source":"hang_under45 = hang_time_bin.loc[hang_time_bin.hang_time_bin.isin(['(3.0, 3.5]',\n                                                                   '(3.5, 4.0]',\n                                                                   '(4.0, 4.5]',\n                                                                  ])]\nhang_over45 = hang_time_bin.loc[hang_time_bin.hang_time_bin.isin(['(3.0, 3.5]',\n                                                                   '(3.5, 4.0]',\n                                                                   '(4.0, 4.5]'\n                                                                  ])==False]\n\nprint('Return Rate for punts under 4.5 sec hang time is {}'\\\n          .format(str(round(hang_under45.return_count.sum() / hang_under45.play_count.sum() * 100, 1))) +'%')\n\nprint('Return Rate for punts over 4.5 sec hang time is {}'\\\n          .format(str(round(hang_over45.return_count.sum() / hang_over45.play_count.sum() * 100, 1))) +'%')","execution_count":null,"outputs":[]},{"metadata":{"_uuid":"c9357a47e0dfcadb95fbfcab70799295b8be378d"},"cell_type":"markdown","source":"Create a violin plot to visualize return distance by hang time bin"},{"metadata":{"trusted":true,"scrolled":false,"_uuid":"e4345685b0305331dfded5239d87e37c0d2817e8","_kg_hide-input":true},"cell_type":"code","source":"pdd_ret = pdd.loc[(pdd.return_dist <=100) & (pdd.outcome == 'return') & (pd.notnull(pdd.hang_time_bin))]\npdd_ret['hang_time_bin'] = pdd_ret['hang_time_bin'].astype(str)\n\ndata = []\nfor i in range(0,len(pd.unique(pdd_ret['hang_time_bin']))):\n    trace = {\n            \"type\": 'violin',\n            \"x\": pdd_ret['hang_time_bin'][pdd_ret['hang_time_bin']\\\n                                               == pd.unique(pdd_ret['hang_time_bin'])[i]],\n            \"y\": pdd_ret['return_dist'][pdd_ret['hang_time_bin']\\\n                                               == pd.unique(pdd_ret['hang_time_bin'])[i]],\n            \"name\": pd.unique(pdd_ret['hang_time_bin'])[i],\n            \"box\": {\n                \"visible\": True\n            },\n            \"meanline\": {\n                \"visible\": True\n            }\n        }\n    data.append(trace)\n\n        \nfig = {\n    \"data\": data,\n    \"layout\" : {\n        \"title\": \"\",\n        \"yaxis\": {\n            \"zeroline\": False,\n        }\n    }\n}\n\n\niplot(fig, filename='violin-hang-time')","execution_count":null,"outputs":[]},{"metadata":{"_uuid":"11b68f9f27ed1bb53fc88ca1ae3b052147393469"},"cell_type":"markdown","source":"#### Quick Conclusions: \n* Return rates are lowest on punts with short distance and high hang time\n* Return rates are 59% lower when the punt distance is under 45 yards compared to over 45 yards\n* Return rates are 11% lower when the hang time is above 4.5 seconds compared to below 4.5 seconds \n    \nExplore:\n* On punts that may have been touchbacks (inside the 10-yard line) but ended up getting returned:\n    * How far does the returner get?\n    * What % of returns get at least to the 20-yard line?\n    * What % of returns get at least to the 25-yard line?\n    \nI segment data for punts inside the 10-yard line that are returned and then add a column for the yard line the return is taken to.  If punt returners are not getting passed the 20-yard line then they should not be returning the ball.  An additional 5 yards would make that threshold even more difficult for a returner to reach."},{"metadata":{"trusted":true,"_uuid":"9b4ef13e914d72f113bdbda0b90acfa2e00f9cf3","_kg_hide-input":true},"cell_type":"code","source":"inside_ten_ret = pdd.loc[(pdd.punt_to < 10) & (pdd.punt_to > 0) & (pdd.outcome=='return')]\n\ninside_ten_ret[['punt_to', 'return_dist']] = inside_ten_ret[['punt_to', 'return_dist']].astype(int)\ninside_ten_ret['return_to'] = (inside_ten_ret['punt_to'] + inside_ten_ret['return_dist'])\n\nprint('The median yard line for a return on a punt inside the 10 yard line is the {}-yard line'\\\n      .format(int(inside_ten_ret.return_to.median())))\n\nprint('The % of returns starting from inside the 10 yard line that get at least to the 20-yard line is {}%'\\\n    .format(round(len(inside_ten_ret.loc[inside_ten_ret.return_to >= 20]) / len(inside_ten_ret) * 100, 1)))\n\nprint('The % of returns starting from inside the 10 yard line that get at least to the 25-yard line is {}%'\\\n    .format(round(len(inside_ten_ret.loc[inside_ten_ret.return_to >= 25]) / len(inside_ten_ret) * 100, 1)))","execution_count":null,"outputs":[]},{"metadata":{"_uuid":"a9f53ecc1de16b9e10fed3a911a30d24de73d31a"},"cell_type":"markdown","source":"#### Quick Conclusions: \n* The returner gets to or beyond the current touchback location less than 1/3 of the time, so it is in their best interest to let the ball bounce into the end zone\n* That success rate is cut in half when extending the touchback location to the 25-yard line\n\nExplore:\n* Injury rates on each of the bins\n    \n#### Punt Distance Bin Injury Rate"},{"metadata":{"trusted":true,"_uuid":"e6b541ac362d8d27a325f946c69893506d4fd15d","_kg_hide-input":true},"cell_type":"code","source":"pdd = pdd.merge(video_review[['Season_Year', 'GameKey', 'PlayID', 'GSISID']], \n                how ='left', \n                on = ['Season_Year', 'GameKey', 'PlayID'])\npdd.rename(columns={'GSISID':'injury'}, inplace=True)\npdd['injury'] = np.where(pd.notnull(pdd['injury']), 1, 0).astype(int)\n\npunt_dist_grouped = pdd.groupby(['punt_dist_bin'], as_index=False)['injury']\\\n    .agg({'play_count':'count','injury':sum})\n    \npunt_dist_grouped['injury_rate'] = (punt_dist_grouped['injury'] \\\n                                    / punt_dist_grouped['play_count'] * 100).round(1).astype(str) +'%'\n\npunt_dist_grouped.loc[punt_dist_grouped.play_count > 150].reset_index(drop=True)[['punt_dist_bin', 'injury_rate']]","execution_count":null,"outputs":[]},{"metadata":{"_uuid":"641d0361de7f97d233ab39c329707d68aeebd303"},"cell_type":"markdown","source":"#### Punt To Bin Injury Rate"},{"metadata":{"trusted":true,"_uuid":"2cde8dce10f124573568d4fe5ee9dbdee074a06f","_kg_hide-input":true},"cell_type":"code","source":"punt_to_grouped = pdd.groupby(['punt_to_bin'], as_index=False)['injury']\\\n    .agg({'play_count':'count','injury':sum})\npunt_to_grouped['injury_rate'] = (punt_to_grouped['injury'] \\\n                                  / punt_to_grouped['play_count'] * 100).round(1).astype(str) +'%'\n\npunt_to_grouped[['punt_to_bin', 'injury_rate']]","execution_count":null,"outputs":[]},{"metadata":{"_uuid":"d950a423849a5e492349628626f8899d029fb7ec"},"cell_type":"markdown","source":"#### Hang Time Bin Injury Rate"},{"metadata":{"trusted":true,"_uuid":"76cf75cddd2459e829437d638d94b971ef54d8cb","_kg_hide-input":true},"cell_type":"code","source":"hang_time_grouped = pdd.groupby(['hang_time_bin'], as_index=False)['injury']\\\n    .agg({'play_count':'count','injury':sum})\nhang_time_grouped['injury_rate'] = (hang_time_grouped['injury'] \\\n                                  / hang_time_grouped['play_count'] * 100).round(1).astype(str) +'%'\n\nhang_time_grouped[['hang_time_bin', 'injury_rate']]","execution_count":null,"outputs":[]},{"metadata":{"_uuid":"0eabfc53f55664821a567547933c5d6d38e2200f"},"cell_type":"markdown","source":"#### Quick Conclusions: \n* Not surprisingly, injury rates are highest on long punts with low hang times\n    * Higher likelihood of a return on these plays\n\nExplore:\n* Correlation between:\n    * punt_to\n    * punt_dist\n    * return_dist\n    * hang_time\n* Fitting a logistic regression\n\n#### Correlation Heat Map\n\nI looke at the correlation between the 4 features listed above in a visual heat map"},{"metadata":{"trusted":true,"_uuid":"f4f2ad4ee9fab08c4ed2984248ed8a58ece02fce","_kg_hide-input":true},"cell_type":"code","source":"#Subset the columns to be used in the correlation table\ncorr_df = pdd_ret[['punt_to', 'punt_dist', 'return_dist', 'hang_time']]\ncorr_df.corr().round(2)","execution_count":null,"outputs":[]},{"metadata":{"trusted":true,"_uuid":"ff8d04049bb0a35b68a6542497d94162622c0606","_kg_hide-input":true},"cell_type":"code","source":"#Create correlation heatmap\nplt.rcParams[\"figure.figsize\"] = (10,10)\ncmap = ListedColormap(['#000431', '#01065A', '#021CA4', '#0275F4','#3393FF', '#F5FAFF'])\n\nplt.matshow(corr_df.corr(), cmap=cmap)\nplt.xticks(range(len(corr_df.columns)), corr_df.columns)\nplt.yticks(range(len(corr_df.columns)), corr_df.columns)\nplt.colorbar()\nplt.show()","execution_count":null,"outputs":[]},{"metadata":{"_uuid":"525cb99f45eb67babfda42cb71e1f0738dfb64d5"},"cell_type":"markdown","source":"#### Quick Conclusions: \n*  The correlation matrix shows that return distance has a slight negative correlation with hang time and a slight positive correlation with punt distance\n*  Therefore if a punter is trying to limit return yards it would be in their best interest to kick the ball high and short\n\n#### Logistic Regression\n\nI fit a logistic regression to find the marginal effects of punt to, punt distance, and hang time on returns"},{"metadata":{"trusted":true,"_uuid":"0938533e8b5b94269035b11e117e1e2d791a689a"},"cell_type":"code","source":"import statsmodels.api as sm\n\nlog_df = pdd.loc[(pd.notnull(pdd.punt_to)) & (pd.notnull(pdd.hang_time))][['punt_dist','hang_time','punt_to','outcome']]\nlog_df['return_bool'] = np.where(log_df.outcome=='return', 1, 0)\n\nlogit = sm.Logit(log_df['return_bool'], log_df[['punt_dist','hang_time','punt_to']])\nresult = logit.fit()\nresult.summary()","execution_count":null,"outputs":[]},{"metadata":{"trusted":true,"_uuid":"d8fb0ce0f046a8dbf889780ebba570baae07fb0e"},"cell_type":"code","source":"params = result.params\nconf = result.conf_int()\nconf['OR'] = params\nconf.columns = ['2.5%', '97.5%', 'OR']\nnp.exp(conf).round(2)","execution_count":null,"outputs":[]},{"metadata":{"_uuid":"739d4beb844a026c6b6c941d1455bdf596652025"},"cell_type":"markdown","source":"#### Quick Conclusions: \n* The logistic regression results demonstrate that short punts and high hang times can significatly decrease the chance of returning a punt\n    * For a 1 unit increase in punt distance, the odds of a return increase by 17%\n    * For a 1 unit increase in hang time, the odds of a return decrease by 20%\n\n## Proposal #2\nIf the result of the punt play is a touchback:\n\nThe dead-ball spot for the following snap will be from the 25-yard line\n\n#### Intuitive Reasoning:\n* Fearing the extra 5 yard penalty, punters will be even more wary of avoiding a touchback.  They will punt the ball shorter and with more hang time\n\n* From a returners perspective, if the ball is heading close to the goaline, they will be more likely to let it bounce \nand take the touchback than return it because of the extra 5 yard bonus\n\n* Both of these motivations working simultaneously will lead to less returns and thus less injuries\n\n#### Unintended Consquences\n* I don’t see a downside to this rule change as it specifically relates to injuries on punts\n* However, it could have an impact on other aspects of the game.  For instance, it clearly helps offenses by giving them an extra 5-yards\n* As Coaches may decide to go for it more on 4th down, knowing that possible touchback would bring the ball out to the 25-yard line\n    * This would actually be a benefit as less punts means less injuries on punts\n* The graph below shows that there was already a huge up-tick in 4th down attempt rates in 2018"},{"metadata":{"trusted":true,"_uuid":"b806c24d76068c4543ee02627f34110f30f2b2b6","scrolled":true,"_kg_hide-input":true},"cell_type":"code","source":"#*Source Pro Football Reference\nyears = list(range(2000, 2019, 1))\nattempt_rate = [.121, .123, .131, .128, .121, .120, .123, .142, .132, .145, \n                .126, .112, .117, .12, .12, .126, .128, .125, .149]\n\ntrace0 = go.Scatter(\n    x=years,\n    y=attempt_rate,\n    line=dict(color='green')\n)\n\nlayout = dict(title = '4th Down Attempt Rate',\n              xaxis = dict(title = 'Year'),\n              yaxis = dict(title = 'Attempt Rate', tickformat=',.1%'),\n              hovermode = 'closest')\n\nfig = dict(data=[trace0], layout=layout)\n             \niplot(fig, filename='attempt_rates')","execution_count":null,"outputs":[]},{"metadata":{"_uuid":"5148542cd02903c4a50539b6226445f5dc284271"},"cell_type":"markdown","source":"### Conclusions & Proposals\nIn summary, my two proposed rule changes (and evidence) are:\n1. **Gunner Blockers** - When Team A presents a punt formation, Team B may have at most 1 blocker aligned opposite each of Team A’s end men on the line of scrimmage (gunners) at the snap of the ball. \n    - Return alignments with only 2 gunner blockers allow for better coverage from the gunners\n    - Better coverage leads to an increase in fair catch signals and less returns\n     - Less returns will decrease injuries and thus make make punts safer\n\n2. **Touchbacks** - If the result of the punt play is a touchback,  the dead-ball spot for the following snap will be from the 25-yard line.\n    - Moving touchbacks on punts to the 25-yard line will incentivize:\n     - Returners to let the football bounce into the end zone\n     - Kickers to keep their punts high and short\n    - Both of these motivations will lead to less returns and less injuries\n\n### Closing Thoughts\nI really enjoyed working on this project and participating in the NFL Punt Analytics Competition.  As a huge fan of the NFL, the project has definitely given me a new perspective while watching games, specifically punt plays.  \n\nI explored a few other ideas for rule changes such as adding a bonus amount of yards for a fair catch, or allowing all players on the punt team to move passed 1 yard from line of scrimmage as soon as the ball is snapped.  But I ultimately decided that these rules would be a drastic shift in the way the game is played and that there are significant unkowns that could prove to be detrimental.   \n\nI believe both of my proposed rule changes are moderate and have little downside, but will be able to make a significant impact in making the NFL a safer league.\n\nThanks any and all for reading this far.  I look forward to feedback."},{"metadata":{"trusted":true,"_uuid":"2d9479f5ac02c4fc4f3b15aecdc910d567a6581b"},"cell_type":"code","source":"","execution_count":null,"outputs":[]}],"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":4,"nbformat_minor":4}