{"cells":[{"metadata":{"_uuid":"dbd30b2afd57b1698dc551cb4be5fda0107477be"},"cell_type":"markdown","source":"# *How Removing A 45-Year-Old Rule Would Bring Down Concussion Rates*\n\nBy Alex Wainger (Data Engineer, Facebook)"},{"metadata":{"_uuid":"23b62db635c367ac5e67fdf76d64a2ba00d3e0ef"},"cell_type":"markdown","source":"## Proposed Rule Change\nEliminate the rule that kicking team players cannot go more than a yard past the line of scrimmage before a punt.$^{[1]}$\n\n## How Does That Help?\nRemoving this rule will increase the fair-catch rate and discourage teams from returning punts, which is the most dangerous part of the play. Allowing the kicking team to release downfield before the ball leaves the punter's foot **will reduce punt-related concussions by ~33%** while minimally impacting the receiving team's starting field position and the occurrence of exciting returns.\n\n## A Brief History Of The Existing Rule\nThe NFL introduced this rule before the 1974 season,$^{[2]}$ inspired by a similar rule change made by the NCAA in 1967.$^{[3]}$ The rule was part of \"a package of changes to reinvigorate the game by...increasing the opportunity for big plays on kickoff and punt returns...\"$^{[4]}$ in an effort to combat the launch of the World Football League — a rival league that \"sought to attract fans with rules that were friendlier to the offense than the NFL’s\" — in the year prior.$^{[4]}$ \n\nHowever, even in 1974, some were concerned about the health implications of this new rule. Bill Curry, President of the Players Association at the time, said \"the possibility of wide‐open punt returns with bodies crashing downfield is anathema to the players.\"$^{[5]}$ Despite the concerns, the rule change was approved and has now been part of the NFL rulebook for 45 years."},{"metadata":{"_uuid":"6fb059c6eb6e356a29576a1cc2b92bf027ff42dd"},"cell_type":"markdown","source":"Sources:\n1. https://operations.nfl.com/the-rules/2018-nfl-rulebook/#article-2.-kicking-team-players-on-line-during-kick\n2. https://en.wikipedia.org/wiki/1974_NFL_season#Major_rule_changes\n3. https://en.wikipedia.org/wiki/1967_college_football_season#Rule_changes\n4. https://operations.nfl.com/the-rules/evolution-of-the-nfl-rules\n5. https://www.nytimes.com/1974/05/21/archives/changes-bynfl-opposed-nfl-rule-changes-bring-opposition.html"},{"metadata":{"_kg_hide-input":true,"trusted":true,"_uuid":"cb6bdb08c5e754f99e68327871d9b1129508b57e"},"cell_type":"code","source":"import feather\nimport matplotlib.pyplot as plt\nimport numpy as np\nimport pandas as pd\n\nfrom scipy.stats import linregress\n\nfrom scipy.stats import ttest_ind\nfrom sklearn.ensemble import GradientBoostingClassifier\nfrom sklearn.linear_model import LogisticRegression\nfrom sklearn.metrics import roc_auc_score\nfrom sklearn.model_selection import train_test_split, GridSearchCV\n\n%matplotlib inline\npd.set_option('display.max_columns', None)\npd.set_option('display.expand_frame_repr', False)\npd.set_option('max_colwidth', -1)","execution_count":null,"outputs":[]},{"metadata":{"_uuid":"29742abd69e1bbf241864d833c3f7d31fddff5ee"},"cell_type":"markdown","source":"## Exploratory Data Analysis\nI'll spare you from going through all the exploration and visualization I did for this project. Instead, I'll show a couple relevant charts and numbers that inspired my proposed rule change."},{"metadata":{"_kg_hide-input":true,"_kg_hide-output":false,"trusted":true,"_uuid":"670c2f67f69e866f51ee801706b121e762560457"},"cell_type":"code","source":"# Loading all the datasets I built in the preprocessing notebook\nplay_information = feather.read_dataframe('../input/nflpuntpreprocessed/play_information.feather')\ngame_data = feather.read_dataframe('../input/nflpuntpreprocessed//game_data.feather')\nplay_player_role_data = feather.read_dataframe('../input/nflpuntpreprocessed/play_player_role_data.feather')\nmin_distances = feather.read_dataframe('../input/nflpuntpreprocessed/min_distances.feather')\nsecond_min_distances = feather.read_dataframe('../input/nflpuntpreprocessed/second_min_distances.feather')\npunt_hangtime = feather.read_dataframe('../input/nflpuntpreprocessed/punt_hangtime.feather')\nvideo_review = feather.read_dataframe('../input/nflpuntpreprocessed/video_review.feather')","execution_count":null,"outputs":[]},{"metadata":{"_kg_hide-input":true,"trusted":true,"_uuid":"2f5a3723ea5190c88f8a0801fe448990385e9237"},"cell_type":"code","source":"# Building a master play-level dataset\naug_play_information = play_information.merge(\n    min_distances[['GameKey', 'PlayID', 'Coverage_Distance']],\n    on=['GameKey', 'PlayID'], how='left', validate='one_to_one'\n).merge(\n    second_min_distances[['GameKey', 'PlayID', 'Coverage_Distance']],\n    on=['GameKey', 'PlayID'], how='left', validate='one_to_one',\n    suffixes=['_1', '_2']\n).merge(\n    game_data[['GameKey', 'Temperature', 'Is_Grass', 'Is_Outdoor']] ,\n    on=['GameKey'], how='left', validate='many_to_one'\n).merge(\n    punt_hangtime[['GameKey', 'PlayID', 'Hangtime']],\n    on=['GameKey', 'PlayID'], how='left', validate='one_to_one'\n).merge(\n    video_review,\n    on=['GameKey', 'PlayID'], how='left', validate='one_to_one',\n)\n\naug_play_information['Has_Concussion'] = ~aug_play_information.GSISID.isna()","execution_count":null,"outputs":[]},{"metadata":{"_kg_hide-input":true,"trusted":true,"_uuid":"2de9e68e734ff22cdd85372efd05846f8ed6f81b"},"cell_type":"code","source":"plays_with_punts = aug_play_information[aug_play_information.Has_Punt]\nplays_with_concussions = aug_play_information[aug_play_information.Has_Concussion]\n\nprint(\"Total plays:\", aug_play_information.shape[0])\nprint(\"Plays with a punt:\", plays_with_punts.shape[0])\nprint(\"Plays with a concussion:\", plays_with_concussions.shape[0])\nprint(\"Plays with a punt and a concussion:\", plays_with_concussions[plays_with_concussions.Has_Punt].shape[0])","execution_count":null,"outputs":[]},{"metadata":{"_uuid":"23c3d5ad912d8f37f1edfe70065ab0075fe4d562"},"cell_type":"markdown","source":"The 219 plays without a punt are mostly fakes and plays blown dead by penalties, and with only 1 concussion, I'm going to exclude them from the remainder of the analysis and focus on plays in which a punt actually occurs.\n\nI looked at a bunch of slices of these 36 punt-related concussions, and one stood out in particular — concussion rates broken down by how the receiving team fielded the punt. I categorized each punt return type into 1 of 3 groups — fair catch, return, or unreturnable (i.e. punts out of bounds, downed punts, touchbacks, etc)."},{"metadata":{"_kg_hide-input":true,"trusted":true,"_uuid":"15423954e6f4aac3f86a40ef3a07f912f6d724fb"},"cell_type":"code","source":"plays_with_concussions = plays_with_concussions[plays_with_concussions.Has_Punt]\n\nconcussion_rates = plays_with_concussions.Punt_Type.value_counts() / plays_with_punts.Punt_Type.value_counts()\nax = concussion_rates.plot(\n    kind='barh', figsize=(10,5), color='#2678B2', fontsize=12,\n    title='Concussions Occur Over 7x More Often On Returned Punts')\nvals = ax.get_xticks()\nax.set(xlabel=\"Concussion Rate\", xticklabels=['{:,.1%}'.format(x) for x in vals]);\nax.xaxis.label.set_size(14)\nax.title.set_size(16)\n\nttest_res = ttest_ind(\n    plays_with_punts[plays_with_punts.Has_Return].Has_Concussion, \n    plays_with_punts[~plays_with_punts.Has_Return].Has_Concussion\n)\n\nif ttest_res.pvalue < .05:\n    print('The difference in concussion rates on plays with and without returns is statistically significant.')\nelse:\n    print('The difference in concussion rates on plays with and without returns is not statistically significant.')\nprint('p-value:', '%f' % ttest_res.pvalue)","execution_count":null,"outputs":[]},{"metadata":{"_uuid":"2b19aa07ad464eee7729b4e042b9c06b3aca0a29"},"cell_type":"markdown","source":"With a concussion rate of over 1%, a player is over 7x more likely to get concussed on a punt play with a return than a play without one (and that difference is statistically significant).\n\nTo validate this finding, I reviewed all of the videos of the plays with concussions. Of the 31 concussions that occurred on plays with returns, **30 of the concussions occurred after the punt was caught by the returner.** This point is essential to the rest analysis — fair catch plays and return plays are essentially identical up until the moment the punt returner catches the ball. In one, the play ends the moment the ball is caught, in the other, players run full-speed into one another from all sides.\n\nTherefore, incentivizing punt returners to call more fair catches, rather than returning punts, should reduce the overall concussion rate, as the return play concussions would not happen if the play ended when the punt was caught."},{"metadata":{"_uuid":"cb8d998bb7a60bd60648d05ec937d12a6d9c31de"},"cell_type":"markdown","source":"## Fair Catch Model\nIn order to incentivize teams to call more fair catches, I first had to understand what the drivers of fair catches were.\n\nI had a hypothesis that the distance between the punt returner and the nearest member of the kicking team would give us a lot of information about whether or not a fair catch was called. To prove it, I built a model to predict whether or not a punt play ended with a fair catch.\n\n### Features\n\n - **Coverage_Distance_1 and Coverage_Distance_2:** Coverage distance is the distance between the punt returner and a member of the coverage team at the moment the punt is caught. Each play has 11 coverage distance values (one for each member of the coverage team), so we pick the smallest and second smallest and assign them the \\_1 and \\_2 subscripts, respectively. \n - **Punt_Distance:** How many yards the punt traveled.\n - **Time_Passed_Sec:** How many seconds of game-time have passed when the punt is happening. 0 and 3600 represent the start and end of the game, 900, 1800, and 2700 represent the start of the second, third, and fourth quarters, etc.\n - **Score_Differential:** kicking team score - receiving team score\n - **Yard_Line_Absolute:** The line of scrimmage, represented as a number between 0 and 100. 0 represents the kicking team's goal line, 100 represents the receiving team's goal line.\n - **Hangtime:** Amount of time between ball leaving punter's foot and the punter returning catching it.\n - **Temperature:** For outdoor games, it's the temperature outside, for indoor games, 70 was used to represent an average indoor stadium temperature (as we don't have the inside temperature available)\n - **Is_Grass:** Boolean, whether or not the game is being played on grass.\n - **Is_Outdoor:**: Boolean, whether or not the game is being played outdoors."},{"metadata":{"_kg_hide-input":true,"trusted":true,"_uuid":"d6386c8a2f9aa9f16ec96558fcf6c222d16cfecf"},"cell_type":"code","source":"plays_with_return = plays_with_punts[plays_with_punts.Has_Return]\nplays_with_fair_catch = plays_with_punts[plays_with_punts.Has_Fair_Catch]\n\nlabel = ['Has_Fair_Catch']\n\nfeatures = [\n    'Punt_Distance', 'Time_Passed_Sec', 'Score_Differential',\n    'Yard_Line_Absolute', 'Coverage_Distance_1', 'Coverage_Distance_2',\n    'Hangtime', 'Temperature', 'Is_Grass', 'Is_Outdoor'\n]\n\nplays_with_return_or_fc = pd.concat(\n    [plays_with_return, plays_with_fair_catch]\n)[['GameKey', 'PlayID'] + features + label].dropna()\n\nx_train, x_test, y_train, y_test = train_test_split(\n    np.array(plays_with_return_or_fc[features]),\n    np.array(plays_with_return_or_fc[label]).ravel(),\n    test_size=0.2,\n    random_state=0\n)","execution_count":null,"outputs":[]},{"metadata":{"_kg_hide-input":true,"trusted":true,"_uuid":"ac34c6bd7c8d974166c88d3a535fb3006454782b"},"cell_type":"code","source":"parameters = {\n    'n_estimators':[10, 50, 100],\n    'learning_rate': [.01, .05, .1],\n    'max_depth': [1, 2, 3]\n}\n\nsearch = GridSearchCV(GradientBoostingClassifier(random_state=0), parameters, cv=3)\nsearch.fit(x_train, y_train)\n\nprint(search.best_params_)","execution_count":null,"outputs":[]},{"metadata":{"_uuid":"bf92696439722b418f0a23b0a2e2aee4b277e4db"},"cell_type":"markdown","source":"The model is trained on punts with returns as negative examples (unreturnable punts aren't really relevant here, since we're trying to learn how to convert returns to fair catches) and performs pretty well! Given the sample sizes we're working with, the data we have available and it's reliability, and the amount of actual randomness in the decision to call a fair catch, I'm happy with the accuracy."},{"metadata":{"_kg_hide-input":true,"trusted":true,"_uuid":"7973987d4302b0270611f1bad6777342ad53cb46"},"cell_type":"code","source":"# Using best params from grid-search above\ngbdt = GradientBoostingClassifier(\n    n_estimators=search.best_params_['n_estimators'],\n    learning_rate=search.best_params_['learning_rate'],\n    max_depth=search.best_params_['max_depth'],\n    random_state=0\n).fit(x_train, y_train)\n\ny_scores_gbdt = gbdt.predict_proba(x_test)[:,1]\n\nprint(\"Train accuracy: {:,.1%}\".format(gbdt.score(x_train, y_train)))\nprint(\"Test accuracy: {:,.1%}\".format(gbdt.score(x_test, y_test)))\nprint(\"ROC AUC: {:,.3f}\".format(roc_auc_score(y_test, y_scores_gbdt)))","execution_count":null,"outputs":[]},{"metadata":{"_uuid":"6116f8006922bb5e7a68ccdf69d416eb20fb6973"},"cell_type":"markdown","source":"Looking at the feature importances for our fair-catch model, my original hypothesis was correct — the minimal coverage distance is by far the most important feature in predicting whether or not a fair catch was called."},{"metadata":{"_kg_hide-input":true,"trusted":true,"_uuid":"b35bc3b564010a81519396b5d9139288c2f26d45"},"cell_type":"code","source":"ax = pd.DataFrame(\n    gbdt.feature_importances_,\n    index = features,\n    columns=['importance']\n).sort_values('importance', ascending=True).plot(\n    title='Fair-Catch Model Feature Importances', kind='barh', legend=False, figsize=(10,5), fontsize=12\n)\n\nax.title.set_size(16)","execution_count":null,"outputs":[]},{"metadata":{"_uuid":"f795ff4fc4c312e964f3550d548c5967b3e049ae"},"cell_type":"markdown","source":"So we know coverage distance is by far the most important feature in predicting fair catches. Logically, there is probably an inverse relationship between coverage distance and fair catches, i.e. smaller coverage distance —> larger odds of a fair catch. To confirm this, I trained a logistic regression model to perform the same task and looked at the direction of the coefficients -- unsurprisingly, coverage distance has a negative coefficient."},{"metadata":{"_kg_hide-input":true,"trusted":true,"_uuid":"684a584240e3fed49c45c4aceade1907d22347b6"},"cell_type":"code","source":"logreg = LogisticRegression(solver='liblinear', random_state=2).fit(x_train, y_train)\ny_scores_logreg = logreg.predict_proba(x_test)[:,1]\ny_pred_logreg = logreg.predict(x_test)\n\nprint(\"Train accuracy:\",logreg.score(x_train, y_train))\nprint(\"Test accuracy:\", logreg.score(x_test, y_test))\nprint(\"ROC AUC:\", roc_auc_score(y_test, y_scores_logreg))\n\nax = pd.DataFrame(\n    logreg.coef_.transpose(),\n    index=features,\n    columns=['coefficients']\n).sort_values('coefficients').plot(\n    kind='barh', legend=False, title='Logistic Regression Feature Coefficients', figsize=(10,5), fontsize=14\n)\n\nax.title.set_size(16)\n","execution_count":null,"outputs":[]},{"metadata":{"_uuid":"4774ca12e2e5c68ebdfa9d8d21bb774bba5cbde0"},"cell_type":"markdown","source":"## Estimating Impact of the Rule Change\nKnowing what we now know about fair catches, the rule holding non-gunners at the line of scrimmage until the punt stood out as an area of opportunity — it gives returners space to advance the ball, which we know is associated with higher concussion rates.\n\nRemoving the rule would allow players to release downfield into coverage whenever they want. It will strictly decrease the minimal coverage distance — conservative teams afraid of punt blocks will hold their blocking as long as they do now, while more aggressive teams will leave the line of scrimmage well before the punt leaves the punter's foot.\n\nTo estimate how many returns get converted into fair catches, I did the following:\n1. Measure the amount of time between the snap and punt events.\n2. Calculate the average speed of each non-gunner on the kicking team between the punt and the reception.\n3. Picking some fraction to represent how much sooner the kicking team would start downfield, calculate \"adjusted coverage distances\" —\n   1. For gunners, this is the same as the coverage distance we originally calculated for the model\n   2. For non-gunners, *adjusted_coverage_distance = coverage_distance - (snap_to_punt_time X average_speed X blocking_to_coverage_time_fraction)*\n4. Feed the smallest and second smallest adjusted coverage distances back into our trained fair catch model that we developed earlier and see how many returns are now predicted to be fair catches.\n\nI picked 0.5 for the blocking_to_coverage_time_fraction, which means in my simulation, the non-gunners will start heading downfield half way between the snap and the punt. As I wrote above, conservative teams may not send their players much earlier, while aggressive teams may just chip the receiving blockers and then head downfield, so 0.5 seemed like a reasonable middle ground."},{"metadata":{"_kg_hide-input":true,"trusted":true,"_uuid":"3d7d5cf3f26c07ae9bb09a8219ffe8235ee8be99"},"cell_type":"code","source":"def update_coverage_distance(row):\n    snap_to_punt_time_adjustment = .5 # Percentage of blocking time converted to coverage time\n    minimal_coverage_distance = .5 # don't let adjusted coverage distance get smaller than half a yard\n    if 'GL' in row.Role_k or 'GR' in row.Role_k:\n        return row.Coverage_Distance\n    else:\n        return max(\n            row.Coverage_Distance - (\n                row.Yards_Per_Second * (\n                    row.Snap_To_Punt_time * snap_to_punt_time_adjustment\n                )\n            ), minimal_coverage_distance\n        )","execution_count":null,"outputs":[]},{"metadata":{"_kg_hide-input":true,"trusted":true,"_uuid":"92220670b13754f0edae11da0ff643b723746d63"},"cell_type":"code","source":"# Calculate Adjusted Coverage Distances\nadjusted_coverage_distances = feather.read_dataframe('../input/nflpuntpreprocessed/adjusted_coverage_distances.feather')\n\nadjusted_coverage_distances['Adj_Coverage_Distance'] = adjusted_coverage_distances.apply(\n    update_coverage_distance, axis=1\n)\n\nmin_acd = adjusted_coverage_distances.loc[\n    adjusted_coverage_distances.groupby(['GameKey', 'PlayID'])['Adj_Coverage_Distance'].idxmin()\n][['GameKey', 'PlayID', 'Adj_Coverage_Distance', 'Role_k', 'Super_Role_k']]\n\nadjusted_coverage_distances_2 = adjusted_coverage_distances.drop(\n    adjusted_coverage_distances.groupby(['GameKey', 'PlayID'])['Adj_Coverage_Distance'].idxmin()\n)\n\nsecond_min_acd = adjusted_coverage_distances_2.loc[\n    adjusted_coverage_distances_2.groupby(['GameKey', 'PlayID'])['Adj_Coverage_Distance'].idxmin()\n][['GameKey', 'PlayID', 'Adj_Coverage_Distance', 'Role_k', 'Super_Role_k']]","execution_count":null,"outputs":[]},{"metadata":{"_uuid":"6a69b40f3b142529b31c24328f7f364b8f29a256"},"cell_type":"markdown","source":"Looking first at the breakdown of which member of the coverage team is closest to the punt returner at punt reception time, we can see that the rule change drastically changes the distribution. In the game today, gunners are the closest player nearly 80% of the time, but that number gets nearly cut in half after my rule change. This is a good sign for the rule change, as it means the minimum coverage distance is getting smaller, as linemen and protectors are getting to the punt returner before the gunners much more often than before the rule change."},{"metadata":{"_kg_hide-input":true,"trusted":true,"_uuid":"6eba74cd60bd960caa6e689b12e9f8fcd9f48290"},"cell_type":"code","source":"ax = pd.DataFrame({\n    \"Before Rule Change\": min_distances.Super_Role_k.value_counts() / min_acd.shape[0],\n    \"After Rule Change\": min_acd.Super_Role_k.value_counts() / min_acd.shape[0]\n}).plot(kind='barh', figsize=(10,5), title='Nearest Coverage Team Member At Time Of Punt Reception')\n\nvals = ax.get_xticks()\nax.set(xlabel=\"% of Plays\", xticklabels=['{:,.0%}'.format(x) for x in vals])\n\nax.title.set_size(16)\nax.xaxis.label.set_size(14)","execution_count":null,"outputs":[]},{"metadata":{"_kg_hide-input":true,"trusted":true,"_uuid":"aa66e14fc6520ee9d04350f5b1865d67cacd249f"},"cell_type":"code","source":"# Evaluate trained model on new adjusted coverage distance features\nadj_features = [\n    'Punt_Distance', 'Time_Passed_Sec', 'Score_Differential',\n    'Yard_Line_Absolute', 'Adj_Coverage_Distance_1', 'Adj_Coverage_Distance_2',\n    'Hangtime', 'Temperature', 'Is_Grass', 'Is_Outdoor'\n]\n\nadj_X = plays_with_return_or_fc.merge(\n    min_acd, on=['GameKey', 'PlayID'], how='inner', validate='one_to_one'\n).merge(\n    second_min_acd, on=['GameKey', 'PlayID'], how='inner', validate='one_to_one', suffixes=[\"_1\", \"_2\"]\n)\n\nadj_X['Adj_Has_Fair_Catch_pred'] = gbdt.predict(adj_X[adj_features]) | adj_X.Has_Fair_Catch","execution_count":null,"outputs":[]},{"metadata":{"_kg_hide-input":true,"trusted":true,"_uuid":"61c9c76bf871bee35f903b05076af7cc3dc97cf4"},"cell_type":"code","source":"adj_plays_with_punts = plays_with_punts.merge(\n    adj_X[['GameKey', 'PlayID', 'Adj_Has_Fair_Catch_pred']],\n    on=['GameKey', 'PlayID'], how='left', validate='one_to_one'\n)\n\nadj_plays_with_punts['Adj_Punt_Type'] = adj_plays_with_punts.apply(\n    lambda row:\n        row.Punt_Type\n        if pd.isna(row.Adj_Has_Fair_Catch_pred)\n        else (\n            'fair catch'\n            if row.Adj_Has_Fair_Catch_pred\n            else 'return'\n        )\n    , axis=1\n)\nadj_plays_with_punts['Adj_Has_Fair_Catch_pred'] = adj_plays_with_punts.Adj_Punt_Type == 'fair catch'\nadj_plays_with_punts['Adj_Punt_Return_Length'] = adj_plays_with_punts.apply(\n    lambda row:\n        row.Punt_Return_Length\n        if not row.Adj_Has_Fair_Catch_pred\n        else 0.0\n    , axis=1\n)","execution_count":null,"outputs":[]},{"metadata":{"scrolled":true,"_kg_hide-input":true,"trusted":true,"_uuid":"f87cc200db708ece570dd82bd9ae9437ca2fd706"},"cell_type":"code","source":"ax = pd.DataFrame({\n    \"(Estimated) After Rule Change\": adj_plays_with_punts.Adj_Punt_Type.value_counts() / adj_plays_with_punts.shape[0],\n    \"Before Rule Change\": adj_plays_with_punts.Punt_Type.value_counts() / adj_plays_with_punts.shape[0]\n}, index=adj_plays_with_punts.Punt_Type.unique()).transpose().plot(\n    kind='barh', title='Rule Change Increases Fair Catch Frequency from 25% to 40%', figsize=(10,5)\n)\nvals = ax.get_xticks()\nax.set(xlabel=\"% of Plays\", xticklabels=['{:,.0%}'.format(x) for x in vals]);\n\nax.xaxis.label.set_size(14)\nax.title.set_size(16)","execution_count":null,"outputs":[]},{"metadata":{"_uuid":"8e1154a4499586c467491f661509082e45f94b18"},"cell_type":"markdown","source":"By reevaluating the model with the adjusted coverage distance features, we can essentially simulate the two seasons of punting with my rule change and see what kind of effects it has on fair-catch frequency. As expected, the percent of plays that end with a fair catch jumps from 25% to 40% and becomes the most common way to end a punt play."},{"metadata":{"_uuid":"d595b5a1a455ad68eba54d4cd62689ab0157031f"},"cell_type":"markdown","source":"### Concussion Rate"},{"metadata":{"_kg_hide-input":true,"trusted":true,"_uuid":"9a7ad417aa5474edfb076c0479e744ab700ec8d6"},"cell_type":"code","source":"pre_concussions = adj_plays_with_punts.Has_Concussion.sum()\npost_concussions = (\n    adj_plays_with_punts.Has_Concussion & ~(\n        (adj_plays_with_punts.Punt_Type == 'return') & (adj_plays_with_punts.Adj_Punt_Type == 'fair catch')\n    )\n).sum()\nprint(\"# Concussions Before Rule Change:\", pre_concussions)\nprint(\"# Concussions After Rule Change:\", post_concussions)\nprint(\"Percent Decrease in Concussions: {:,.1%}\".format((pre_concussions - post_concussions) / pre_concussions))","execution_count":null,"outputs":[]},{"metadata":{"_uuid":"1d48a862b58def28948515a92a5c17d797141cfa"},"cell_type":"markdown","source":"To understand the impact that this rule change could have on the occurrences of concussions, I identified all of the concussions that happened on returns that my rule change would have converted to fair catches — these would not have happened in this alternate universe. This is obviously a rough estimate, as there are a number of assumptions and randomness baked into the analysis, but the result shows that converting returns to fair catches could have a significant impact on overall concussion rates."},{"metadata":{"_uuid":"b227ca202ed752027ec49f3671c28ba284042d2c"},"cell_type":"markdown","source":"### Field Position and \"Exciting\" Returns\nSo concussion rates are going down, great! But what if this rule change takes away all the excitement of punt returns, or drastically changes the average starting field position for the receiving team? Luckily, the increases in fair catches doesn't have too big an impact on either."},{"metadata":{"_kg_hide-input":true,"trusted":true,"_uuid":"8e97ae407a806ce715d4cc9ff0f67c6532347bd0"},"cell_type":"code","source":"adj_plays_with_punts['Next_Poss_Yard_Line'] = adj_plays_with_punts.apply(\n    lambda row:\n        np.nan\n        if row.Punt_Type == 'unreturnable'\n        else 100 - (row.Yard_Line_Absolute + row.Punt_Distance - row.Punt_Return_Length)\n    , axis=1\n)\n\nadj_plays_with_punts['Adj_Next_Poss_Yard_Line'] = adj_plays_with_punts.apply(\n   lambda row:\n        np.nan\n        if row.Adj_Punt_Type == 'unreturnable'\n        else 100 - (row.Yard_Line_Absolute + row.Punt_Distance - row.Adj_Punt_Return_Length)\n    , axis=1\n)\n\nadj_plays_with_returnable_punts = adj_plays_with_punts[\n    adj_plays_with_punts.Has_Return | adj_plays_with_punts.Has_Fair_Catch\n]\n\nax = pd.DataFrame(\n    {\n        'Before Distribution':adj_plays_with_returnable_punts.Next_Poss_Yard_Line,\n        'After Distribution': adj_plays_with_returnable_punts.Adj_Next_Poss_Yard_Line\n    }\n).plot(\n    kind='density', xlim=(0,100), ylim=(0,.03), figsize=(12,6),\n    title='Distribution of Receiving Team\\'s Starting Field Position',\n)\n\nbefore_mean_yard_line = adj_plays_with_returnable_punts.Next_Poss_Yard_Line.mean()\nafter_mean_yard_line = adj_plays_with_returnable_punts.Adj_Next_Poss_Yard_Line.mean()\n\nyard_line_vals = [0,10,20,30,40,50,60,70,80,90,100]\nyard_line_labels = [\n    'Own Goal Line' if x == 0\n    else (\n        'Opp Goal Line' if x == 100\n        else (\n            x if x <= 50 \n            else 100-x\n        )\n    ) for x in yard_line_vals]\nax.set(\n    xlabel=\"Yard Line\", xticks=yard_line_vals, xticklabels=yard_line_labels,\n    yticklabels= [''] + [str(x) for x in ax.get_yticks()[1:]]\n)\nplt.axvline(\n    before_mean_yard_line, linestyle='dashed', color='tab:blue', linewidth=2,\n    label='Before Mean: {:,.1f} yard line'.format(before_mean_yard_line)\n)\nplt.axvline(\n    after_mean_yard_line, linestyle='dashed', color='tab:orange', linewidth=2,\n    label='After Mean: {:,.1f} yard line'.format(after_mean_yard_line)\n)\nax.xaxis.label.set_size(14)\nax.title.set_size(16)\nplt.legend();","execution_count":null,"outputs":[]},{"metadata":{"_uuid":"718b2a7f62d56cc6c1c86ca4e90bf3ad1c000bad"},"cell_type":"markdown","source":"Average starting field position drops by just a yard and a half, from the 28.4 to the 27. After the rule change, we see a small spike around the 15 and a small decrease in frequency of drives starting between the 40's. These are pretty minimal effects given the amount we're reducing concussions by.\n\nA savvy reader may notice that I'm assuming punt return distances wouldn't be affected by the rule change. I originally built a model using my coverage distance features to predict punt return distance, thinking that the closer the coverage team was to the returner when he caught the ball, the smaller the return will be. That model performed pretty poorly, and calculating a simple correlation between coverage distance and punt return distance shows why:"},{"metadata":{"_kg_hide-input":true,"trusted":true,"_uuid":"c9a6805bd79d248d19a8790609d3408cc8c26b3c"},"cell_type":"code","source":"x = plays_with_return[['Punt_Return_Length', 'Coverage_Distance_1']].dropna()\nlinregress_result = linregress(x.Coverage_Distance_1, x.Punt_Return_Length)\nprint('R^2: {:,.3f}'.format(linregress_result.rvalue ** 2))\nprint('P-value: {}'.format(linregress_result.pvalue))","execution_count":null,"outputs":[]},{"metadata":{"_uuid":"cb634be08898543d0451911fee98a55d1f7ed40c"},"cell_type":"markdown","source":"Just 4.2% of the variance in punt return return distance can be explained by the minimal coverage distance. The p-value is extremely small, meaning that the minimum coverage distance would be a meaningful addition to a model predicting punt return distance, but there is a ton of variance in punt return distance that coverage distance just can't explain.\n\nThe primary impact of my rule change would be in reducing the minimum coverage distance, and there are so many other factors that influence how far a punt gets returned, so it would have just added noise to the field position analysis to include an adjustment from my rule change."},{"metadata":{"_kg_hide-input":true,"trusted":true,"_uuid":"377f7511f58bb9a27bd489f6b44462ee05558689"},"cell_type":"code","source":"exciting_return_length = 20\n\nprint(\"Of All Punts (returnable and unreturnable)\")\nprint(\"    Percent of total punt plays with >20 yard return before change: {:,.1%}\".format(\n    (adj_plays_with_punts.Punt_Return_Length > exciting_return_length).sum() / adj_plays_with_punts.shape[0]\n))\nprint(\"    Percent of total punt plays with >20 yard return after change: {:,.1%}\".format(\n    (adj_plays_with_punts.Adj_Punt_Return_Length > exciting_return_length).sum() / adj_plays_with_punts.shape[0]\n))","execution_count":null,"outputs":[]},{"metadata":{"_uuid":"066e786a8994fca756e9e395fbdd38a3064ccea5"},"cell_type":"markdown","source":"Looking now at \"exciting\" punt returns (defined here as a return of 20 yards or more), we see the percent of punt plays with one drops by a percentage point, from 4.1% to 3.1%. While in the abstract this represents a ~25% decrease in exciting returns, the fact that only 1 in 25 punts end up having an exciting return to begin with likely means fans won't notice the downtick, as long as there is still a big run back every now and then. Tyreek Hill and Odell Beckham will still be able to break off electrifying returns that set their teams up with good field position."},{"metadata":{"_uuid":"938e761d3390ffb47dad148699ac024803fdfa96"},"cell_type":"markdown","source":"## Unintended Consequences"},{"metadata":{"_uuid":"ef5cef1d00df4f258735d24efef0ecbccd3f4d65"},"cell_type":"markdown","source":"In terms of effects I can reasonably estimate with data, the rule change seems to do exactly what I want it to do. However, there would almost certainly be second- and third-order effects or unintended consequences of the rule change. It's hard (or even impossible) to quantify how each of these might affect the game, but I don't think that even in a worst-case scenario that any of them outweigh the reduction in concussions."},{"metadata":{"_uuid":"79321de0d9b727ed68bc9077b0a3f1b8f8b9d65c"},"cell_type":"markdown","source":"#### NCAA-style Rugby Roll Out Punts\nSince the NCAA allows the kicking team to release downfield on the snap, we can learn a lot from how college teams cover punts. The most obvious difference between the college and professional punting game is the use of a rugby-style punt — some college teams will roll their punter out toward the sideline, buying time for the rest of the coverage team to sprint further downfield than they would during a standard 3-step punt. My rule change could cause an inadvertent rise in this type of strategy.\n\nHowever, I don't actually think rugby roll out punts would catch on in the NFL. College punters often don't have the leg strength to hit a 50 yard punt with 4-5 seconds of hang time. As a result, teams turn to a gimmicky play to try to buy more time for their coverage team. At the NFL level, punters can boom kicks far and high enough that coverage teams have time to get downfield with just 1-2 seconds from snap to kick (and they'll have even more time under my rule change).\n\nInterestingky, last season Texas and Alabama had the 1st and 3rd ranked punters in the country in Michael Dickson and JK Scott (both of whom are now starters in the NFL), and both teams primarily kicked traditional punts — despite Dickson having played Australian-rules football until he was 19. If your punter has the leg strength to boom the ball down the field, there's no point in trying to pull off a risky on-the-move punt to buy a few extra seconds.\n\nRelatedly, the rugby roll out also requires a very particular skill-set — ability to punt (or at least threaten to punt) with either leg while on the run — that most NFL punters likely don't practice (Australians like Dickson being the exception here, but while they're extremely common in college, there aren't an overwhelming number of starting Australian punters in the NFL today). Initially, teams may try the tactic, but it would take a few seasons of experimenting and practice under the rule change to master the rugby roll out, at which point the NFL would have some data to evaluate whether or not it's good for the game.\n\n#### Changes In Receiving Team Strategy\nAnother side effect I could see arising from my rule change is the receiving team changing the way they approach blocking for the punt returner. Some teams may choose to drop a few blockers back near the punt returner to give the returner time and space to advance the ball. This could potentially dampen my rule change's reduction in returns (which in turn would reduce the impact on overall concussion rates).\n\nHowever, if the return team goes overboard with dropping blockers back, they will leave themselves susceptible to fake punts. For example, if the return team lines up with a returner, 2 gunners, and 3 \"personal protectors\" for the returner who are somewhere between the returner and the line of scrimmage, that would leave just 5 down linemen at the line. Traditional formations for the kicking team have two gunners, a punter, and 8 linemen, wings, and protectors all near the ball on the line of scrimmage. An 8 vs. 5 advantage at the line would make it trivially easy for the kicking team to direct snap to a skill-position player, overpower the 5 down linemen, and pickup a first down. As a result, I wouldn't be worried about teams dropping a significant number of players away from the line.\n\nIn the opposite extreme, some teams may abandon the punt return all together and instead send all their players after a punt block, always calling a fair catch if they don't block the punt (since there would be no blocking for the returner). This poses a legitimate threat to my rule change's impact, as kicking teams may be forced to hold their blocks until the punt, which would give the returner the same time and space he has now to attempt a return.\n\nIf we saw teams employing this strategy after my rule change gets implemented, it would be pretty easy to pass a formation rule that prevents the receiving team from sending everyone after the punt (e.g. the receiving team has to at least match the number of players the kicking team has outside the numbers, so two gunners must be matched by two jammers) to mitigate this strategy."},{"metadata":{"_uuid":"5de6fa80a9bf0d9bce32f8efc8c5caaf87b10f75"},"cell_type":"markdown","source":"## Conclusion\n\nSo there you have it! The removal of a 45-year-old rule could help prevent around 1 out of 3 punt-related concussions while minimally impacting the integrity and quality of the return game. I'll leave you with one final advantage of my rule change — it takes something out of the rulebook. I brainstormed a ton of reward-based rules like gifting returners some amount of yards for doing some action, or adding additional penalties to try to take away certain types of hits. Even if those rule changes are just as effective as mine, they also makes the game more complicated. \n\nIf we continue down the path of adding new rules to incentivize players to do what we want, we'll eventually end up with a complicated web of rewards and penalties, with each new rule addition becoming harder to evaluate and implement than the last. A rule change that effectively reduces concussions while also making the game just touch less complicated is a win-win in my book."}],"metadata":{"kernelspec":{"display_name":"Python 3","language":"python","name":"python3"},"language_info":{"codemirror_mode":{"name":"ipython","version":3},"file_extension":".py","mimetype":"text/x-python","name":"python","nbconvert_exporter":"python","pygments_lexer":"ipython3","version":"3.7.2"}},"nbformat":4,"nbformat_minor":1}