{"metadata":{"kernelspec":{"display_name":"Python 3","language":"python","name":"python3"},"language_info":{"name":"python","version":"3.12.12","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"varInspector":{"cols":{"lenName":16,"lenType":16,"lenVar":40},"kernels_config":{"python":{"delete_cmd_postfix":"","delete_cmd_prefix":"del ","library":"var_list.py","varRefreshCmd":"print(var_dic_list())"},"r":{"delete_cmd_postfix":") ","delete_cmd_prefix":"rm(","library":"var_list.r","varRefreshCmd":"cat(var_dic_list()) "}},"types_to_exclude":["module","function","builtin_function_or_method","instance","_Feature"],"window_display":false},"kaggle":{"accelerator":"none","dataSources":[{"sourceType":"competition","sourceId":12125,"databundleVersionId":862173},{"sourceType":"datasetVersion","sourceId":12713865,"datasetId":98965,"databundleVersionId":13327470},{"sourceType":"kernelVersion","sourceId":9758020},{"sourceType":"kernelVersion","sourceId":9987655}],"dockerImageVersionId":19836,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"# Reducing Concussions for Punt Plays\n<img src=\"https://s3.amazonaws.com/nonwebstorage/headstrong/lineup.png\" />","metadata":{"_uuid":"10fb411bbe1c16f42f461cb9c52990f403f4d17d"}},{"cell_type":"code","source":"%%HTML\n<style type=\"text/css\">\n\ndiv.h1 {\n    background-color: steelblue; \n    color: white; \n    padding: 8px; \n    padding-right: 300px; \n    font-size: 20px; \n    max-width: 1500px; \n    margin: auto; \n    margin-top: 50px;\n}\ndiv.h3 {\n    color: steelblue; \n    font-size: 14px; \n    margin-top: 20px; \n    margin-bottom:4px;\n}\ndiv.h4 {\n    font-size: 15px; \n    margin-top: 20px; \n    margin-bottom: 8px;\n}\nspan.note {\n    font-size: 5; \n    color: gray; \n    font-style: italic;\n}\nspan.captiona {\n    font-size: 5; \n    color: dimgray; \n    font-style: italic;\n    margin-left: 130px;\n    vertical-align: top;\n}\nhr {\n    display: block; \n    color: gray\n    height: 1px; \n    border: 0; \n    border-top: 1px solid;\n}\nhr.light {\n    display: block; \n    color: lightgray\n    height: 1px; \n    border: 0; \n    border-top: 1px solid;\n}\ntable.dataframe th \n{\n    border: 1px darkgray solid;\n    color: black;\n    background-color: white;\n}\ntable.dataframe td \n{\n    border: 1px darkgray solid;\n    color: black;\n    background-color: white;\n    font-size: 14px;\n    text-align: center;\n} \ntable.rules th \n{\n    border: 1px darkgray solid;\n    color: black;\n    background-color: white;\n    font-size: 14px;\n}\ntable.rules td \n{\n    border: 1px darkgray solid;\n    color: black;\n    background-color: white;\n    font-size: 13px;\n    text-align: center;\n} \ntable.rules tr.best\n{\n    color: green;\n}\n\n</style>","metadata":{"_kg_hide-input":true,"_kg_hide-output":false,"trusted":true,"_uuid":"252ca74e330c40f55b588b2d6684f8b9896b6b40","execution":{"iopub.status.busy":"2025-08-09T01:39:45.496686Z","iopub.execute_input":"2025-08-09T01:39:45.497412Z","iopub.status.idle":"2025-08-09T01:39:45.510480Z","shell.execute_reply.started":"2025-08-09T01:39:45.497373Z","shell.execute_reply":"2025-08-09T01:39:45.509537Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"! pip install hvplot","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-09T01:39:45.512486Z","iopub.execute_input":"2025-08-09T01:39:45.512781Z","iopub.status.idle":"2025-08-09T01:39:49.475753Z","shell.execute_reply.started":"2025-08-09T01:39:45.512757Z","shell.execute_reply":"2025-08-09T01:39:49.474852Z"},"_kg_hide-input":true,"_kg_hide-output":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import warnings\n\nimport numpy as np\nimport pandas as pd\nfrom scipy import stats\n\nimport hvplot.pandas\nfrom IPython.display import HTML, Image\n\n\n# set additional display options for report\npd.set_option(\"display.max_columns\", 100)\nth_props = [\n    (\"font-size\", \"13px\"),\n    (\"background-color\", \"white\"),\n    (\"color\", \"#666666\"),\n]\ntd_props = [(\"font-size\", \"15px\"), (\"background-color\", \"white\")]\nstyles = [\n    dict(selector=\"td\", props=td_props),\n    dict(selector=\"th\", props=th_props),\n]\n","metadata":{"_kg_hide-input":true,"trusted":true,"_uuid":"912933533fc57111d7139d91471cb8248f00d388","_kg_hide-output":false,"execution":{"iopub.status.busy":"2025-08-09T01:39:49.476823Z","iopub.execute_input":"2025-08-09T01:39:49.477104Z","iopub.status.idle":"2025-08-09T01:39:53.196680Z","shell.execute_reply.started":"2025-08-09T01:39:49.477075Z","shell.execute_reply":"2025-08-09T01:39:53.195877Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"PROLOGUE 2026: \n\nThe NFL used this research to support the case for eliminating blindside blocks in certain circumstances, particularly for punt returns. I presented my findings to NFL excutives during SuperBowl week in early 2020, and they in turn used them to secure a majority vote from team owners to add a rule change for the 2020 season. \n\nThe rule change and it's enforcement have been a source of ongoing controversy among fans. Many see it as a softening of the game and miss the \"good old days\" when players knocked heads at high rates of speed. Owners, however, know the impact of traumatic brain injury on profitability and player health, and continue to support the rule change.\n","metadata":{"execution":{"iopub.status.busy":"2026-04-16T03:18:34.525001Z","iopub.execute_input":"2026-04-16T03:18:34.525398Z","iopub.status.idle":"2026-04-16T03:18:34.536208Z","shell.execute_reply.started":"2026-04-16T03:18:34.525370Z","shell.execute_reply":"2026-04-16T03:18:34.534936Z"}}},{"cell_type":"markdown","source":"This report represents my analysis for the NFL Punt Analytics Competition. It is my opinion that based on the data provided, changing two rules for punt plays could result in up to 8 fewer concussions per year. The two proposed changes are as follows:\n\n  - *Move the ball forward 10 yards after a Fair Catch.* After fair catch of a scrimmage kick, play would start 10 yards forward from the spot of catch, or the succeeding spot after eforcement of penalties. This would apply when the receiving team elects to put the ball in play by a snap.\n\n  - *Penalize blindside blocks with forcible contact to any part of a defenseless player's body.*  Defenseless players as described by NFL rules include players receiving a blindside block from a blocker whose path is toward or parallel his own end line. Prohibited contact for punt plays would include blindside blocks with forcible contact to any part of a defenseless player's body, including below the neck.\n\nThe figure below shows the potential reduction in concussions based on 2016-2017 data and associated assumptions.\n<br>\n\n<span style=\"display:inline-block; margin-top:30px; margin-left:112px; font-size:12pt; font-weight:bold\">Potential Reductions</span>\n<img src=\"https://s3.amazonaws.com/nonwebstorage/headstrong/redux.png\" width=\"600\" height=\"400\">\n\n\n<p style=\"margin-top:40px\"> \nMy analysis is thoroughly documented in the following sections:  </p>\n\n - Background\n - Methodology\n - Analysis\n - Conclusion\n - Qualifications\n - Appendix\n\n  \nYou can find the complete code for preparing the attached data files in the following kernels and datasets:\n\n   - [NFL Data Preparation](https://www.kaggle.com/jpmiller/nfl-data-preparation)\n   - [NFL Punt Play Animation](https://www.kaggle.com/jpmiller/nfl-punt-play-animation)\n   - [NFL Concussion Data](https://www.kaggle.com/jpmiller/nfl-competition-data)\n\n<p style=\"margin-top:40px\">\nThank you and happy reading!<br>\nJohnM<br>","metadata":{"_uuid":"3a2b8722db6fb977d829ab533938013b661b6748"}},{"cell_type":"markdown","source":"<a id='bg'></a>\n# Background\n\n<p style=\"margin-top: 20px\">Since 1920 the National Football League has developed the model for a successful modern sports league. The NFL enterprise includes national and international distribution, extensive revenue sharing, competitive excellence, and strong franchises across the country. Last year, 29 of the NFL's 32 teams appeared in the Forbes Top 50 most valuable sports franchises in the world. Football is also regarded as the United States' most popular sport. A 2018 Gallup poll found that among US adults, 37% name football as their favorite sport to watch. The number tops basketaball(11%), baseball(9%) and soccer(7%) by a wide margin. </p>\n\nPlayer safety has always been a concern for football. Over the past few years, concussions sustained during play have become one of the most visible issues affecting players. Concussion incidents rose in 2017 even as the NFL implemented several safety measures. The figure below shows the incidence of concussions since 2012. \n\n<span class=\"note\"> <i>Hover over the points to see exact numbers.</i> </span>","metadata":{"_uuid":"f13772a6f3a9b6c1555c24a7da621b0eef414dba"}},{"cell_type":"code","source":"hist_df = pd.DataFrame(\n    {\n        \"Season\": np.arange(2012, 2018),\n        \"Concussions\": [265, 244, 212, 279, 250, 291],\n    }\n)\nline_incidents = hist_df.hvplot.line(\n    x=\"Season\",\n    y=\"Concussions\",\n    xlim=(2011.5, 2017.5),\n    ylim=(0, 350),\n    title=\"Concussion Incidents for Full Season (incl Practice)\",\n    yticks=np.arange(50, 350, 50).tolist(),\n    xticks=np.arange(2012, 2018).tolist(),\n    grid=True,\n)\nscat_incidents = hist_df.hvplot.scatter(\n    x=\"Season\", y=\"Concussions\", size=50\n)\ndisplay(\n    line_incidents * scat_incidents,\n    HTML('<span class=\"captiona\">' + \"Source: IQVIA\" + \"<span\"),\n)\n","metadata":{"_kg_hide-input":true,"trusted":true,"_uuid":"704b7a65a88e1a5bce497c352de1ca09c3555c36","execution":{"iopub.status.busy":"2025-08-09T01:39:53.198289Z","iopub.execute_input":"2025-08-09T01:39:53.198724Z","iopub.status.idle":"2025-08-09T01:39:53.412644Z","shell.execute_reply.started":"2025-08-09T01:39:53.198698Z","shell.execute_reply":"2025-08-09T01:39:53.411773Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"<br>\nKick and punt plays have historically posed the highest risk of concussion to players. During the 2015-2017 seasons, the kickoff represented only six percent of plays but 12 percent of concussions, making the risk four times greater than running or passing plays. In response, the NFL revised its kicking rules for 2018 to reduce risk during kickoffs. \n\nThe NFL is now increasing the attention given to punt plays. According to NFL executive Jeff Miller, concussion risk during punts is twice that of running or passing plays. In response the NFL is sponsoring this competition as part of an overall effort to make punt plays safer. The goal of the competition is to discover specific rule modifications, supported by data, that may reduce the occurrence of concussions during punt plays.","metadata":{"_uuid":"6b4fcb219d8c42ac7a91e850f1e1cac04027c47b"}},{"cell_type":"markdown","source":"<a id='mt'></a>\n# Methodology\n\n<p style=\"margin-top: 20px\">I followed a rigorous methodology designed to help solve problems like the one posed in this challenge. The figure below shows the steps and the tools used for each step. The last step is shown for completeness only since it was out of scope for this analysis.</p>\n\n<span style=\"display:inline-block; margin-top:30px; margin-left:60px; font-size:12pt; font-weight:bold\">Problem Solving Steps and Tools </span>\n<img src=\"https://s3.amazonaws.com/nonwebstorage/headstrong/problemsolve2.png\" width=\"700\">\n\nIn practice, my path through the steps was not completely linear as the figure suggests. I iterated across steps as the analysis progressed.\n\nI used machine learning for part of the analysis in addition to conventional analytics. I applied a type of modeling called *Unsupervised Machine Learning*. Such models are well-suited for uncovering patterns hidden in complex data. The model works by grouping \"like with like\", i.e., punt plays of a similar nature as they appear in the data.","metadata":{"_uuid":"f3a11f01cd03eeddb934903117c44078fc69bdd3"}},{"cell_type":"markdown","source":"<a id='an'></a>\n# Analysis\n\n<div class=\"h3\"> UNDERSTANDING THE PROBLEM </div>\nMy first task was to explore the problem presented by the NFL. I researched information on football and sports safety to improve my understanding. Resources included the following items:\n\n  - Sports articles on punt plays and players\n  - NFL Official Rules\n  - Videos and game highlights\n  - NFL press releases, articles and general statistics\n  - Information on NFL Play Smart, Play Safe to include past and current initiatives\n  - Information from other sports and football leagues addressing concussions  \n  - Articles and research papers on sports-related concussions \n  \n  \nI also needed to get an independent idea of concussion risk for punt plays relative to other plays. I first analyzed data provided for the competition along with data scraped from nfl.com and concussion data compiled by IQVIA. The table below shows the percentage of concussions for passing and running plays vs. the percentage from punt plays.","metadata":{"_uuid":"c4e8ed148a3203ddd999a2fb75e24bc3f3ff12b9"}},{"cell_type":"code","source":"def get_sums(file, collist):\n    df = pd.read_csv(file, usecols=[\"Year\"] + collist)\n    return [df.loc[df.Year == y, collist].sum().sum() for y in years]\n\n\nyears = [2016, 2017]\nscrims = get_sums(\n    \"../input/nfl-competition-data/nflcom_scrims.csv\", [\"Scrm Plys\"]\n)\npunts = get_sums(\n    \"../input/nfl-competition-data/nflcom_punts.csv\", [\"Punts\", \"Blk\"]\n)\n\npunt_concussions = (\n    pd.read_csv(\n        \"../input/NFL-Punt-Analytics-Competition/video_review.csv\",\n        usecols=[\"Season_Year\"],\n    )\n    .Season_Year.value_counts()\n    .values\n)\n\nKICK_CONC_PCT = 0.12  # source: Kaggle competition Overview\nscrimmage_concussions = (\n    np.array([217, 235]) * (1 - KICK_CONC_PCT) - punt_concussions\n)\n\npcts_df = pd.DataFrame(\n    {\n        \"Passes_Runs\": (scrimmage_concussions / scrims),\n        \"Punts\": punt_concussions / punts,\n    },\n    index=years,\n)\npcts_df[\"Risk_Multiple\"] = (pcts_df.Punts / pcts_df.Passes_Runs).apply(\n    \"{:.1f}\".format\n)\npcts_df[\"Passes_Runs\"] = pcts_df.Passes_Runs.apply(\"{:.2%}\".format)\npcts_df[\"Punts\"] = pcts_df.Punts.apply(\"{:.2%}\".format)\ndisplay(\n    HTML(\n        '<span style=\"font-weight:bold\">'\n        + \"Concussion Percentages by Play Type\"\n        + \"</span>\"\n    ),\n    pcts_df,\n)\n","metadata":{"_kg_hide-input":true,"trusted":true,"_uuid":"b98d38440e27c1de1b451ca1dadeb461fae28032","execution":{"iopub.status.busy":"2025-08-09T01:39:53.413502Z","iopub.execute_input":"2025-08-09T01:39:53.413786Z","iopub.status.idle":"2025-08-09T01:39:53.449541Z","shell.execute_reply.started":"2025-08-09T01:39:53.413760Z","shell.execute_reply":"2025-08-09T01:39:53.448588Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"According to these approximate calculations, punt plays were 1.2 to 1.4 times more dangerous than pass and run plays in 2016 and 2017. These numbers include all punt plays to include out of bounds punts and down punts. I show later in the analysis that punt plays carry a higher risk multiple when the ball stays in play.","metadata":{"_uuid":"11ff18cde0babe18644979df8f50f0c0981ca5a5"}},{"cell_type":"markdown","source":"I broke the issue down into simpler pieces using a logic tree. The tree helped identify areas that could be addressed with targeted data analysis in search of root causes. I also used the tree to prioritize areas and make the best use of the allotted time. Rather than start with general data exploration looking for patterns, I focused my number crunching in specific areas. The figure below shows the logic tree with numbered circles to indicate each issue's assigned priority. I assigned priorities based on the potential impact and the availability of data for each opportunity.\n\n\n<span style=\"display:inline-block; margin-top:30px; margin-left:84px; font-size:12pt; font-weight:bold\">Logic Tree</span>\n<img src=\"https://s3.amazonaws.com/nonwebstorage/headstrong/issuetree3.png\" width=\"650\">\n\n","metadata":{"_uuid":"a344101b0274e4e8aec7eb8820b970a6b7d67349"}},{"cell_type":"markdown","source":"The following section describes my in-depth analysis of Opportunity Areas 1 and 2. My analysis in Opportunity Area 3 did not result in any recommendations. It is included in the Appendix. \n<hr>","metadata":{"_uuid":"4fa9d0cd807742e00d61ea92e0df76980f448c3a"}},{"cell_type":"markdown","source":"<div class=\"h3\"> IDENTIFYING CORRELATIONS AND CAUSES </div>\n\n<div class=\"h4\"><span style=\"color:#DD0000\"> Opportunity Area 1. </span>  Fewer 'live' returns.</div>\n\nFor this opportunity I looked at the data to see if concussion rates differed by the various outcomes possible for a punt play. For instance, a punt that quickly goes out of bounds typically presents less risk to players than a long return.  I used the following classifications for punt outcomes:\n    \n  - Returned\n  - Fair Catch\n  - Touchback\n  - Downed\n  - Out of Bounds\n  - Not Punted (inlcudes blocks and fakes)\n  \nI classified each of the 6,681 punt plays using the play's text description provided in the data. Below is a sample of play descriptions:","metadata":{"_uuid":"b4cad9718100c9091305e5c6378796828aa33bee"}},{"cell_type":"code","source":"descriptions = pd.read_csv(\n    \"../input/NFL-Punt-Analytics-Competition/play_information.csv\",\n    usecols=[\"PlayDescription\"],\n    nrows=50,\n).PlayDescription.tolist()\n\nfor i in range(0, 45, 15):\n    display(\n        HTML(\n            \"<span style='color:steelblue'>\" + descriptions[i] + \"</span>\"\n        )\n    )\n","metadata":{"trusted":true,"_uuid":"a91f85c9bc9ef7c4633e7647d0126056bbecced6","_kg_hide-input":true,"execution":{"iopub.status.busy":"2025-08-09T01:39:53.450525Z","iopub.execute_input":"2025-08-09T01:39:53.450796Z","iopub.status.idle":"2025-08-09T01:39:53.466830Z","shell.execute_reply.started":"2025-08-09T01:39:53.450775Z","shell.execute_reply":"2025-08-09T01:39:53.466150Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"One can see from the first description that a single play in the data can actually be two separate plays as experienced by the players. Approximately 200 of 6,681 plays had complex descriptions. I classified these plays in a way that most accurately represented player risk. For example, if an out-of-bounds punt was invalidated by a penalty and then followed by a fair catch, I represented the play as a fair catch. After classifying the plays in this way I compared totals to the events found in the Next Gen Stats (NGS) data. The totals fell within 3% of each other for returns, fair catches, and touchbacks. Downed and out-of-bounds punts differed more, but not to an extent that changed the analysis or conclusions. ","metadata":{"_uuid":"c1220c04283a5f6c41a37c112a19f9d513d12571"}},{"cell_type":"code","source":"plays_all = pd.read_parquet(\"../input/nfl-data-preparation/plays.parq\")\nplays_all = plays_all.set_index([\"GameKey\", \"PlayID\"])\n\noutcomes = pd.DataFrame(\n    {\"PlayDescription\": plays_all.PlayDescription}, index=plays_all.index\n)\npunttypes = {\n    \"not_punted\": [\n        \"no play\",\n        \"delay of game\",\n        \"false start\",\n        \"blocked\",\n        \"incomplete\",\n    ],\n    \"out_of_bounds\": [\"out of bounds\"],\n    \"downed\": [\"downed\"],\n    \"touchback\": [\"touchback\"],\n    \"fair_catch\": [\"fair catch\"],\n    \"returned\": [\"no gain\", \"for (.*) yard\"],\n}\n\nwith warnings.catch_warnings():\n    warnings.simplefilter(\"ignore\")\n    for k, v in punttypes.items():\n        outcomes[k] = outcomes.PlayDescription.str.contains(\n            \"|\".join(v), case=False, regex=True\n        ).astype(int)\n\n\n# Helper for updating typesum after each logic block\ndef update_typesum(df):\n    df[\"typesum\"] = df[list(punttypes.keys())].sum(axis=1)\n\n\nupdate_typesum(outcomes)\n\n# Set not_punted to 0 if \"punts\" in description, else set other types to 0\nmask_punts = outcomes.PlayDescription.str.contains(\n    \"punts\", case=False, regex=False\n)\noutcomes.loc[mask_punts, \"not_punted\"] = 0\noutcomes.loc[~mask_punts, list(punttypes.keys())[1:]] = 0\noutcomes.loc[~mask_punts, \"not_punted\"] = 1\nupdate_typesum(outcomes)\n\n# If no type detected, set returned to 1\noutcomes.loc[outcomes.typesum == 0, \"returned\"] = 1\nupdate_typesum(outcomes)\n\n# Remove fair_catch if invalid fair catch and returned both present\nmask_invalid_fc = outcomes.PlayDescription.str.contains(\n    \"invalid fair catch\", case=False, regex=False\n)\noutcomes.loc[\n    mask_invalid_fc & (outcomes.returned == 1) & (outcomes.typesum == 2),\n    \"fair_catch\",\n] = 0\nupdate_typesum(outcomes)\n\n# Remove not_punted and out_of_bounds if both returned and \"punts\" present\noutcomes.loc[\n    mask_punts & (outcomes.returned == 1) & (outcomes.typesum == 2),\n    [\"not_punted\", \"out_of_bounds\"],\n] = 0\nupdate_typesum(outcomes)\n\n# Remove all but not_punted if blocked\nmask_blocked = outcomes.PlayDescription.str.contains(\n    \"blocked\", case=False, regex=False\n)\noutcomes.loc[mask_blocked, list(punttypes.keys())[1:]] = 0\nupdate_typesum(outcomes)\n\n# If still no type, set not_punted to 1\noutcomes.loc[outcomes.typesum == 0, \"not_punted\"] = 1\nupdate_typesum(outcomes)\n\n# Remove conflicting types for touchback, returned, fair_catch, downed\noutcomes.loc[\n    (outcomes.touchback == 1) & (outcomes.typesum == 2),\n    [\"out_of_bounds\", \"downed\"],\n] = 0\noutcomes.loc[\n    (outcomes.returned == 1) & (outcomes.typesum == 2), \"returned\"\n] = 0\noutcomes.loc[\n    (outcomes.fair_catch == 1) & (outcomes.typesum == 2),\n    [\"out_of_bounds\", \"downed\"],\n] = 0\noutcomes.loc[\n    (outcomes.downed == 1) & (outcomes.typesum == 2), \"out_of_bounds\"\n] = 0\noutcomes.drop([\"PlayDescription\", \"typesum\"], axis=1, inplace=True)\nplays_all[\"outcome\"] = outcomes.dot(outcomes.columns).values  # condense\n\n# get yardage for return plays\nplays_all[\"yardage\"] = plays_all.PlayDescription.str.extract(\n    \"for (.{1,3}) yard\"\n)\nplays_all[\"yardage\"] = plays_all[\"yardage\"].fillna(0)\nplays_all.loc[plays_all.outcome != \"returned\", \"yardage\"] = 0\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-09T01:39:53.467862Z","iopub.execute_input":"2025-08-09T01:39:53.468534Z","iopub.status.idle":"2025-08-09T01:39:53.733597Z","shell.execute_reply.started":"2025-08-09T01:39:53.468503Z","shell.execute_reply":"2025-08-09T01:39:53.732689Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"The figures and table below show that returned punts are by far the most common outcome for both the absolute number of concussions and the percent of concussions for that play type. Fair catches, by contrast are low on both counts. \n\n\n<span class=\"note\"> <i>Hover over the bars to see exact numbers.</i> </span>","metadata":{"_uuid":"d319d25996ec14816c8494b8f88670b58205decd"}},{"cell_type":"code","source":"\n# format data for plotting\ncrosstable = (\n    pd.crosstab(plays_all.outcome, plays_all.concussion)\n    .reset_index()\n    .sort_values(1, ascending=False)\n)\ncrosstable.columns = [\"Play_Outcome\", \"Zero_Concussions\", \"Concussions\"]\ncrosstable[\"Pct_of_Type\"] = (\n    crosstable.Concussions\n    / (crosstable.Concussions + crosstable.Zero_Concussions)\n    * 100\n)\n\nbar_concs_all = crosstable.hvplot.bar(\n    \"Play_Outcome\",\n    \"Concussions\",\n    ylim=(0, 35),\n    rot=45,\n    yticks=np.arange(5, 40, 5).tolist(),\n    width=400,\n    height=300,\n    color=\"lightgray\",\n)\nbar_concs_returned = crosstable[\n    crosstable.Play_Outcome == \"returned\"\n].hvplot.bar(\n    \"Play_Outcome\",\n    \"Concussions\",\n    title=\"Punt Concussions 2016-2017\",\n    color=\"#ffa43d\",\n)\n\nbar_pcts_all = crosstable.hvplot.bar(\n    \"Play_Outcome\",\n    \"Pct_of_Type\",\n    ylim=(0, 1.21),\n    rot=45,\n    yticks=np.arange(0, 1.4, 0.2).tolist(),\n    width=400,\n    height=300,\n    color=\"lightgray\",\n)\nbar_pcts_returned = crosstable[\n    crosstable.Play_Outcome == \"returned\"\n].hvplot.bar(\n    \"Play_Outcome\",\n    \"Pct_of_Type\",\n    title=\"Punt Concussion Pcts 2016-2017\",\n    color=\"#ffa43d\",\n)\n\ndisplay(\n    bar_concs_all * bar_concs_returned + bar_pcts_all * bar_pcts_returned\n)\ncrosstable[\"Pct_of_Type\"] = (crosstable.Pct_of_Type / 100).apply(\n    \"{:.2%}\".format\n)\ncrosstable[\"Play_Outcome\"] = crosstable.Play_Outcome.str.title()\nctable = crosstable.sort_values(\"Concussions\", ascending=False).set_index(\n    \"Play_Outcome\"\n)\n\ndisplay(\n    HTML(\n        '<span style=\"font-weight:bold\">'\n        + \"Comparison of Play Outcomes\"\n        + \"</span>\"\n    ),\n    ctable,\n)\n","metadata":{"_kg_hide-input":true,"trusted":true,"_uuid":"5a0f89cd28df0d6136e8a30c712094b49b65a76c","execution":{"iopub.status.busy":"2025-08-09T01:39:53.734603Z","iopub.execute_input":"2025-08-09T01:39:53.735264Z","iopub.status.idle":"2025-08-09T01:39:54.100711Z","shell.execute_reply.started":"2025-08-09T01:39:53.735231Z","shell.execute_reply":"2025-08-09T01:39:54.099660Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"The NFL would most likely see fewer concussions if some returned punts are shifted to other outcomes by changing the rules. This is consistent with my findings in the next section that over half of the recorded concussons occurred near the punt returner.\n\nAnother important consideration here was to see yards gained from returned punts. The figure below shows yards gained on each punt when a returner gets control of the ball with the intent to move forward. It does not include muffed catches. The vertical line in the figure indicates the median of all returns.","metadata":{"_uuid":"4f524233acaf407658c969761ea4062c03616d07"}},{"cell_type":"code","source":"returns = plays_all.loc[\n    (plays_all.outcome == \"returned\")\n    & (~plays_all.PlayDescription.str.contains(\"MUFFS\")),\n    [\"outcome\", \"yardage\", \"concussion\", \"Rec_team\"],\n].sort_values(\"Rec_team\")\nreturns[\"yardage\"] = returns.yardage.astype(int)\nreturns_median = returns.yardage.median()\n","metadata":{"_kg_hide-input":true,"_kg_hide-output":false,"trusted":true,"_uuid":"307bb9ae5f916df8c82f96bd4569bab76d9190f9","execution":{"iopub.status.busy":"2025-08-09T01:39:54.101747Z","iopub.execute_input":"2025-08-09T01:39:54.102127Z","iopub.status.idle":"2025-08-09T01:39:54.118231Z","shell.execute_reply.started":"2025-08-09T01:39:54.102095Z","shell.execute_reply":"2025-08-09T01:39:54.117298Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import matplotlib as mpl\nimport matplotlib.pyplot as plt\nimport matplotlib.ticker as ticker\nimport seaborn as sns\n\n%matplotlib inline\n\n\nsns.set(rc={\"axes.facecolor\": \"darkseagreen\"})\nwith warnings.catch_warnings():\n    warnings.simplefilter(\"ignore\")\n    mpl.rcParams[\"figure.dpi\"] = 600\n    mpl.rcParams[\"figure.figsize\"] = [9, 6.5]\n    mpl.rcParams[\"ytick.labelsize\"] = 10\n    ax2 = sns.boxplot(\n        x=returns.yardage,\n        y=returns.Rec_team,\n        fliersize=4,\n        whis=20,\n        color=\"#CCCCCC\",\n    )\n\n    for i, artist in enumerate(ax2.artists):\n        artist.set_edgecolor(\"lightgray\")\n        for j in range(i * 6, i * 6 + 6):\n            line = ax2.lines[j]\n            line.set_color(\"gray\")\n            line.set_mfc(\"lightgray\")\n            line.set_mec(\"lightgray\")\n    sns.stripplot(\n        x=returns.yardage,\n        y=returns.Rec_team,\n        alpha=0.9,\n        size=3,\n        jitter=True,\n        color=\"steelblue\",\n    )\n    ax2.set_title(\"Punt Return Yardage\", size=14, loc=\"left\")\n    ax2.set_title(\"2016-2017\", size=14, loc=\"right\")\n    ax2.set_xlim([-15, 100])\n    ax2.xaxis.set_major_locator(ticker.MultipleLocator(10))\n\n    ax2.set_xlabel(\"Yards Gained\", size=10)\n    ax2.set_ylabel(\"Receiving Team\", size=10)\n    plt.axvline(returns_median, linewidth=1, linestyle=\"-\", color=\"red\")\n","metadata":{"_kg_hide-input":true,"trusted":true,"_uuid":"967a8e8707aac2ff1d0e2a8e8c22fb648f01c2a7","execution":{"iopub.status.busy":"2025-08-09T01:39:54.120873Z","iopub.execute_input":"2025-08-09T01:39:54.121188Z","iopub.status.idle":"2025-08-09T01:39:56.907729Z","shell.execute_reply.started":"2025-08-09T01:39:54.121165Z","shell.execute_reply":"2025-08-09T01:39:56.906468Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"The above figure shows that most every team had returns falling largely in the 5-15 yard range. The median return for all teams, all returns was 8 yards. \n\nThe table below shows the percentiles for five-yard intervals as reflected in the figure. For instance, 63% of all punt returns (meeting the criteria stated above) gained 10 yards or less. Returns over 20 yards, which account for most of the excitement on punt returns, occurred only 10% of the time.","metadata":{"_uuid":"d984ff370d648c258df62571be003c2b96cdfdb2"}},{"cell_type":"code","source":"\nyardlist = [0, 5, 10, 15, 20]\ncutpts = [\n    stats.percentileofscore(returns.yardage, yd) / 100 for yd in yardlist\n]\n\npdf = pd.DataFrame(\n    {\"Yards_Gained\": yardlist, \"Total_Pct_of_Returns\": cutpts}\n)\npdf[\"Yards_Gained\"] = pdf.Yards_Gained.astype(str).str.cat(\n    [\"Yards or Less\"] * len(yardlist), sep=\" \"\n)\npdf = pdf.set_index(\"Yards_Gained\")\npdf[\"Total_Pct_of_Returns\"] = pdf.Total_Pct_of_Returns.apply(\n    \"{:.0%}\".format\n)\n\ndisplay(\n    HTML(\n        '<span style=\"font-weight:bold\">'\n        + \"Punt Return Percentages by Yards Gained\"\n        + \"</span>\"\n    ),\n    pdf,\n)\n","metadata":{"_kg_hide-input":true,"trusted":true,"_uuid":"d587b812e8e732aff4da191437a91c5488510489","execution":{"iopub.status.busy":"2025-08-09T01:39:56.908856Z","iopub.execute_input":"2025-08-09T01:39:56.909378Z","iopub.status.idle":"2025-08-09T01:39:56.927289Z","shell.execute_reply.started":"2025-08-09T01:39:56.909353Z","shell.execute_reply":"2025-08-09T01:39:56.926084Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"#### Recommendation\n\nThe above table shows that nearly 2/3 of all returned punts ended in gains of 10 yards or less. A full 1/3 resulted in gains of less than 5 yards. Based on these findings, I recommend changing the rules so that more punts result in a fair catch or other outcome. My recommendation is to spot the ball 10 yards forward of the point where a fair catch is made, adjusting for any penalties. The  intent is to provide a conservative but meaningful alternative to risking concussion in cases where it is highly likely that the runner will only gain a few yards. \n\nThis recommendation and its potential impacts are detailed later in the report.\n\n<hr class=\"light\">","metadata":{"_uuid":"964b9af0eeabefede7696e0ea815ab5560e4cfb1"}},{"cell_type":"markdown","source":"<div class=\"h4\"><span style=\"color:#DD0000\"> Opportunity Area 2. </span>Less Harmful Contact.</div>\n\nFor this issue I looked at the types of collisions that occurred, where they occurred,  and the players involved. The figure below shows the numerous play flows that result in concussion. Players receiving a concussion appear to the left and primary partners appear to the right. The width of the flowpaths indicates the number of concussions occurring with that combination. For example, offensive linemen from the punting team received 9 concussions while tackling. 8 of those were while the returner was tackled, as one might expect. The 9th was from a defensive lineman blocking the offensive lineman during a tackle.\n\n<span class=\"note\"> Hover over flowpaths to see exact values. </span>","metadata":{"_uuid":"7912968240f339a2c18f4688fcb50fbbb4d7c4b2"}},{"cell_type":"code","source":"%%opts Sankey [width=750]\n\nimport holoviews as hv\nfrom holoviews import opts\n\n\nconcussion_df = plays_all[plays_all.concussion == 1].copy()\n\nconcussion_df = (\n    concussion_df\n    .assign(\n        Type_partner=concussion_df.Type_partner.replace(\"Unclear\", \"Unknown\").str.title(),\n        Type_player=concussion_df.Type_player.replace(\"Unclear\", \"Unknown\").str.title() + '_',\n        Player_Activity_Derived=concussion_df.Player_Activity_Derived + '_'\n    )\n)\n\ndef make_sankey(colfrom, colto):\n    conc_table = (\n        concussion_df[~concussion_df.Player_Activity_Derived.isnull()]\n        .groupby([colfrom, colto])[\"PlayDescription\"]\n        .size()\n        .to_frame()\n        .reset_index()\n    )\n    return conc_table.values\n\n\nsankey_cols = [\n    \"Type_player\",\n    \"Player_Activity_Derived\",\n    \"Primary_Partner_Activity_Derived\",\n    \"Type_partner\",\n]\nsankey_list = []\nfor i in range(3):\n    sankey_piece = make_sankey(sankey_cols[i], sankey_cols[i + 1])\n    sankey_list.append(sankey_piece)\n\nsankey_table = np.concatenate(sankey_list)\nc_sankey = hv.Sankey(sankey_table)\ndisplay(\n    HTML(\n        '<span style=\"font-weight:bold; margin-left:84px\">'\n        + \"Concussion Roles and Activities\"\n        + \"</span>\"\n    ),\n    c_sankey,\n)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-09T01:39:56.928216Z","iopub.execute_input":"2025-08-09T01:39:56.928576Z","iopub.status.idle":"2025-08-09T01:39:57.371708Z","shell.execute_reply.started":"2025-08-09T01:39:56.928545Z","shell.execute_reply":"2025-08-09T01:39:57.370851Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"The above diagram shows a variety of scenarios in which concussions occur. One can see by the green box to the left that 20 offensive linemen sustained concussions from tackling and being blocked. Although punt returners received the most concussions as a single-person role, offensive linemen collectively received over half the concussions during the two-season period. Gunners (typically two per play) also collectively received more concussions than most roles.","metadata":{"_uuid":"346174e87bad934f2f582e40451e8ea669783a4d"}},{"cell_type":"markdown","source":"I next reviewed the videos for all concussion plays along with NGS animations for select plays. The Next Gen Stats (NGS) data for 2016 and 2017 include the specific postion of each player during the play at intervals of 0.1 seconds, as well as the player's direction of travel and orientaton. The data also include the events that occurred on the field at each time interval. Below is a small excerpt from the 66 Million rows of data for punt plays.\n","metadata":{"_uuid":"ff979944419d56c5bed8ce17dfc8d641a97987be"}},{"cell_type":"code","source":"from sklearn.manifold import TSNE\nfrom sklearn.preprocessing import StandardScaler\n\n\nngs = (\n    pd.read_parquet(\"../input/nfl-data-preparation/NGS.parq\")\n    .sort_values([\"GSISID\", \"Time\"])\n    .reset_index(drop=True)\n)\nngs_piece = ngs.loc[4281:4285].assign(\n        x = lambda df: df.x.map(\"{:.2f}\".format),\n        y = lambda df: df.y.map(\"{:.2f}\".format),\n        dis = lambda df: df.dis.map(\"{:.3f}\".format)\n    )\nngs_piece","metadata":{"trusted":true,"_uuid":"e7839d6dc66a3b78305419301111dc32c0b6e001","_kg_hide-input":true,"execution":{"iopub.status.busy":"2025-08-09T01:39:57.372602Z","iopub.execute_input":"2025-08-09T01:39:57.372866Z","iopub.status.idle":"2025-08-09T01:40:35.357474Z","shell.execute_reply.started":"2025-08-09T01:39:57.372846Z","shell.execute_reply":"2025-08-09T01:40:35.356556Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"NGS data can be used in many ways to include seeing how plays unfold over time. Below is an animation of an actual punt play in which a lineman from the punting team receives a blindside block from a fast-moving opponent as he closes in on the punt returner. The player receiving the concussion and the partner involved appear as triangles. The play description links to a video of the play for comparison. \n\nFull code for creating the animation is located in the [NFL Punt Play Animation](https://www.kaggle.com/jpmiller/nfl-punt-play-animation) kernel.","metadata":{"_uuid":"324980cce2a0a48c6008549609b0caba2bf595bf"}},{"cell_type":"code","source":"%%HTML \n<a href=\"http://a.video.nfl.com//films/vodzilla/153321/Lechler_55_yd_punt-lG1K51rf-20181119_173634665_5000k.mp4\"> \n    (2:57) (Punt formation) S.Lechler punts 48 yards to TEN 16, Center-J.Weeks. A.Jackson pushed ob at TEN 32 for 16 \n    yards (J.Jenkins).\n</a> \n<img src=\"https://s3.amazonaws.com/nonwebstorage/headstrong/animation_585_733_3.gif\" width=\"650\">","metadata":{"trusted":true,"_uuid":"72111f482e0fbcd6dafacc4adbad66bde1633d22","_kg_hide-input":true,"execution":{"iopub.status.busy":"2025-08-09T01:40:35.358394Z","iopub.execute_input":"2025-08-09T01:40:35.358675Z","iopub.status.idle":"2025-08-09T01:40:35.364874Z","shell.execute_reply.started":"2025-08-09T01:40:35.358653Z","shell.execute_reply":"2025-08-09T01:40:35.363988Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"It's unfortunate there's an injury on the play. On the plus side, however, notice how Jackson (the returner) puts a ninja move on No. 22 as he catches the ball!","metadata":{"_uuid":"10bdb17c9ed6825103639ff21f85ab0adb69ab4b"}},{"cell_type":"markdown","source":"#### Recommendations\nI synthesized the various contact situations into scenarios with the highest potential for harm. For each situation I consider rules changes here in a general way. A detailed discussion and evaluation of recommendations appear later in the report.\n\n*Offensive linemen and gunners blocked downfield.* As the ball is snapped, gunners work to outmaneuver the jammers and get downfield. Within seconds the ball is punted and offensive linemen also run down field, ahead of the defensive line to tackle the punt returner. Offensive linemen and gunners are at risk as defensive players make their way over and back to protect the returner. Players are usually moving at a high rate of speed and focused on the returner, which amplifies the risk. Players are at risk downfield as long as the ball remains in play.\n\nI recommend expanding the current rule prohibiting illegal blindside blocks to include contact below the neck. 6 of the 10 downfield blocks leading to concussion occurred when an offensive player was moving toward the returner and got hit from the side or back. In at least 5 of these cases, the block occurred as a member of the receiving team moved toward or parallel to his own end line. However, contact is permitted below the neck and no penalties were assessed. \n\n   \n*Offensive linemen blocking at the line of scrimmage.* In punt plays, linebackers from the defense are often placed as linemen on the line of scrimmage. Their job is to aggressively rush the punter and block the punt. The offensive line often inlcudes tight ends and running backs with a job usually staffed by a much larger player. The offensive linemen are at risk during the first several seconds of every punt play.\n  \nI have no recommended rule change specifically related to concussions at the line of scrimmage.\n\n\n*Offensive linemen tackling returners.* Although a large proportion of concussions in 2016 and 2017 involved tackling, I have no additional recommendations at this time. The NFL added a rule for 2018 that brings a foul if a player lowers his head to initiate and make contact with his helmet against an opponent. The new rule may very well reduce concussions incurred when tackling.\n","metadata":{"_uuid":"89dc445802d02cb7cde7af44b7d3a89e32fbafbf"}},{"cell_type":"markdown","source":"<div class=\"h3\"> Evaluating Proposed Changes </div>\n\nThere are many rules changes one can consider to reduce concussions associated with punts. A drastic but effective example would be to declare a dead ball wherever the punt lands, goes out of bounds, or is touched by the receiving team. Clearly this would change the nature of the punt play as we know it. I chose a more conservative approach that also took into account ease of implementation and preserving game integrity. The table below lists the potential rules changes I considered along with a summary evaluation.\n\n#### Evaluation Summary of Proposed Changes\n<table class=\"rules\">\n<tr>\n<th>Description</th><th>Reduces Concussions</th><th>Reduces Secondary Risks</th><th>Easy to Enforce</th>\n<th>Preserves Game Integrity</th>\n</tr>\n    \n<tr>\n<td>10-yard forward spot after fair catch</td>\n<td>++++</td>\n<td>+++</td>\n<td>+++</td>\n<td>++</td>\n</tr>\n\n<tr>\n<td>Expand illegal blind-side blocks <br> to include contact below the neck</td>\n<td>++</td>\n<td>+++</td>\n<td>++</td>\n<td>+++</td>\n</tr>\n\n</table>","metadata":{"_uuid":"e7c0aca44061ac459a5f9ef9ce2e5696b1c44b4c"}},{"cell_type":"markdown","source":"The summary ratings above resulted from the following considerations.\n\n<b>Change 1. *Move the ball forward 10 yards after a Fair Catch.*</b> After fair catch of a scrimmage kick, play would start 10 yards forward from the spot of catch, or the succeeding spot after eforcement of penalties. This would apply when the receiving team elects to put the ball in play by a snap.\n\n   - Fair catches had a concussion rate 1/8 that of live returns. Increasing the incentive for a fair catch could reduce risk to both punt returners and offensive linemen downfield. Punt returners may not be as pressured to take a hit in situations where the most likely outcome is at best a 5-10 yard return. Also the play will end sooner which reduces risk to offensive linemen getting hit by defensive linemen running to block for the return. The rule would additionally reduce the risk of tackling which accounted for another 13 concussions, although the new Use of Helmet rule may have already had an impact there.   \n  \n   - Secondary effects should be positive. If the punting team wants to prevent a fair catch they have to kick the ball out of bounds or for a touchback if possible. Both of these outcomes are safer than live returns. \n    \n   - The rule is fairly easy to enforce. Officials already spot the ball after a fair catch according to the rules. With this change they would simply move the ball forward by 10 yards.\n    \n   - The rule would not present a large change to the game. 2/3 of all returned punts ended in a net gain of 10 yards or less so most returns would see no impact. From the fans' standpoint the change would amount to 2-3 more fair catches per game and possibly even more scoring as the receiving team gets better field position. A returner with room to run or exceptional talent could still run the ball and preserve the rare but exciting possibility of a long return. Also, punting strategies have evolved in recent years as leading punters come up with new ways to gain an advantage over the return team. This change could give them added incentive. \n  \n  \n<b>Change 2. *Expand illegal blindside blocks to include forcible contact below the blocked player's neck.*</b> Article 7 lists players in a defenseless posture who are protected from prohibitive contact. The list includes players receiving a blindside block from a blocker whose path is toward or parallel his own end line. However, prohibited contact does not include forcible contact below the neck as long as it is not with the helmet and the blocker does not illegally launch. I recommend expanding the definition of prohibited contact for punt plays to include forcible contact to any part of a player's body, provided he meets the criteria for a defenseless player.\n   \n   - Blindside blocks accounted for at least 5 concussions during the 2016-2017 seasons. In most cases an offensive lineman was maneuvering to intercept a returner and was hit by an approaching defensive lineman moving at a higher rate of speed.\n    \n   - Secondary effects on safety should be positive. Blindside blocks are dangerous even when they are legal.\n    \n   - The rule is relatively easy to enforce. There would need to be additional training for officials, coaches and players as with any rule change. There may be a challenge in seeing all possible fouls and calling penalties given the fast action and chaotic nature of the play.\n    \n   - Emphasis on the rule would generally preserve game integrity. A potential downside is that more penalties would be assessed, slowing down the game.","metadata":{"_uuid":"857b9692eb0b5abdaf6a70490cc216b345660ebb"}},{"cell_type":"markdown","source":"<a id='cn'></a>\n# Conclusion\n\n<p style=\"margin-top: 20px\">In this report I presented my problem solving methodology, analysis, and recommendations. I analyzed three specific areas of opportunity to reduce concussions for punt plays and developed two rules changes. The two changes presented have the potential to eliminate 8 concussions per year. This represents a 40% decrease in concussions from punt plays. The changes would improve player safety, be relatively easy to enforce, and have minimal impact to overall game integrity.</p>","metadata":{"_uuid":"9b3c31442db72c6f28cc1020c2aec67d1b29c6a5"}},{"cell_type":"markdown","source":"<a id='ap'></a>\n# Appendix\n  - Calculations for potential reduction of concussions\n  - Analysis of Opportunity Area 3, Safer Play Strategies\n  \n  <hr>\n\n<p style=\"margin-top: 20px\">The table below shows calculations and assumptions used to estimate the potential reduction in concussions made possible by the recommended rules changes.</p>\n\n<img src=\"https://s3.amazonaws.com/nonwebstorage/headstrong/calcs.png\" align=\"left\">\n\n<hr>","metadata":{"_uuid":"2e093ce7241df1ce60b6f17d64ed5e9673cf279c"}},{"cell_type":"markdown","source":"<div class=\"h4\"><span style=\"color:#DD0000\"> Opportunity Area 3. </span> Safer play strategies.</div>\n*Note: I did not propose any rules changes after analyzing Opportunity Area 3. It is possible that additional exploration could result in insights to play strategy.*\n\n\nFor this opportunity I looked at the data to see how concussion incidents correlated to the play strategies followed by teams. Special teams strategies have many layers and elements. I focused on the relatively few elements where we had data. I captured the following factors:\n\n  - Distance from scrimmage line to goal\n  - Score difference  \n  - Formation of each team at line-up\n  - Location of players on the field during the play\n  - Player speeds\n  - Punt distance\n\nI relied heavily on the NGS data for this part of the analysis. ","metadata":{"_uuid":"2435503358300d799c505778a1023be042d5b131"}},{"cell_type":"markdown","source":"I used the NGS data along with other provided data to analyze the factors listed above. NGS Data was aggregated first for each player, and then for each play. I applied a technique called t-SNE to the aggregated data as a form of unsupervised machine learning. t_SNE compares the factors given and groups like points together in a simple plot.\n\nThe figure below represents a spatial map, or projection, of the various factors onto a 2-D scatter plot. The figure shows  punt plays with different characteristics. There are three groups separate from the main mass, appearing as \"islands\" of points above, right, and below the main mass of points. These are most likely due to the different scenarios that occur during a punt play. For example, a short punt near the goal line looks different than a 60-yard boomer. Formations, the speed of players, and relative location will typically be different.\n\nPlays in which a concussion occurred are shown in orange. To gain insight on what separates plays with concussion events from those without, one would need to see areas of the map without orange dots. In this case the dots are found across the map, including within the separated island areas. \n\n<span class=\"note\">Hover over points to see play descriptions.</span>","metadata":{"_uuid":"a58068bfc70b66c57fb1dded98bcbc41b7b5f035"}},{"cell_type":"code","source":"x_dist = ngs.x - ngs.x.shift(-1), ngs.y - ngs.y.shift(-1)\nngs['speed'] = 10*np.hypot(ngs.x - ngs.x.shift(-1), ngs.y - ngs.y.shift(-1))\n\n# get player-level agg\naggdict_player = {'Time': ['size'],\n                  'x': ['mean', 'max', 'min', 'var'],\n                  'y': ['mean', 'max', 'min', 'var'],\n                  'speed': ['mean', 'max']}\nngs_agg = ngs.groupby(['GameKey', 'PlayID', 'GSISID']).agg(aggdict_player)\nngs_agg.columns = [n[0] + '_' + n[1] for n in ngs_agg.columns]\nngs_agg\n\n\n# get play-level agg\naggdict_play = {'x_mean': ['mean', 'var'],\n                'x_max': ['max', 'var'],\n               'x_min': ['min'],\n                'x_var': ['max', 'mean'],\n                'y_mean': ['mean', 'var'],\n                'y_max': ['max', 'var'],\n               'y_min': ['min'],\n                'y_var': ['max', 'mean'],\n                'speed_mean': ['mean', 'var'],\n                'speed_max': ['max', 'var']}\nngs_agg = ngs_agg.groupby(['GameKey', 'PlayID']).agg(aggdict_play)\nngs_agg.columns = [n[0] + '_' + n[1] for n in ngs_agg.columns]\n\nplays_all['points_ahead'] = np.where(plays_all.Poss_Team == \n        plays_all.HomeTeamCode, plays_all.home_score - plays_all.visit_score,\n        plays_all.visit_score - plays_all.home_score)\n\nplay_cols = ['Type_dlineman_agg',\n             'Type_fullback_agg',\n             'Type_gunner_agg',\n             'Type_jammer_agg',\n             'Type_linebacker_agg',\n             'Type_olineman_agg',\n             'Type_protector_agg',\n             'Type_punter_agg', \n             'dist_togoal',\n             'concussion',\n             'yardage',\n             'points_ahead',\n             'PlayDescription']\n\nplays_strategy = plays_all[play_cols]\n\nngs_agg2 =  ngs_agg.join(plays_strategy, how=\"inner\").replace(-99, -1)\nngs_agg2['yardage'] = pd.to_numeric(ngs_agg2.yardage)\n\ntargets = ngs_agg2[['concussion', 'PlayDescription']]  # for t-sne chart\nngs_agg2 = ngs_agg2.drop(['concussion', 'PlayDescription'], axis=1)\n\nngs_scaled = StandardScaler().fit_transform(ngs_agg2.fillna(0).values)\n\ntsne = TSNE(n_components=2, perplexity=30.0, verbose=1, \n        learning_rate=50, random_state=222)\n\nngs_emb = tsne.fit_transform(ngs_scaled)\n\n","metadata":{"trusted":true,"_uuid":"433ae101b694b84514913e5ce640838161004101","_kg_hide-output":true,"_kg_hide-input":true,"execution":{"iopub.status.busy":"2025-08-09T01:44:58.965283Z","iopub.execute_input":"2025-08-09T01:44:58.966180Z","iopub.status.idle":"2025-08-09T01:45:47.801632Z","shell.execute_reply.started":"2025-08-09T01:44:58.966148Z","shell.execute_reply":"2025-08-09T01:45:47.800821Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"strategy = pd.DataFrame(ngs_emb, columns=[\"x\", \"y\"])\nstrat_annotated = pd.concat(\n    [strategy, ngs_agg2.reset_index(), targets.reset_index(drop=True)],\n    axis=1,\n)\n\nno_conc = strat_annotated[strat_annotated.concussion == 0].hvplot.scatter(\n    \"x\",\n    \"y\",\n    alpha=0.4,\n    size=5,\n    grid=True,\n    hover_cols=[\"PlayDescription\"],\n    height=550,\n    width=700,\n)\nyes_conc = strat_annotated[\n    strat_annotated.concussion == 1\n].hvplot.scatter(\"x\", \"y\", hover_cols=[\"PlayDescription\"], size=30)\n\nno_conc * yes_conc\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-09T01:45:47.803935Z","iopub.execute_input":"2025-08-09T01:45:47.804707Z","iopub.status.idle":"2025-08-09T01:45:48.259109Z","shell.execute_reply.started":"2025-08-09T01:45:47.804680Z","shell.execute_reply":"2025-08-09T01:45:48.258101Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"I concluded that based on the factors listed above, there was nothing about specific punt strategies that seemed to make one type of punt play noticably more dangerous than another. Differences in concussion risk appeared to be more related to the outcome of the punt (returned, fair catch, etc.) A more advanced model based on Recurrent Neural Networks may prove better at detecting patterns across each point in the time interval of the play. ","metadata":{"_uuid":"4cd9f4f7dc70873405d37da238689192eaec3426"}}]}