{"cells":[{"metadata":{"_uuid":"9c83d607bcff3b20f10f0af8f7f9bf2e7ef2881b"},"cell_type":"markdown","source":"**Executive Summary**\n\nWe would like to give coaches an incentive to \"go for it\" on fourth down more often. We propose the following amendment to Section 33, Article 6:\n\n*When the offensive team fails to reach the line to gain on fourth down, the ball is moved from the previous spot half the distance to the goal line of the defensive team.*\n\nOur simulations, which you can run below, show that with this rule change, there would be between 1 and 12 fewer concussions per season and between 2 and 9 more points per game. Whenever safety and scoring improves, we believe that most NFL stakeholders are happier.\n"},{"metadata":{"_uuid":"c97ca401e0530c6f1b13e3b0647a815573010f29"},"cell_type":"markdown","source":"<img src=\"https://www.playsmartplaysafe.com/wp-content/uploads/2018/01/injury-data-slide-1.jpg\" width=\"640\"/>"},{"metadata":{"trusted":true,"_uuid":"cbd3be48e44ba7f473c185351bcf03736d6a8ff5"},"cell_type":"code","source":"import numpy as np\nimport pandas as pd\nimport random","execution_count":null,"outputs":[]},{"metadata":{"trusted":true,"_uuid":"708bad5280e9bf8085633cd7a2a27f009fa30ec5"},"cell_type":"code","source":"DATA_DIR = '../input/NFL-Punt-Analytics-Competition/'\nvideo_review_df = pd.read_csv(DATA_DIR + 'video_review.csv', dtype='unicode')\n# Load a dataset of all plays between 2016 and 2017\nDATA_DIR = '../input/nflscraprdataplay-by-play-data-20162017/'\npbp_df = pd.read_csv(DATA_DIR + 'pre_pbp_2016.csv', dtype='unicode')\npbp_df = pbp_df.append(pd.read_csv(DATA_DIR + 'reg_pbp_2016.csv', dtype='unicode'), ignore_index=True)\npbp_df = pbp_df.append(pd.read_csv(DATA_DIR + 'post_pbp_2016.csv', dtype='unicode'), ignore_index=True)\npbp_df = pbp_df.append(pd.read_csv(DATA_DIR + 'pre_pbp_2017.csv', dtype='unicode'), ignore_index=True)\npbp_df = pbp_df.append(pd.read_csv(DATA_DIR + 'reg_pbp_2017.csv', dtype='unicode'), ignore_index=True)\npbp_df = pbp_df.append(pd.read_csv(DATA_DIR + 'post_pbp_2017.csv', dtype='unicode'), ignore_index=True)\n\nprint('The following plays are distinguished in the play-by-play dataset: %s.'\\\n     % ', '.join([str(pt) for pt in pbp_df.play_type.unique()]))\n","execution_count":null,"outputs":[]},{"metadata":{"trusted":true,"_uuid":"c19b5a658d533d29b7b4721cece9548c9cc34c49"},"cell_type":"code","source":"# yardline_100 is the number of yards left till the end zone of the defensive team\nnumeric_cols = ['down', 'ep', 'epa', 'qtr', 'third_down_converted', 'yardline_100', 'yards_gained', 'ydstogo']\npbp_df[numeric_cols] = pbp_df[numeric_cols].apply(pd.to_numeric)","execution_count":null,"outputs":[]},{"metadata":{"trusted":true,"_uuid":"6883b55fce2df19ddf27939941973b315e386099"},"cell_type":"code","source":"concussion_count = 217 + 235\nkickoff_concussion_count = int(0.12*concussion_count)\npunt_concussion_count = len(video_review_df)\npunt_return_concussion_count = 22\n#\ngame_count = len(pbp_df.game_id.unique())\npass_run_concussion_ceiling = concussion_count - punt_concussion_count - kickoff_concussion_count\n#\npass_run_count = len(pbp_df[pbp_df.play_type == 'pass']) + len(pbp_df[pbp_df.play_type == 'run'])\npunt_return_count = len(pbp_df[(pbp_df.play_type == 'punt')\\\n                        & (pbp_df.punt_in_endzone == '0')\\\n                        & (pbp_df.punt_out_of_bounds == '0')\\\n                        & (pbp_df.punt_downed == '0')\\\n                        & (pbp_df.punt_fair_catch == '0')\\\n                        ])\n#\npunt_return_concussion_risk = punt_return_concussion_count / punt_return_count\npass_run_concussion_risk = pass_run_concussion_ceiling / pass_run_count","execution_count":null,"outputs":[]},{"metadata":{"trusted":true,"_uuid":"9ebdadcaa3d63c2d4e72f1c211c2bd89bda61165"},"cell_type":"code","source":"print('According to the table above, there was a total of 217 + 235 = %d concussions during' \n      ' all NFL games between 2016 and 2017.'\\\n      % concussion_count)\nprint('After reviewing the provided videos, we count that in the same time period, there were'\n      ' at least %d concussions on punt returns (out of %d concussions on all punts).'\\\n      % (punt_return_concussion_count, punt_concussion_count))\nprint('According to the challenge description, 12%% of concussions in the 2015-2017 seasons'\n      ' occurred on kickoffs. Assuming that the risk of kickoff concussions had remained constant'\n      ' before a new rule was implemented in 2018, we can esimate that there were %d concussions'\n      ' on kickoffs during the 2016-2017 seasons.'\\\n     % kickoff_concussion_count)\nprint('So we estimate that at most %d concussions occurred on pass/run plays.'\\\n     % pass_run_concussion_ceiling)\nprint('----------------------')\nprint('Since there were %d pass/run plays and %d punt returns during the 2016-2017 seasons,'\n      ' we calculate that players had more than %.1f times the risk of concussions on punt returns'\n      ' compared to running or passing plays.'\\\n      % (pass_run_count, punt_return_count, punt_return_concussion_risk / pass_run_concussion_risk))\nprint('Therefore, our goal is to reduce the number of punt returns by providing coaches with incentives'\n     ' to \"go for it\" on fourth down.')\nprint('----------------------')","execution_count":null,"outputs":[]},{"metadata":{"_uuid":"6415459d96c187515cced42aee5f4e8d70833d38"},"cell_type":"markdown","source":"**PROPOSED RULE CHANGE : Amend Section 33, Article 6**\n\nWhen the offensive team fails to reach the line to gain on fourth down, the ball is moved from the previous spot half the distance to the goal line of the defensive team."},{"metadata":{"trusted":true,"_uuid":"2a61c37b2b9dbc9506186974ca1ca0bbfda6eaaf"},"cell_type":"code","source":"punts_df = pbp_df[pbp_df.play_type == 'punt']\npunt_gains_by_yardline_100 = {}\nfor idx in punts_df.index:\n    yardline_100 = pbp_df.iloc[idx].yardline_100\n    try:\n        gain = int(pbp_df.iloc[idx+1].yardline_100 - (100 - yardline_100))\n    except:\n        # Ignore very few instance where we have NaNs\n        continue\n    if yardline_100 not in punt_gains_by_yardline_100:\n        punt_gains_by_yardline_100[yardline_100] = []\n    punt_gains_by_yardline_100[yardline_100].append(gain)\n    \navg_punt_gain_by_yardline_100 = {}\nfor yardline_100, gains in punt_gains_by_yardline_100.items():\n    avg_punt_gain_by_yardline_100[yardline_100] = int(sum(gains)/len(gains))\n    \ndef avg_punt_gain(yardline_100):\n    return avg_punt_gain_by_yardline_100[yardline_100]","execution_count":null,"outputs":[]},{"metadata":{"trusted":true,"_uuid":"06724772caacd259ff0cab0ff3748facc643409f"},"cell_type":"code","source":"# We believe that this rule change will increase the number of fourth down pass/run plays, therefore reducing the number of punt returns.\n# Let's estimate the impact of the rule change by running a simulation. To simplify, we will only analyze punts in the first and third quarters \n# (when clock issues can be ignored).\n# We will use Expected Points (EP) to evaluate the options available to the coach.\n# A coach can be conservative, neutral, or aggressive in his decisions.\n\n# For all statistical estimates, we will use the values from the play-by-play dataset\nstats_df = pbp_df[(pbp_df.qtr.isin([1,3])) & (pbp_df.play_type.isin(['run', 'pass']))]\nep_df = stats_df[(stats_df.down == 1) & (stats_df.ydstogo == 10)]\nep_df = ep_df[['yardline_100', 'ep']]\n# ToDo: Investigate why EP values are not all the same  under these circumstances\nep_df  = ep_df.groupby('yardline_100').mean()\n\ndef ep_1st_and_10(yardline_100):\n        return ep_df.ep[yardline_100]\n    \n# Estimate that probability that a fourth down is converted by using third down conversion data, because we have a lot more third down data than fourth down data\n\nthird_down_df = stats_df[stats_df.down == 3]\nthird_down_df = third_down_df[['third_down_converted', 'ydstogo']]\n# We use the mean as the estimate for the fourth down conversion probability\nthird_down_df  = third_down_df.groupby(['ydstogo']).mean()\n\ndef fourth_down_converted_prob(ydstogo):\n    if ydstogo < 1:\n        # Sanity check\n        return 1\n    try:\n        return third_down_df.third_down_converted[ydstogo]\n    except KeyError:\n        # No such scenario in our dataset, so try to approximate\n        return fourth_down_converted_prob(ydstogo - 1) - 0.1\n\n    ","execution_count":null,"outputs":[]},{"metadata":{"trusted":true,"_uuid":"3100d86fd07acdfe5cab13f22378c6264a177b54"},"cell_type":"code","source":"# Now, the simulation\n\ndef coach_fourth_down_prob_estimate(ydstogo, coach_type='neutral'):\n    actual_prob = fourth_down_converted_prob(ydstogo)\n    if coach_type == 'conservative':\n        return actual_prob - 0.1\n    if coach_type == 'aggressive':\n        return actual_prob + 0.1\n    #if coach_type == 'neutral':\n    return actual_prob\n\n# Our new rule says to move the ball back half the distance towards opponent's end zone\ndef yardline_100_after_new_rule(yardline_100_before_new_rule):\n    return 100 - int(yardline_100_before_new_rule/2);\n    \ndef simulate(coach_type):\n    punt_return_reductions = 0\n    min_ep_diff = 0\n    for idx, play in punts_df.iterrows():\n        is_actual_punt_return = play.punt_in_endzone == '0'\\\n                        and play.punt_out_of_bounds == '0'\\\n                        and play.punt_downed == '0'\\\n                        and play.punt_fair_catch == '0'\\\n                    \n        next_play = pbp_df.iloc[idx+1]\n        actual_ep = next_play.ep\n        if np.isnan(actual_ep):\n            continue\n\n\n        # Let's make a decision\n        # Calculate my EP (= -1 * opponent's EP), if I choose to punt\n        expected_opp_yardline_100_if_punt = 100 - (play.yardline_100 - avg_punt_gain(play.yardline_100))\n        ep_if_punt = -1 * ep_1st_and_10(expected_opp_yardline_100_if_punt)\n\n        # Instead of punting, should I go for it on fourth down?\n\n        # Calculate my minimum EP, if fourth down converted\n        min_my_yardline_100_if_converted = play.yardline_100 - play.ydstogo\n        try:\n            min_ep_if_converted = ep_1st_and_10(min_my_yardline_100_if_converted)\n        except:\n            # ToDo: fix a bug\n            continue\n\n        # Calculate my minimum EP, if fourth down failed\n        # For the Kaggle challenge, we deem losing 2 yards to be the worst case\n        # ToDo: in the future, cases where more than 2 yards are lost should be taken into consideration\n        max_opp_yardline_100_if_failed = yardline_100_after_new_rule(play.yardline_100 + 2)\n        min_ep_if_failed = -1 * ep_1st_and_10(max_opp_yardline_100_if_failed)\n\n        # Now I can make the decision\n        my_success_prob = coach_fourth_down_prob_estimate(play.ydstogo, coach_type)\n        min_ep_if_going_for_it = my_success_prob * min_ep_if_converted \\\n            + (1 - my_success_prob) * min_ep_if_failed\n        if min_ep_if_going_for_it < ep_if_punt:\n            # Nah, let's punt - just as we were\n            continue\n        # Go for it!\n        if is_actual_punt_return:\n                punt_return_reductions += 1\n        fourth_down_converted = random.random() < fourth_down_converted_prob(play.ydstogo)\n        if fourth_down_converted:\n            simulated_ep = min_ep_if_converted\n        else:\n            simulated_ep = abs(min_ep_if_failed)\n        # Calculate how many EPs would be gained/lost compared to the actual outcome\n        min_ep_diff += (simulated_ep - actual_ep)\n    concussion_reductions = int((punt_return_concussion_risk - pass_run_concussion_risk) * punt_return_reductions)\n    print('If coaches were %s, there may be %d fewer punt returns, and therefore %d fewer concussions.'\\\n          % (coach_type, punt_return_reductions, concussion_reductions))\n    print('Also, we expect there would be %.1f more points scored per game.'\\\n          % (min_ep_diff/game_count))\n    print()\n    \n    \nsimulate('neutral')\nsimulate('aggressive')\nsimulate('conservative')\n# ToDo: survey real coaches to measure a more realistic impact of the rule change","execution_count":null,"outputs":[]}],"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}