{"cells":[{"metadata":{"_uuid":"fafefa4ac79c15d3ac59989ec053273577d4ec48"},"cell_type":"markdown","source":"# How to increase the probability of fair catch?\n\nFair catch eliminates tackling and reduces the need of blocking, which could reduce the likelihood of concussion.  To force a punt returner to fair catch, the gunners have to arrive where the punt returner is before he catches the ball.  Therefore, if there were fewer jammers, the gunners should be able to get to the punt returner faster. To test the idea, the null hypothesis is the number of jammers has no effect on the percentage of fair catch plays.\n\nHere is the summary of steps to test the hypothesis. First, I determined how each play ended from NGS events.  Second, the number of jammers and gunners were computed from the positions of players for each punt plays.  Lastly, I bootstrapped the trials 100 times to compute the probability of fair catch plays given 2, 3, and 4 jammers, and performed two-sample Kolmogorov-Smirnov Tests to see whether the probabilities were significantly different. They were indeed different and the null hypothesis was rejected.  This result suggested that the fewer the jammers, the higher the likelihood of a fair catch play.  Because fair catch plays reduce the need for collision, the likelihood of a concussion is also decreased."},{"metadata":{"_uuid":"76e6002a02627a33854a756b028179cbe4e4e654"},"cell_type":"markdown","source":"## Step 1. Determine how a play ended by NGS events \n\nBased on the NGS events, a punt play could end in the following ways: 'tackle', 'touchdown', 'touchback', 'fair_catch', 'punt_downed', 'out_of_bounds', 'out_of_bounds_direct' (punter punted the ball directly out of bounds),\n    and 'no_play' (e.g., false start, delay of game)\n\nThe first cell includes libraries and functions to be used later. "},{"metadata":{"trusted":true,"_uuid":"786ddff04b2e9b5e262555dedafa1dc3c5778ec0"},"cell_type":"code","source":"from os import listdir\nfrom scipy.stats import ks_2samp\nfrom pathlib import Path\nimport numpy as np\nimport pandas as pd\nimport matplotlib.pyplot as plt\nfrom matplotlib import rc as pltrc\n\n'''\nget_end_event check NGS data to see how the play ends.\n- parameters:\n    - thisplay, NGS data of this play for all the players\n- return:\n    a string that could be 'tackle', 'touchdown', 'touchback', 'fair_catch', 'punt_downed', \n    'out_of_bounds', 'out_of_bounds_direct' (punter punted the ball directly out of bounds),\n    and 'no_play' (e.g., false start, delay of game)\n'''\ndef get_end_event(thisplay): \n    end_evts = ['tackle', 'touchdown', 'fair_catch', 'punt_downed', 'out_of_bounds', 'touchback']\n    evts = thisplay.Event.unique()\n    \n    match = set(evts).intersection(end_evts)\n    if len(match) == 0:\n        return 'no_play'\n    else:\n        match = match.pop()\n\n        if match == 'out_of_bounds':\n            if any(evts == 'punt_received'):\n                return match\n            else:\n                return 'out_of_bounds_direct'\n        else:\n            return match\n    \n'''\ncheck the play description if it is a no_play\n'''    \ndef print_no_play_description(playsum, gamekey, playid):    \n    \n    # show play description\n    play_cond = (\n        (playsum.GameKey==gamekey) &\n        (playsum.PlayID==playid))\n    pd.set_option('display.max_colwidth', -1)\n    display(playsum.loc[play_cond, ['PlayDescription']])\n","execution_count":null,"outputs":[]},{"metadata":{"_uuid":"5e7fa56cd39995b61a399e7dcefb16202f035cb0"},"cell_type":"markdown","source":"Check whether the prepared data frame exists. If it does, then load it and skip data preparation steps."},{"metadata":{"trusted":true,"_uuid":"78a69e09bfae7114082321d17e8b0b7248379b81"},"cell_type":"code","source":"derived_summary_file = Path('../working/data_fair_catch.csv')\nif derived_summary_file.is_file():\n    playsum = pd.read_csv(derived_summary_file)\n    prepare_data = False\nelse:\n    prepare_data = True","execution_count":null,"outputs":[]},{"metadata":{"_uuid":"6be716a6b270e1c1eedb05ba68e6589358f94e3d"},"cell_type":"markdown","source":"Add three columns to the play_information table: 'End_Event' is the event of how a play ended; 'NumV' is the number of jammers; 'NumG' is the number of gunners. "},{"metadata":{"trusted":true,"_uuid":"53d9bcd14bff39dfbc389004fc0fba6fbc9fc872"},"cell_type":"code","source":"datapath = '../input/'\nif prepare_data:\n    playsum = pd.read_csv(datapath + 'play_information.csv') # play summary table\n    playsum[\"End_Event\"] = \"\" # how this play ends\n    playsum[\"NumV\"] = \"\"      # number of jammers (V)\n    playsum[\"NumG\"] = \"\"      # number of gunners (G)","execution_count":null,"outputs":[]},{"metadata":{"_uuid":"f29cb7e452db3975ab3b681e390931cbbd748edd"},"cell_type":"markdown","source":"Looping through all the NGS files and plays to extract the event of how a play ended"},{"metadata":{"trusted":true,"_uuid":"efc0bd7eff7584d8abdc5426bc46a26324b388db"},"cell_type":"code","source":"if prepare_data:\n    # loop through all the NGS files\n    ngsfiles = [filename for filename in listdir(datapath) if filename.startswith(\"NGS\")]\n    for playfile in ngsfiles:\n        print(playfile)\n        ngsplays = pd.read_csv(datapath + playfile, low_memory=False)\n\n        # get a concise list of the plays in this ngs file\n        playf = ngsplays.drop_duplicates(subset=['GameKey','PlayID'], keep='first').copy()\n        playf.reset_index(drop=True, inplace=True)\n\n        # loop through all the plays\n        for play_ind in range(len(playf)):\n            # check ngsplays to see how this play ends\n            play_cond = (\n                (ngsplays.GameKey==playf.loc[play_ind,'GameKey']) &\n                (ngsplays.PlayID==playf.loc[play_ind,'PlayID']))\n\n            # get the end event\n            thisplay = ngsplays[play_cond]\n            endevt = get_end_event(thisplay)\n\n            # if you'd like to print out play descriptions of no_play, uncomment below\n#             if endevt == 'no_play':\n#                 print_no_play_description(playsum, \n#                                           playf.loc[play_ind,'GameKey'], \n#                                           playf.loc[play_ind,'PlayID'])\n\n            # update play summary\n            play_cond = (\n                (playsum.GameKey==playf.loc[play_ind,'GameKey']) &\n                (playsum.PlayID==playf.loc[play_ind,'PlayID']))\n            playsum.loc[play_cond, 'End_Event'] = endevt\n\n        ngsplays = None\n        playf = None","execution_count":null,"outputs":[]},{"metadata":{"_uuid":"827df20b6da2282adce36b23ae40bf5722ba847d"},"cell_type":"markdown","source":"## Step 2. Computes the number of jammers (V) and gunners (G) from play_player_role_data.csv"},{"metadata":{"trusted":true,"_uuid":"26fbf788d46d7e644497b7e9f0071142bfc3a36c"},"cell_type":"code","source":"if prepare_data:\n    players = pd.read_csv(datapath + 'play_player_role_data.csv')\n\n    for ind in range(playsum.shape[0]):\n        gamekey = playsum.loc[ind, 'GameKey']\n        playid = playsum.loc[ind, 'PlayID']\n\n        thisplay = players[(players.GameKey==gamekey)&(players.PlayID==playid)]\n        v = set(thisplay['Role']).intersection(['VLi','VLo','VRi','VRo','VR','VL'])\n        g = set(thisplay['Role']).intersection(['GLi','GLo','GRi','GRo','GR','GL'])\n\n        playsum.loc[ind,'NumV'] = len(v)\n        playsum.loc[ind,'NumG'] = len(g)","execution_count":null,"outputs":[]},{"metadata":{"_uuid":"1e9e1a9876318e229c7a8337d7eca339430bee08"},"cell_type":"markdown","source":"There are some plays that don't have NGS data. So, how those plays ended were unknown.  Those plays are removed here."},{"metadata":{"trusted":true,"_uuid":"17885d5272042ce3aca461f10b6930bd4a807961"},"cell_type":"code","source":"if prepare_data:\n    playsum = playsum.drop(playsum.index[(playsum.End_Event=='')])   \n    playsum.reset_index(drop=True, inplace=True)\n\n    # save the data frame\n    playsum.to_csv(derived_summary_file)\n\n# print out how plays end\nvc = playsum.End_Event.value_counts()\nprint(vc)","execution_count":null,"outputs":[]},{"metadata":{"_uuid":"9bbcbad48d87553db14b0a34c517b9ae5960cfa8"},"cell_type":"markdown","source":"[[OPTIONAL]] Three end events (no_play, out_of_bounds_direct, and touchback) could be excluded from the analysis because changing the number of V and G won't have an effect on these plays.  Uncomments the code if you want to exclude these plays."},{"metadata":{"trusted":true,"_uuid":"3cacba7d1141d4e8108bce87cae5026b2b17360a"},"cell_type":"code","source":"# playsum = playsum.drop(playsum.index[(playsum.End_Event=='no_play')])   \n# playsum = playsum.drop(playsum.index[(playsum.End_Event=='out_of_bounds_direct')])   \n# playsum = playsum.drop(playsum.index[(playsum.End_Event=='touchback')])   \n# playsum.reset_index(drop=True, inplace=True)","execution_count":null,"outputs":[]},{"metadata":{"_uuid":"38b9fe7eccd6ed51ed26d8ef13f2c0aa3a8e80a0"},"cell_type":"markdown","source":"Print out the percentage of how each play ended"},{"metadata":{"trusted":true,"_uuid":"a57c3b099aef6712cc3a5809840c30a99b83ee4a"},"cell_type":"code","source":"vc = playsum.End_Event.value_counts()\nprint('percentage of plays')\nprint(vc/sum(vc))\n\nprob_faircatch = vc['fair_catch']/sum(vc)","execution_count":null,"outputs":[]},{"metadata":{"_uuid":"e5e7410bc3959138105ea39a5b06069be1c5667e"},"cell_type":"markdown","source":"Overall 24.6% of the plays were fair catches.  First, we would like to see whether the number of gunners (G) has an effect on the percentage of fair catch plays. However, below shows that almost all the punt plays had only 2 gunners (99.1%), which makes other cases neglectable.  "},{"metadata":{"trusted":true,"_uuid":"7062ce39a136b454008d2ce3f9a046f82e0b7e81"},"cell_type":"code","source":"vc = playsum.NumG.value_counts()\nvc/sum(vc)","execution_count":null,"outputs":[]},{"metadata":{"_uuid":"3d6bf91f3b8f65804957567241406b25e5507fb2"},"cell_type":"markdown","source":"## Step 3. Test whether the number of jammers (V) has an effect on the percentage of fair catch plays\n\nPrint out the percentage of fair catch plays given 2, 3, or 4 jammers"},{"metadata":{"trusted":true,"_uuid":"7ee1f57c76d20fe8960f33a6db7bbc1ec1b6e677"},"cell_type":"code","source":"nvs = [2,3,4]\nfc = []\nfor nv in nvs:\n    pp = playsum.loc[(playsum.NumV==nv) & (playsum.NumG==2)]\n    vc = pp.End_Event.value_counts()\n    fc.append(vc['fair_catch']/sum(vc))    \n    print('{} jammers, {:.2f}% ({}) plays were fair catch. 2 jammers were {:.2f} times more'.format(nv, \n                                                                      100*fc[-1], \n                                                                      vc['fair_catch'],                                                                      \n                                                                      fc[0]/fc[-1]))\n    ","execution_count":null,"outputs":[]},{"metadata":{"_uuid":"43e4011da1933d28083b3754409e8ba7e26c550e"},"cell_type":"markdown","source":"Bootstrap plays for 100 times to compute the probability of fair catch given a different number of jammers"},{"metadata":{"trusted":true,"_uuid":"b7b06936e90300c3e2c9207a2b44ec4c552d52d3"},"cell_type":"code","source":"n = playsum.shape[0]\nnrun = 100\n\nfair_prob = {'2':[], '3':[], '4':[]}\nfor i in range(nrun):\n    pboot_ind = np.ceil(n * np.random.rand(n))\n    pboot = playsum.reindex(pboot_ind)\n\n    for nv in nvs:\n        pp = pboot.loc[(pboot.NumV==nv) & (pboot.NumG==2)]\n        vc = pp.End_Event.value_counts()\n        fair_prob[str(nv)].append(vc['fair_catch']/sum(vc))        ","execution_count":null,"outputs":[]},{"metadata":{"_uuid":"837f7a09140103a174a7efe9d95c86a4ac5ae752"},"cell_type":"markdown","source":"Perform Kolmogorov-Smirnov two-sample test with Bonferroni Correction"},{"metadata":{"trusted":true,"_uuid":"f2e946c97331b632208a8efc64ab6095a2750edf"},"cell_type":"code","source":"for nv1 in [2, 3]:\n    for nv2 in range(nv1+1, 5):\n        value, pvalue = ks_2samp(fair_prob[str(nv1)], fair_prob[str(nv2)])\n        print('{} vs {} jammers, fair catch probabilities are {:.3f} vs {:.3f}, bonferroni-corrected p-value = {:.4f}'.format(nv1,  nv2, \n                                                                             np.median(fair_prob[str(nv1)]), \n                                                                             np.median(fair_prob[str(nv2)]),                                                                              \n                                                                             3*pvalue))\n\n","execution_count":null,"outputs":[]},{"metadata":{"_uuid":"4c936323c4bea309de9ca4b561ef27c9d6a9fef2"},"cell_type":"markdown","source":"Plot error bar graph"},{"metadata":{"trusted":true,"_uuid":"b6e5913b7e37c1594e67b99367660b5bf9ebf697"},"cell_type":"code","source":"fc = []\nfnvs = np.array(nvs)\nfnvs = fnvs[::-1]\n\nfor nv in fnvs:\n    fc.append(np.percentile(fair_prob[str(nv)], [0.005, 0.5, 0.995]))\n\nfc = np.array(fc)    \n\n# set font for figures\nfont = {'weight' : 'bold',\n        'size'   : 14}\npltrc('font', **font)\n\n# plot it\nplt.bar(fnvs, fc[:,1], yerr=np.diff(fc).T, align='center', alpha=0.3, width=0.35, color='blue')\nplt.xlim([1,5])\nplt.xticks(nvs, nvs)\nplt.ylabel('percentage of fair catch plays')\nplt.xlabel('number of jammers')\nplt.show()","execution_count":null,"outputs":[]},{"metadata":{"_uuid":"448ac1ac5d392ec47f64538946568189f59e1bcd"},"cell_type":"markdown","source":"Plot the percentage of plays and the risk of injury for fair catch vs other plays."},{"metadata":{"trusted":true,"_uuid":"982d2b32387f6e42a229ead030e3e9e3e475d3d6"},"cell_type":"code","source":"review = pd.read_csv(datapath+'video_review.csv')\nvideo = pd.read_csv(datapath+'video_footage-injury.csv')\n\nfc = pd.merge(playsum, review, left_on=['PlayID','GameKey'], right_on=['PlayID','GameKey'])\nv = fc.End_Event.value_counts()\ninj_faircatch = v['fair_catch']/sum(v)\n\n# plotting\nfig, ax = plt.subplots()\nindex = np.array([1, 2])\nbar_width = 0.2\nopacity = 0.5\n\nrects1 = plt.bar(index, [prob_faircatch, inj_faircatch], bar_width,\n                alpha=opacity, color='b', label='fair_catch')\n\nrects2 = plt.bar(index+bar_width, [1-prob_faircatch, 1-inj_faircatch], bar_width,\n                alpha=opacity, color='g', label='others')\n\nplt.xticks(index+0.5*bar_width, ('all plays', 'injuried plays'))\nplt.ylabel('percentage of plays')\nplt.legend(loc=2)\nplt.xlim([0.5,2.75])","execution_count":null,"outputs":[]},{"metadata":{"_uuid":"8d49c0a63d12907373c6e92b5ad4a3e27255ac99"},"cell_type":"markdown","source":"Show the videos of injuries that occurred in fair catch plays "},{"metadata":{"trusted":true,"_uuid":"e22a84ddb1d164fc39452a0339d861ca1a835e6a"},"cell_type":"code","source":"fc = fc[(fc.End_Event=='fair_catch')]\nfc = pd.merge(fc, video, left_on=['PlayID','GameKey'], right_on=['playid','gamekey'])\n\npd.set_option('display.max_colwidth', -1)\nfc['PREVIEW LINK (5000K)']","execution_count":null,"outputs":[]},{"metadata":{"_uuid":"d4310111201e7a38512225df3efd8ab7cd00f336"},"cell_type":"markdown","source":"## Conclusion\nThis analysis suggests a way to decrease concussion for punt plays, which is to increase the probability of fair catch by allowing no more jammers than gunners.  By having two jammers against two gunners, the probability of a fair catch is 32.3%, which is **1.73 times** (32.3%/18.7%) more likely that the play ends up by a fair catch comparing to having three jammers, and **2.36 times** (32.3%/13.7%) more likely than using four jammers. In addition, note that while fair catch takes up 24.6% among all punt plays, there were only 8.1% (3 occurrences among 37 injuries) concussions that happened in a fair catch play.  From the videos (see above), two of the injuries occurred at the line of scrimmage, which could have happened in any type of plays; the other injury happened when the punt returner changed his mind to return the ball although he signaled fair catch in the first place.  If he had fair catch the ball, his injury could have been avoided.    **In summary, if the rule limits the number of jammers, it is a statistically significant way to increase the probability of fair catch, which would lead to less high-speed collisions, less concussion, and better player safety.**\n\n\n"}],"metadata":{"kernelspec":{"display_name":"Python 3","language":"python","name":"python3"},"language_info":{"name":"python","version":"3.6.6","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"}},"nbformat":4,"nbformat_minor":1}