{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","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"},"kaggle":{"accelerator":"none","dataSources":[{"sourceId":15696,"databundleVersionId":907058,"sourceType":"competition"},{"sourceId":17484,"databundleVersionId":828454,"sourceType":"competition"},{"sourceId":26113332,"sourceType":"kernelVersion"}],"dockerImageVersionId":29844,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"<div class=\"h1\">Reducing Lower Body Injuries</div>\n<img src=\"https://nonwebstorage.s3.amazonaws.com/lowerbody/zeke.jpg\" alt=\"zeke\" height=\"400\" width=\"720\"/>","metadata":{}},{"cell_type":"code","source":"%%HTML\n<style type=\"text/css\">\n\ndiv.h1 {\n    font-size: 32px; \n    margin-bottom:2px;\n}\ndiv.h2 {\n    background-color: steelblue; \n    color: white; \n    padding: 8px; \n    padding-right: 300px; \n    font-size: 24px; \n    max-width: 1500px; \n    margin-top: 50px;\n    margin-bottom:4px;\n}\ndiv.h3 {\n    color: steelblue; \n    font-size: 18px; \n    margin-top: 4px; \n    margin-bottom:8px;\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.lists {\n    font-size: 16; \n    color: dimgray; \n    font-style: bold;\n    vertical-align: top;\n}\nspan.captions {\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,"trusted":true,"execution":{"iopub.status.busy":"2025-08-09T02:55:51.469528Z","iopub.execute_input":"2025-08-09T02:55:51.469880Z","iopub.status.idle":"2025-08-09T02:55:51.477722Z","shell.execute_reply.started":"2025-08-09T02:55:51.469829Z","shell.execute_reply":"2025-08-09T02:55:51.476836Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"! pip install hvplot==0.5.1 lifelines==0.23.7\n# conda update bokeh -y","metadata":{"_kg_hide-input":true,"_kg_hide-output":true,"trusted":true,"execution":{"iopub.status.busy":"2025-08-09T02:55:51.480176Z","iopub.execute_input":"2025-08-09T02:55:51.480486Z","iopub.status.idle":"2025-08-09T02:55:57.126352Z","shell.execute_reply.started":"2025-08-09T02:55:51.480414Z","shell.execute_reply":"2025-08-09T02:55:57.125223Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import warnings\nimport hvplot.pandas\nimport holoviews as hv\nimport matplotlib.pyplot as plt\nimport numpy as np\nimport pandas as pd\nfrom IPython.display import HTML, Image\nfrom bokeh.models import HoverTool\nfrom holoviews import opts\nfrom lifelines import KaplanMeierFitter\nfrom lifelines.statistics import logrank_test\n\nwarnings.filterwarnings(\"ignore\", category=DeprecationWarning)\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]","metadata":{"_kg_hide-input":true,"_kg_hide-output":true,"trusted":true,"execution":{"iopub.status.busy":"2025-08-09T03:01:49.436461Z","iopub.execute_input":"2025-08-09T03:01:49.436795Z","iopub.status.idle":"2025-08-09T03:01:49.444606Z","shell.execute_reply.started":"2025-08-09T03:01:49.436746Z","shell.execute_reply":"2025-08-09T03:01:49.443638Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"The NFL has challenged the data science community to join them in reducing lower body injuries. We were asked to examine the effects of playing on synthetic turf versus natural turf along with other factors that might influence player movements and safety. This report represents my analysis. \n\nBased on the data provided, it is my opinion that playing on synthetic turf over two or more seasons is associated with a higher rate of injury to knees, ankles, and feet. I did not find that a history of playing on synthetic fields was the most important indicator of player injury. The following factors showed more importance in the final model used for this analysis:\n\n  - The amount of rest between games\n  - Lateral changes in speed from cutting or turning\n  - Changes in speed from starting and stopping\n  - Environmental factors of precipitation, temperature and stadium type\n\nI find only a weak connection to the playing surface as measured at the time of injury. In other words, the plays ending in player injury on synthetic turf did not appear different from plays without injuries. Rather it was a player's exposure to the factors over time that correlated with increased risk.\n\nAn interesting finding is that the proportion of games played on synthetic turf varied quite a bit among the 250 players in the sample. It also varies across all 32 NFL teams. The chart below shows the number of games played on each type of turf for the 2018 season. \n\n\n<span class=\"note\"> <i>Hover over the bars to see exact numbers.</i> </span>","metadata":{}},{"cell_type":"code","source":"cols = [\"GameId\", \"Turf\", \"HomeTeamAbbr\", \"VisitorTeamAbbr\"]\ntrain = pd.read_csv(\n    \"../input/nfl-big-data-bowl-2020/train.csv\", usecols=cols\n).drop_duplicates(\"GameId\")\n\ntrain = train[train.GameId.astype(\"str\").str.startswith(\"2017\")]\n\ngrass_labels = [\"grass\", \"natural\"]  # includes hybrids (16 games)\ntrain[\"HomeSynth\"] = np.where(\n    train.Turf.str.lower().str.contains(\"|\".join(grass_labels)), 0, 1\n)\n\ngames = pd.melt(\n    train,\n    id_vars=[\"GameId\", \"HomeSynth\"],\n    value_name=\"Team\",\n    value_vars=[\"HomeTeamAbbr\", \"VisitorTeamAbbr\"],\n).sort_values(\"GameId\")\n\nhomes = (\n    train[[\"HomeTeamAbbr\", \"HomeSynth\"]]\n    .drop_duplicates()\n    .set_index(\"HomeTeamAbbr\")\n)\n\ntotals = games.groupby(\"Team\").agg({\"HomeSynth\": [\"size\", \"sum\"]})\ntotals.columns = [\"Games\", \"Synthetic\"]\ntotals[\"Natural\"] = totals.Games - totals.Synthetic\n\ntotals = totals.join(homes).reset_index().sort_values(\"Natural\")\n\n# plot\nplot_opts = {\n    \"invert\": True,\n    \"height\": 500,\n    \"width\": 320,\n    \"grid\": True,\n    \"line_alpha\": 0,\n    \"ylim\": (0, 16),\n    \"xticks\": np.arange(0, 17, 2).tolist(),\n}\nnatural = totals.hvplot.bar(\n    x=\"Team\",\n    y=\"Natural\",\n    flip_xaxis=True,\n    title=\"2018 Games by Field Surface\",\n    **plot_opts,\n)\nsynth = totals.hvplot.bar(\n    x=\"Team\", y=\"Synthetic\", color=\"orange\", yaxis=\"right\", **plot_opts\n)\ndisplay(natural + synth)\n","metadata":{"_kg_hide-input":true,"trusted":true,"execution":{"iopub.status.busy":"2025-08-09T02:55:59.216956Z","iopub.execute_input":"2025-08-09T02:55:59.217261Z","iopub.status.idle":"2025-08-09T02:56:01.227617Z","shell.execute_reply.started":"2025-08-09T02:55:59.217203Z","shell.execute_reply":"2025-08-09T02:56:01.226686Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"Team experience varied in 2018 between the Oakland Raiders, with 1 game on synthetic turf, and the Atlanta Falcons, with 13 games. I discuss the differences in more detail within the body of the analysis.\n\nMy report contains the following sections:\n  - Background\n  - Methodology\n  - Analysis\n  - Application\n  - Appendix\n  \nThank you and happy reading!\n\nJohn M<br/>","metadata":{}},{"cell_type":"markdown","source":"<a id='bg'></a>\n# Background","metadata":{}},{"cell_type":"markdown","source":"<div class=\"h3\">THE ISSUE</div>\nLower body injuries place a high burden on players, teams, and the league overall. [Earlier this year](https://apnews.com/2fdaa276b87f4a71b248476cf12f3d36), the league's chief medical officer, Dr. Allen Sills, stressed the need to target the lower extremity injuries \"the same was as with concussions.\" \"We are thinking about injury burden,\" Sills said. \"Not only how many injuries but how long a player missed. \n\nThe chart below shows injury counts for torn ligaments in the knee. The figures don't include other types of knee injury, or injury to ankles, feet, etc.","metadata":{}},{"cell_type":"code","source":"tears = pd.DataFrame(\n    {\n        \"Season\": [2016, 2017, 2018],\n        \"ACL Tears\": [29, 21, 24],\n        \"MCL Tears\": [86, 97, 94],\n    }\n)\ndisplay(\n    HTML(\n        '<span style=\"font-weight:bold\">'\n        + \"Knee Injuries, Regular Season Games\"\n        + \"</span>\"\n    ),\n    tears,\n    HTML('<span class=\"captions\">' + \"Source: IQVIA\" + \"<span>\"),\n)","metadata":{"_kg_hide-input":true,"trusted":true,"execution":{"iopub.status.busy":"2025-08-09T02:56:01.229426Z","iopub.execute_input":"2025-08-09T02:56:01.229819Z","iopub.status.idle":"2025-08-09T02:56:01.248493Z","shell.execute_reply.started":"2025-08-09T02:56:01.229747Z","shell.execute_reply":"2025-08-09T02:56:01.247424Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"ACL and MCL tears requiring surgery are particularly damaging to player careers. Players miss the rest of the season and often find that when they do return, their peak performance is less than what it was before. \n\nThe impact of player injuries on team resources is also huge. A [December 2019 article](https://www.espn.com/blog/new-york-jets/post/_/id/81835/the-big-hurt-jets-roster-salary-cap-crushed-by-injuries) by ESPN describes the extreme case of the New York Jets. According to the article, the combined salaries of players on their Injured Reserve list are eating up \\$61.5 million in salary cap space -- roughly 1/3 of the \\$188 million Base Salary Cap set for the NFL. Even though these numbers include injuries other than those to the lower body, it gives an idea of the financial impact.\n\n<hr class=\"light\">","metadata":{}},{"cell_type":"markdown","source":"<div class=\"h3\">RELEVANT FACTORS</div>\n\nThe NFL is interested in the effect of field surface type on lower extremity injuries. Recent studies cited in this challenge show higher injury rates on synthetic turf compared to natural turf. Also of interest are the following factors included in our data:\n  - Stadium type\n  - Weather\n  - Temperature\n  - Rest between games\n  - Play types\n  - Player position\n  - Player movement (as recorded by NGS)\n\nOther factors not included in this challenge have also been linked to lower extremity injuries:\n  - Cleat-turf interaction\n  - Player conditioning\n  - Injury history\n  - Player anatomical, hormonal, neruomuscular factors\n","metadata":{}},{"cell_type":"markdown","source":"<a id='mt'></a>\n# Methodology\n\nI used the following high-level methodology for this analysis:\n\n\n<div>\n<span style=\"color:#445f7c; font-size:16px; line-height:35px;\">\n  1. Examine player survivability relative to field surface <br/>\n  2. Characterize player movement using NGS data<br/>\n  3. Build a machine learning model to identify relevance of factors<br/>\n  4. Examine model factors<br/>\n</span>\n</div>\n","metadata":{}},{"cell_type":"markdown","source":"<a id='an'></a>\n# Analysis","metadata":{}},{"cell_type":"markdown","source":"[](http://)<div class=\"h3\">1. PLAYER SURVIVABILITY</div>\n\nI first looked at player injuries using Survival Analysis. Survival analysis was developed to model the expected time until an event happens for members of a given population. In this case I applied it to estimate how long players might go without injury, and to see if playing on synthetic turf affects the probability of injury. This study presented several challenges to using an approach that is statistically valid: \n\n  - Many players started or stopped playing at various times during the study period. \n  - There is no record of injuries before or after the study period.\n  - Risk is not proportional over time \n\nI used the [Kaplan-Meier estimator](https://en.wikipedia.org/wiki/Kaplan%E2%80%93Meier_estimator) to overcome these challenges. Kaplan-Meier is often used in medical research and was used by a team from Cleveland Clinic in 2018 to model the [effects of concussion on player longevity](https://consultqd.clevelandclinic.org/concussion-in-the-nfl-new-study-demonstrates-significant-detrimental-effects-in-the-short-term/). It is considered to be a reliable indicator when the sample data accurately represent the population.\n\nIn looking at survival rates, I defined \"non-survival\" as missing more than 7 days to injury. That is consistent with the [2018 Mack study](https://www.ncbi.nlm.nih.gov/pubmed/30452873) on the effects of synthetic turf that used injuries with over 8 days missed as a classifier.\n\nI divided the sample of 250 players into two groups: those who played over 50% of all plays on synthetic turf, and those who played less than 50%. The mean percent for all players is 43% so the groups are of roughly equal size. \n\nThe chart below shows the survival rates. For each group you can see the proportion of players who drop out as the number of plays increases form left to right. When one curve is below the other it represents a lower survival rate; i.e., a higher injury rate. ","metadata":{}},{"cell_type":"code","source":"key_cols = [\"PlayerKey\", \"GameID\", \"PlayKey\"]\nplaylist = pd.read_parquet(\n    \"../input/nfl-1standfuture-dataprep/PlayListLabeled.parq\"\n).sort_values(key_cols)\nlast_plays = playlist[playlist.PlayCount == playlist.FinalPlay]\n\n\n# Plot options\nplt.figure(figsize=(12, 8))\nplt.style.use(\"seaborn-whitegrid\")\nSMALL_SIZE = 16\nMEDIUM_SIZE = 20\nBIGGER_SIZE = 24\nplt.rc(\"font\", size=SMALL_SIZE)\nplt.rc(\"axes\", titlesize=MEDIUM_SIZE)\nplt.rc(\"axes\", labelsize=MEDIUM_SIZE)\nplt.rc(\"xtick\", labelsize=SMALL_SIZE)\nplt.rc(\"ytick\", labelsize=SMALL_SIZE)\nplt.rc(\"legend\", fontsize=SMALL_SIZE)\nplt.rc(\"figure\", titlesize=BIGGER_SIZE)\n\n\n# Synthetic turf\nCONFIDENCE = 0.9\nBREAK_PT = 0.5  # mean is 0.43\nidx_ = last_plays.PctPlaysSynthetic < BREAK_PT\n\ndurations_1 = last_plays.loc[idx_, \"FinalPlay\"]\ninjuries_1 = last_plays.loc[idx_, \"Missed7Days\"]\nkmf1 = KaplanMeierFitter()\nkmf1.fit(\n    durations_1,\n    injuries_1,\n    alpha=(1 - CONFIDENCE),\n    label=f\"< {BREAK_PT:.0%} of Plays on Synthetic\",\n)\na1 = kmf1.plot()\n\ndurations_2 = last_plays.loc[~idx_, \"FinalPlay\"]\ninjuries_2 = last_plays.loc[~idx_, \"Missed7Days\"]\nkmf2 = KaplanMeierFitter()\nkmf2.fit(\n    durations_2,\n    injuries_2,\n    alpha=(1 - CONFIDENCE),\n    label=f\"> {BREAK_PT:.0%} of Plays on Synthetic\",\n)\na2 = kmf2.plot(ax=a1)\n\nplt.ylim(0, 1)\nplt.title(\"Survival Rate of Players\")\nplt.xlabel(\"Number of Plays\")\nplt.ylabel(\"Fraction of Players not Injured\")\nplt.show()\n","metadata":{"_kg_hide-input":true,"trusted":true,"execution":{"iopub.status.busy":"2025-08-09T02:56:01.250148Z","iopub.execute_input":"2025-08-09T02:56:01.250642Z","iopub.status.idle":"2025-08-09T02:56:01.798171Z","shell.execute_reply.started":"2025-08-09T02:56:01.250468Z","shell.execute_reply":"2025-08-09T02:56:01.796980Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"The solid lines represent the observed survival rate for each group. The shaded areas represent the 90% confidence interval. Confidence intervals that do not overlap indicate a meaningful difference between the groups. In this case the lines are separate, indicating a higher injury rate for players in the sample with over 50% of plays on synthetic turf. The difference is most noticeable prior to players reaching 500 plays. 80% of players playing mostly on synthetic turf make it to their 250th play, whereas 80% of players playing mostly on nautral turf make it to their 500th play.\n\nOn the other hand, the shaded confidence intervals overlap. This indicates the difference observed in our sample of 250 players may not accurately represent the entire NFL player population.\n\nI used a statistical test and calculated the p-value to more formally assess the relevance of synthetic turf. A p-value can be thought of the probability that our observed difference is due to random chance. Because we are using a sample of 250 players instead of all players, such an outcome is possible. The tables below show the results of the formal test with a p-value of 0.34.\n","metadata":{}},{"cell_type":"code","source":"results_turf = logrank_test(\n    durations_1, durations_2, injuries_1, injuries_2, alpha=0.90\n)\nresults_turf.print_summary()","metadata":{"_kg_hide-input":true,"trusted":true,"execution":{"iopub.status.busy":"2025-08-09T02:56:01.799744Z","iopub.execute_input":"2025-08-09T02:56:01.800113Z","iopub.status.idle":"2025-08-09T02:56:01.866197Z","shell.execute_reply.started":"2025-08-09T02:56:01.800057Z","shell.execute_reply":"2025-08-09T02:56:01.865205Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"A p-value of 0.34 is higher than the widely accepted maximum value of 0.05. The data does not strictly support the idea that players with more plays on synthetic turf have a higher injury rate.\n\nEven so, this doesn't mean that the effect of playing on synthetic turf is nowhere to be seen. To give this number some perspective, think of a weather person telling you it will rain today. She thinks it will rain and admits there's a 34% probability her conclusion is based on random chance. You probably wouldn't bet a day's pay on her being right about the rain, but you certainly might take an umbrella to work. There's still value in her forecast, just as with this one.\n\n<hr class=\"light\">","metadata":{}},{"cell_type":"markdown","source":"<div class=\"h3\">2. CHARACTERIZING MOVEMENT</div>\n\nKaplan-Meier estimators work very well for assessing factors one at a time. They are not able to discover interactions among factors. Machine learning models, on the other hand, are very good at it. Before building the actual model I looked at player movement data and captured it in a way that is useful for machine learning.\n\nThe Next Generation Stats (NGS) system captures player position and orientation 10 times per second. The data provided for this challenge consisted of 76 million snapshots covering 267,000  plays. I used these base measurements to calculate speed, direction of travel, and acceleration. My objective was to transform these metrics into a useful form that relate to injury-causing forces. \n\nThe video below shows wide receiver Jarvis Landry practicing a route from when he was with the Miami Dolphins. Landry accelerates straight ahead and then cuts to the left.\n\n<span class=\"note\"> <i>Play the video to see Landry's skills.</i> </span>","metadata":{}},{"cell_type":"code","source":"from IPython.display import Video\n\ndisplay(Video(\"https://i.imgur.com/H2nvXRc.mp4\"))","metadata":{"_kg_hide-input":true,"trusted":true,"execution":{"iopub.status.busy":"2025-08-09T02:56:01.867548Z","iopub.execute_input":"2025-08-09T02:56:01.867796Z","iopub.status.idle":"2025-08-09T02:56:01.873545Z","shell.execute_reply.started":"2025-08-09T02:56:01.867756Z","shell.execute_reply":"2025-08-09T02:56:01.872421Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"I used information from the field of kinesiology - the study of human movement - to derive relevant measurements. Knees and ankles are generally designed to move a person forward. They can take heavier stress in the forward-backward direction and in the up-down direction than in a sideway direction. Because of this difference, I decomposed the acceleration vector at each point into longitudinal acceleration (forward-backward) and lateral acceleration (sideway). The diagram below shows the forces acting on Landry's body, particularly on the leg in contact with the ground, as he makes the cut.\n\n\n<picture of landry with vectors>\n<img src=\"https://nonwebstorage.s3.amazonaws.com/lowerbody/landry_grid2b.png\" alt=\"landry\" height=\"420\" width=\"720\" />\n\n\n\nThe two components of acceleration have these characteristics at every point along a player's path. \n  - Longitudinal acceleration is proportional to changes in player speed. A player speeding up or slowing down experiences acceleration in the direction of travel.\n  - Lateral acceleration is proportional to changes in speed and in a player's direction. A player turning and running in another direction experiences lateral acceleration. Both speed and change in direction contribute to lateral acceleration.\n\n\n\nThe top chart below shows the movement pattern of a single player on one play as seen from above. The play is shown from when the ball is snapped until the play ends. The x-y plot represents the path traveled with dots at each 1/10 of a second. The two plots after show longitudinal and lateral accelerations during the play.\n","metadata":{}},{"cell_type":"code","source":"tracks = pd.read_parquet(\n    \"../input/nfl-1standfuture-dataprep/PlayerTrackData.parq\"\n)\n\none_play = tracks[\n    (tracks.PlayerKey == 35611)\n    & (tracks.GameID == 7)\n    & (tracks.PlayKey == 42)\n].copy()\n\ndisplay(\n    one_play.hvplot.scatter(\n        x=\"x\",\n        y=\"y\",\n        color=\"darkgray\",\n        # , xlim=(0,120), ylim=(0,54)\n    ),\n    one_play.hvplot.line(x=\"time\", y=\"AccelLong\", height=200),\n    one_play.hvplot.line(\n        x=\"time\", y=\"AccelLateral\", height=200, color=\"red\"\n    ),\n)","metadata":{"_kg_hide-input":true,"trusted":true,"execution":{"iopub.status.busy":"2025-08-09T02:56:23.640934Z","iopub.execute_input":"2025-08-09T02:56:23.641273Z","iopub.status.idle":"2025-08-09T02:56:25.702740Z","shell.execute_reply.started":"2025-08-09T02:56:23.641224Z","shell.execute_reply":"2025-08-09T02:56:25.701863Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"When play begins, acceleration is typically in line with the direction of movement as a player speeds up (longitudinal). When the player reaches running speed, and changes direction, there is lateral acceleration across the direction of movement. If the player also changes speed during a turn, longitudinal acceleration occurs as well.\n\nPlayers also experience acceleration in the up-down direction and angular acceleration from twisting the body. I discuss treatment of these variables and my assumptions in the Appendix. I also include details on the calculation of longitudinal and lateral acceleration.\n\nTo better compare movement patterns across players, I standardized the NGS data. Below are standardized movements for six players. Distances and play time are normalized to a scale of 0 to 1. Also, routes are standardized to go from left to right. Actual magnitudes for distance, time, and acceleration are given in the reference table.\n\n<span class=\"note\"> <i>Hover over points and lines on the chart to see more information.</i> </span>\n","metadata":{}},{"cell_type":"code","source":"# Grab some players for a demo\ngrabs = [46038, 43518, 40405, 41209, 43532, 44165]\ntracks2 = tracks[tracks.PlayerKey.isin(grabs)].reset_index(drop=True)\n\ntracks2[\"LastPlay\"] = tracks2.groupby(key_cols[0:2])[\"PlayKey\"].transform(\n    \"max\"\n)\ntracks2[\"LastGame\"] = tracks2.groupby(\"PlayerKey\")[\"GameID\"].transform(\n    \"max\"\n)\ntracks2 = tracks2[\n    (tracks2.GameID == tracks2.LastGame)\n    & (tracks2.PlayKey == tracks2.LastPlay)\n]\n\n# Create reference table\naggdict = {\n    \"dis\": \"sum\",\n    \"time\": \"max\",\n    \"AccelLateral\": \"max\",\n    \"AccelLong\": \"max\",\n}\ntablecols = [\n    \"PlayerKey\",\n    \"dis\",\n    \"time\",\n    \"AccelLateral\",\n    \"AccelLong\",\n    \"RosterPosition\",\n    \"FieldType\",\n    \"PlayType\",\n    \"BodyPart\",\n    \"DaysMissed\",\n]\ntracks_table = (\n    tracks2.groupby(key_cols)\n    .agg(aggdict)\n    .merge(playlist, how=\"left\", right_on=key_cols, left_index=True)[\n        tablecols\n    ]\n    .round(3)\n    .sort_values(\"DaysMissed\", ascending=False)\n)\n\n# Explore\n# pd.set_option(\"display.max_rows\", 250)\n# tracks_table.reset_index()\n\n\n# Scale all player movement to 0-1\ndef scaler(series_name):\n    player_min = tracks2.groupby(\"PlayerKey\")[series_name].transform(min)\n    player_max = tracks2.groupby(\"PlayerKey\")[series_name].transform(max)\n    grouped = tracks2.groupby(\"PlayerKey\")[series_name].transform(min)\n    scaled = (tracks2[series_name] - player_min) / (\n        player_max - player_min\n    )\n    return scaled\n\n\n# series_names = ['x', 'y', 'AccelLateral', 'AccelLong', 'time']\nseries_names = [\"x\", \"y\", \"time\"]\nscaled_names = [name + \"_scaled\" for name in series_names]\n\nfor name_pair in zip(series_names, scaled_names):\n    tracks2[name_pair[1]] = scaler(name_pair[0])\n\n\n# Standardize movement from left to right\ntracks2[\"x_first\"] = tracks2.groupby(\"PlayerKey\")[\"x\"].transform(\"first\")\ntracks2[\"x_last\"] = tracks2.groupby(\"PlayerKey\")[\"x\"].transform(\"last\")\ntracks2.loc[tracks2.x_last < tracks2.x_first, \"x_scaled\"] = (\n    1 - tracks2.x_scaled\n)\n\ntracks2 = tracks2.assign(\n    AccelLateral = tracks2.AccelLateral.round(2),\n    AccelLong = tracks2.AccelLong.round(2),\n    x = tracks2.x.round(0).astype(int),\n    y = tracks2.y.round(0).astype(int)\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-09T03:00:18.007827Z","iopub.execute_input":"2025-08-09T03:00:18.008261Z","iopub.status.idle":"2025-08-09T03:00:18.313091Z","shell.execute_reply.started":"2025-08-09T03:00:18.008202Z","shell.execute_reply":"2025-08-09T03:00:18.312190Z"},"_kg_hide-input":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Grid of plots\n\n# Make table element\nplays = hv.Table(tracks_table)\n\n# Make plot elements\nnumbers = tracks_table.PlayerKey\npositions = tracks_table.RosterPosition\nlayout_list = []\nfor playa, pos in zip(numbers, positions):\n    label_string = f\"{playa} {pos}\"\n    # Create plot elements\n    pts = hv.Points(\n        tracks2[tracks2.PlayerKey == playa],\n        kdims=[\"x_scaled\", \"y_scaled\"],\n        vdims=[\"x\", \"y\", \"time_scaled\"],\n        label=label_string,\n    )\n    tooltips_pts = [(\"X Scaled\", \"@x_scaled\"), (\"Y Scaled\", \"@y_scaled\")]\n    hover_pts = HoverTool(tooltips=tooltips_pts)\n\n    longitudinal = hv.Curve(\n        tracks2[tracks2.PlayerKey == playa],\n        kdims=[\"time_scaled\", \"AccelLong\"],  #####\n        vdims=[\"x_scaled\", \"y_scaled\"],\n        label=\"Longitudinal\",\n    )\n    lateral = hv.Curve(\n        tracks2[tracks2.PlayerKey == playa],\n        kdims=[\"time_scaled\", \"AccelLateral\"],  #####\n        vdims=[\"x_scaled\", \"y_scaled\"],\n        label=\"Lateral\",\n    )\n    tooltips_cv = [\n        (\"X Original\", \"@x\"),\n        (\"Y Original\", \"@y\"),\n        (\"Time Scaled\", \"@time_scaled\"),\n    ]\n    hover_cv = HoverTool(tooltips=tooltips_cv)\n\n    # Overlay and listify\n    accel = (longitudinal * lateral).opts(show_legend=False)\n\n    layout_list.append(pts)\n    layout_list.append(accel)\n\nneworder = [0, 2, 4, 1, 3, 5, 6, 8, 10, 7, 9, 11]\nlayout_list = [layout_list[i] for i in neworder]\nlayout = (\n    hv.Layout(layout_list)\n    .opts(\n        opts.Points(\n            width=250,\n            height=220,\n            size=4,\n            color=\"darkgray\",\n            show_grid=True,\n            tools=[hover_cv],\n        ),\n        opts.Curve(\n            width=250,\n            height=120,\n            line_width=1,\n            ylabel=\"Acceleration\",\n            ylim=(0, 10),\n            yticks=[0, 5, 10],\n            tools=[hover_pts],\n        ),\n    )\n    .cols(3)\n)\n\n# Show\ndef highlight_max(s):\n    is_large = s.nlargest(2).values\n    return [\n        \"background-color: #ffffcc\" if v in is_large else \"\" for v in s\n    ]\n\n\ndef highlight_hurt(s):\n    return [\"background-color: #ffb3b3\" if v > 0 else \"\" for v in s]\n\n\ndisplay(\n    tracks_table.reset_index(drop=True)\n    .style.apply(highlight_max, subset=[\"AccelLong\", \"AccelLateral\"])\n    .apply(highlight_hurt, subset=\"DaysMissed\"),\n    layout,\n)\n","metadata":{"_kg_hide-input":true,"trusted":true,"execution":{"iopub.status.busy":"2025-08-09T03:00:24.235519Z","iopub.execute_input":"2025-08-09T03:00:24.235830Z","iopub.status.idle":"2025-08-09T03:00:26.110746Z","shell.execute_reply.started":"2025-08-09T03:00:24.235787Z","shell.execute_reply":"2025-08-09T03:00:26.109663Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"In the sample charts above, it can be hard to pick out differences between the 3 injured players in the top row and the 3 non-injured players in the bottom row. Sometimes there appears to be a difference such as with the player in the top right graph, 46038 Safety. Notice the red line representing lateral acceleration. The player reaches peak lateral acceleration of 6.5 yards/second per second and maintains it throughout the turn. That's the equivalent of sustaining over 1/2 a g-force for 5 seconds!\n\nAt the same time, other play charts are less clear, 41209 Linebacker in the lower left experiences the highest lateral acceleration of the group and is on synthetic turf. He doesn't get hurt. \n\nThe most appropriate use of these charts would be in conjunction with a lookup tool. For instance, teams could pull a player's play history over the last several games following an injury, focusing on the same type of play. They might also compare the player's movement patterns to another player who played the same position without getting hurt.\n\n<hr>","metadata":{}},{"cell_type":"markdown","source":"[](http://)<div class=\"h3\">3. MACHINE LEARNING</div>\n\nOne challenge in building a model for machine learning with the dataset is identifying the temporal aspect of the injury when it occurs. As someone who tore his ACL and endured numerous ankle sprains playing sports, I understand the potential for both sudden injuries and injuries that come from stress accumulated over time. \n\nTo account for both types of injuries, I used these steps:\n  - Aggregate the movement metrics over the course of a play, recording maximum and cumulative values.\n  - Merge the play-level data to other play characteristics and injuries including turf type, weather, days missed, etc.\n  - Aggregate play-level data by player, again using averages, maximums, and percentiles of play conditions \n  \nUsing these steps is a common technique to encapsulate lower-level data into a higher level. Given the nature of player injuries, and the goal of improving safety on a per-player basis, a model focused on player history was most appropriate.\n\nIt was important to avoid factors that introduce leakage to the model, meaning anything that hints at whether or not a player was injured. For instance, including the number of plays as a factor allows the algorithm to learn that players only playing a few plays in total were probably injured. Likewise, I chose not to use play type or player position. The data reflects what is already known - certain combinations of plays and positions are more likely to result in injury.\n\nThe table below shows the factors provided as inputs to the machine learning algorithm. As with the survivability analysis and the [Mack study](https://www.ncbi.nlm.nih.gov/pubmed/30452873) mentioned earlier, I defined the target variable by injuries with over 7 days missed.","metadata":{}},{"cell_type":"code","source":"tracks = pd.read_parquet(\n    \"../input/nfl-1standfuture-dataprep/PlayerTrackData.parq\"\n)\n\n# Aggregate NGS data by play\nkey_cols = [\"PlayerKey\", \"GameID\", \"PlayKey\"]\naggdict = {\n    \"time\": [\"min\", \"max\"],\n    \"dis\": [\"sum\"],\n    \"VelocityIn\": [\"max\"],\n    \"AccelLateral\": [\"sum\", \"max\"],\n    \"AccelLong\": [\"sum\", \"max\"],\n}\ntracks_agg = tracks.groupby(key_cols).agg(aggdict)\ntracks_agg.columns = [c[0] + \"_\" + c[1] for c in tracks_agg.columns]\ntracks_agg[\"PlayDuration\"] = tracks_agg.time_max - tracks_agg.time_min\ntracks_agg = tracks_agg.fillna(\n    0\n).reset_index()  # should not be any na's now\n\n# Merge NGS data with other play factors\nplaylist = playlist.drop_duplicates(key_cols)\ntracks_agg = tracks_agg.merge(playlist, how=\"inner\", on=key_cols)\n\n# Null out season breaks and long injuries contained in DaysRest\ntracks_agg.loc[tracks_agg.DaysRest > 11, \"DaysRest\"] = (\n    np.nan\n)  # prevents leakage\n\n# Get targets\ntracks_agg[\"PlayerOut7Days\"] = tracks_agg.groupby(\"PlayerKey\")[\n    \"Missed7Days\"\n].transform(max)\ntracks_agg[\"PlayerOut1Day\"] = tracks_agg.groupby(\"PlayerKey\")[\n    \"Missed1Day\"\n].transform(max)\n\n# Aggregate play-level data by player\naggdict_words = {\n    \"RosterPosition\": [\"last\"],\n    \"FieldType\": [\"last\"],\n    \"PlayType\": [\"last\"],\n}\naggdict_nums = {\n    \"PctWetWeather\": [\"mean\"],\n    \"PctOpenStadium\": [\"mean\"],\n    \"Temperature\": [\"mean\"],\n    \"DaysRest\": [\"mean\"],\n    \"AccelLateral_max\": [\"mean\"],\n    \"AccelLong_max\": [\"mean\"],\n    \"PctPlaysSynthetic\": [\"last\"],\n    \"PlayerOut7Days\": [\"last\"],\n}\ndf_words = (\n    tracks_agg.groupby(\"PlayerKey\")\n    .agg(aggdict_words)\n    .reset_index(drop=True)\n)\ndf_nums = (\n    tracks_agg.groupby(\"PlayerKey\")\n    .agg(aggdict_nums)\n    .reset_index(drop=True)\n)\n\nplays_agg = pd.concat([df_words, df_nums], axis=1)\nplays_agg.columns = [c[0] + \"_\" + c[1] for c in plays_agg.columns]\n\n\n# %%\ndisplay(plays_agg.drop(columns=\"RosterPosition_last\").head())\n","metadata":{"_kg_hide-input":true,"trusted":true,"execution":{"iopub.status.busy":"2025-08-09T03:00:43.344186Z","iopub.execute_input":"2025-08-09T03:00:43.344555Z","iopub.status.idle":"2025-08-09T03:00:47.934168Z","shell.execute_reply.started":"2025-08-09T03:00:43.344495Z","shell.execute_reply":"2025-08-09T03:00:47.933286Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"Note that orientation - the direction a player was facing - was not used in this study. There is an issue with inconsistencies between years where the sensor data was 90 degreees out from the correct value. The issue is documented by Michael Lopez, Director of Football Data & Analytics in [this post](https://www.kaggle.com/c/nfl-big-data-bowl-2020/discussion/112303).\n\nBefore identifying which of the above factors are most relevant to lower-body injuries, it is important to make the most accurate model possible. I used a high-performing machine learning algorithm known as [Extreme Gradient Boosting](https://en.wikipedia.org/wiki/XGBoost) as the basis for a classifier model. As with other machine learning algorithms of this type, it works by playing the Hot and Cold game with itself. The program first searches for patterns to learn what injured players look like as reflected in the data. Then it applies the pattern to data it has not seen and guesses which of these players are injured. After recording where it was correct, it searches for patterns again and makes a new prediction. If the new prediction is better, the model \"gets hotter\" and continues down that path. If the new prediction is \"colder\", the algorithm changes course and uses the factors a different way to find better patterns.\n\nThe intial model scored 0.65 AUC using the factors above as inputs. [AUC](https://en.wikipedia.org/wiki/Receiver_operating_characteristic), which stands for Area under the Curve, is a common way to measure performance for classifiers. Here the score measures the model's ability to correctly separate players injured for more than seven days from those who were not. A score of 0.5 can be had by choosing players at random and lining them up by the likelihood they were injured. A score of 1.0 is perfect meaning that the groups are correctly ranked and separated.","metadata":{}},{"cell_type":"code","source":"\n# Xgboost \n\nimport shap\nimport xgboost as xgb\nfrom category_encoders import OrdinalEncoder, WOEEncoder, TargetEncoder\nfrom sklearn.metrics import roc_auc_score\nfrom sklearn.model_selection import StratifiedKFold\n\nshap.initjs()\n\n\noe = OrdinalEncoder()\ny_initial = plays_agg.pop(\"PlayerOut7Days_last\")\nX_initial = oe.fit_transform(plays_agg)\nprint(X_initial.columns)\nxgbdata = xgb.DMatrix(data=X_initial, label=y_initial.to_numpy())\n\nparams = {\n    \"objective\": \"binary:logistic\",\n    \"eval_metric\": \"auc\",\n    \"eta\": 0.03,\n    \"max_depth\": 4,\n}\ncv_scores = xgb.cv(\n    dtrain=xgbdata,\n    params=params,\n    nfold=5,\n    num_boost_round=1800,\n    early_stopping_rounds=60,\n    verbose_eval=False,\n    as_pandas=True,\n    seed=135,\n)\n\nbest_round = cv_scores[\"test-auc-mean\"].idxmax()\nbest_score = cv_scores[\"test-auc-mean\"].max()\n","metadata":{"_kg_hide-input":true,"_kg_hide-output":true,"trusted":true,"execution":{"iopub.status.busy":"2025-08-09T03:00:50.887973Z","iopub.execute_input":"2025-08-09T03:00:50.888323Z","iopub.status.idle":"2025-08-09T03:00:51.417063Z","shell.execute_reply.started":"2025-08-09T03:00:50.888262Z","shell.execute_reply":"2025-08-09T03:00:51.415307Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"print(f'Model Score: {best_score:.2f} AUC after {best_round} iterations.')","metadata":{"_kg_hide-input":true,"trusted":true,"execution":{"iopub.status.busy":"2025-08-09T03:00:53.388475Z","iopub.execute_input":"2025-08-09T03:00:53.388801Z","iopub.status.idle":"2025-08-09T03:00:53.399560Z","shell.execute_reply.started":"2025-08-09T03:00:53.388752Z","shell.execute_reply":"2025-08-09T03:00:53.398588Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"I improved the model using the following techniques:\n\n  - Combined features into pairs to explicitly include interactions\n  - Randomly shuffled features and selected only those contributing to model perfomance\n  - Tuned the parameters of the model to fit the data\n  \nThe results below show the AUC of the improved model.","metadata":{}},{"cell_type":"code","source":"# Optimized Model\n\n# Keep best factors\nfinal_cols = [\n    \"AccelLateral_max_mean\",\n    \"DaysRest_mean\",\n    \"PctWetWeather_mean\",\n    \"AccelLong_max_mean\",\n    \"PctPlaysSynthetic_last\",\n    \"Temperature_mean\",\n    \"PctOpenStadium_mean\",\n    \"FieldType_last\",\n]\nX = plays_agg[final_cols].copy()\ny = y_initial\nte = TargetEncoder(cols='FieldType_last')\nX = te.fit_transform(X, y=y)\n\nparams_final = {\n    \"objective\": \"binary:logistic\",\n    \"eval_metric\": \"auc\",\n    \"colsample_bytree\": 0.7,\n    \"subsample\": 0.4,\n    \"gamma\": 0.1,\n    \"eta\": 0.05,\n    \"max_depth\": 10,\n    \"max_leaves\": 1024,\n    \"grow_policy\": \"depthwise\",\n    \"min_child_weight\": 1,\n    \"lambda\": 1.0,\n    \"alpha\": 0.0,\n    \"scale_pos_weight\": 1.0,\n    \"tree_method\": \"hist\",\n    \"max_bin\": 256,\n    \"seed\": 429,\n}\n\nskf = StratifiedKFold(n_splits=14, random_state=2233)\noof_preds = np.zeros((len(plays_agg)))\nfor train_idx, val_idx in skf.split(X, y):\n    X_train, X_val = X.loc[train_idx], X.loc[val_idx]\n    y_train, y_val = y.to_numpy()[train_idx], y.to_numpy()[val_idx]\n    dtrain = xgb.DMatrix(data=X_train, label=y_train)\n    dval = xgb.DMatrix(data=X_val, label=y_val)\n    watchlist = [(dtrain, \"train\"), (dval, \"eval\")]\n    model_final = xgb.train(\n        params_final,\n        dtrain,\n        num_boost_round=1200,\n        evals=watchlist,\n        early_stopping_rounds=60,\n        verbose_eval=False,\n    )\n    preds = model_final.predict(\n        dval, ntree_limit=model_final.best_ntree_limit\n    )\n    oof_preds[val_idx] = preds\n","metadata":{"_kg_hide-input":true,"_kg_hide-output":true,"trusted":true,"execution":{"iopub.status.busy":"2025-08-09T03:00:58.987691Z","iopub.execute_input":"2025-08-09T03:00:58.988001Z","iopub.status.idle":"2025-08-09T03:00:59.696133Z","shell.execute_reply.started":"2025-08-09T03:00:58.987952Z","shell.execute_reply":"2025-08-09T03:00:59.695295Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"score = roc_auc_score(y, oof_preds)\nprint(f'Optimized Model Score: {score:.2f} AUC.')","metadata":{"_kg_hide-input":true,"_kg_hide-output":false,"trusted":true,"execution":{"iopub.status.busy":"2025-08-09T03:01:01.558576Z","iopub.execute_input":"2025-08-09T03:01:01.558884Z","iopub.status.idle":"2025-08-09T03:01:01.567723Z","shell.execute_reply.started":"2025-08-09T03:01:01.558838Z","shell.execute_reply":"2025-08-09T03:01:01.566709Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"This score lies half-way between random choice and perfection. It is definitely separating the signal from the noise. Identifying factors as relevant based on their importance to the model is directionally correct. The model's prediction accuracy is not high enough to reliably predict player risk for players not previously seen. Since we are using the model only for factor relevance, we can move forward.\n\nThe following chart shows the relative importance of factors used in the model. Specific definitions are as follows:","metadata":{}},{"cell_type":"code","source":"explainer = shap.TreeExplainer(\n    model_final, data=X, model_output=\"probability\"\n)\nshap_values_final = explainer.shap_values(X)\ndisplay(shap.summary_plot(shap_values_final, X, plot_type=\"bar\"))","metadata":{"_kg_hide-input":true,"_kg_hide-output":true,"trusted":true,"execution":{"iopub.status.busy":"2025-08-09T03:01:04.135251Z","iopub.execute_input":"2025-08-09T03:01:04.135625Z","iopub.status.idle":"2025-08-09T03:01:05.874494Z","shell.execute_reply.started":"2025-08-09T03:01:04.135556Z","shell.execute_reply":"2025-08-09T03:01:05.873465Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"The chart above shows the following:\n  - The factor most relevant to identifying injured players was the average days of rest between games, followed closely by the percent of games in wet or snowy weather. \n  - Lateral and longitudinal acceleration wer fairly important\n  - The percent of plays on a synthetic field had a measurable influence, but it was less than several other factors\n  - The Field Type on the player's last play, including for those who were injured, was barely a factor\n  \nThe relative importance is based on [SHAP](https://github.com/slundberg/shap) values for the factors and take into account the factor on it's own as well as in interactions with other factors. The method is considered to be one of the most comprehensive ways to assess factor importance. In the next section I show the factors one at a time.\n\n<hr class=\"light\">","metadata":{}},{"cell_type":"markdown","source":"[](http://)<div class=\"h3\">4. EXAMINING MODEL FACTORS</div>\n\nBelow are charts examining distributions of each factor for the injured vs. non-injured players. Each of these factors shows some difference in distribution, even if it is small in some cases. As noted before, the factor importance calculated above includes interactions among factors and may not compeltely match differences seen in one factor at a time.","metadata":{}},{"cell_type":"code","source":"df = pd.concat([X, y], axis=1)\ndf[\"Injured\"] = np.where(\n    df.PlayerOut7Days_last == 0, \"Good to Play\", \"Missed >7 Days\"\n)\nplotlist = []\nfor col in df.columns.tolist()[:-2]:\n    kde_ = df.hvplot.kde(\n        y=col,\n        by=\"Injured\",\n        alpha=0.6,\n        yaxis=None,\n        title=col,\n        width=450,\n        height=250,\n    )\n    plotlist.append(kde_)\n\ndisplay(hv.Layout(plotlist).cols(2))\n","metadata":{"_kg_hide-input":true,"trusted":true,"execution":{"iopub.status.busy":"2025-08-09T02:56:03.985883Z","iopub.status.idle":"2025-08-09T02:56:03.986507Z","shell.execute_reply":"2025-08-09T02:56:03.986125Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"The singe factor distributions show differences that are generally consistent with the model importance. As mentioned before, this is not always the case. It is a measure of added insurance when they agree.","metadata":{}},{"cell_type":"markdown","source":"<a id='ap'></a>\n# Application","metadata":{}},{"cell_type":"markdown","source":"In this section I discuss two areas of opportunity to address the factors found to influence lower body injury rates:\n  - Scheduling games\n  - Selecting plays\n  <br/>\n  <br/>\n  <br/>\n  \n<div class=\"h3\">SCHEDULING GAMES</div>\n\nSetting the schedule of games for an NFL season is an enormous task. There are many considerations of which here are a few:\n  - Fan preferences \n  - Rules for the competitive framework\n  - Priorities of broadcast partners\n  - Travel considerations for teams\n  - Rest between games\n\nGiven the demonstration of rest as an important factor, any changes to games per year or rest between games should be carefully considered.\n\nI did not come across any mention of field surface as a consideration for scheduling. The percent of play on synthetic fields, which is relevant to injury rates, differs quite a bit among players. As one might expect, players based at stadiums with natural turf play over half their games on natural turf, and vice-versa for players based on synthetic turfed fields. \n\nThe chart below shows how big the difference can be in practice. It contains each team's games by type of field surface for the 2018 season.\n\n<span class=\"note\"> <i>Hover over the bars to see exact numbers.</i> </span>","metadata":{}},{"cell_type":"code","source":"#%%\ndisplay(natural + synth)","metadata":{"_kg_hide-input":true,"trusted":true,"execution":{"iopub.status.busy":"2025-08-09T02:56:03.988023Z","iopub.status.idle":"2025-08-09T02:56:03.988553Z","shell.execute_reply":"2025-08-09T02:56:03.988245Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"The Atlanta Falcons of the NFC South only played three games on natural turf for 2018. According to NFL scheduling rules, they had to play 5 teams based on synthetic fields - 2 division rivals, 2 teams from NFC East and 1 team from AFC North. However, there may have been an opportunity regarding their home/away schedule - they played 4 games at home against teams based at fields with natural turf. \n\nAdding field surface into the scheduling equation is a daunting proposition. Except for a few swaps, it is fairly certain that current scheduling rules, which have largely been in place since the early 2000s, would have to change. Proposals in the past have included relaxing the constraints on divisional matches, adjusting the rotating division matches, and even changing the league structure. Others have mentioned holding games at neutral sites on occasion, which could be chosen in part based on the playing surface.\n\n<hr>","metadata":{}},{"cell_type":"markdown","source":"<div class=\"h3\">PLAY SELECTION</div>\n\nHigh acceleration has been identified as being relevant to the likelihood of injury to the lower extremities. It's possible that teams can avoid certain plays as they become more aware of how patterns repeated over time increase player risk. Zachary Binney, an epidemiologist and consultant who has worked with Major League Baseball and college sports teams on injury prevention, had the following to say in a [recent article](https://www.theverge.com/2019/12/6/20999403/amazon-nfl-injuries-concussions-big-data-machine-learning):\n\n> “You could look at what happens when a wide receiver moving this quickly makes this sharp turn, and might be able to tease something out. ... One thing they could do is put the information out there and tell coaches that when they ask a lineman to do some kind of block, or route, it creates the sorts of changes in direction or deceleration when we see bad things happen. It’s in the best interest of coaches, after all, to keep players healthy, and there could be alternatives to riskier plays.\n\nI speculate that coaches and players already consider situational risk to the extent possible and make suitable choices. The opportunity going forward is to present additional information to further reduce risk.\n\n<hr>\n\n<br/>\n<br/>\nIn conclusion, there is evidence that playing on synthetic turf over two or more seasons is associated with a higher rate of injury to knees, ankles, and feet. Other factors also show evidence of influencing the likelihood of player injury over time: \n\n  - Lateral changes in speed from cutting or turning\n  - Changes in speed from starting and stopping\n  - Rain or snow during a game\n  - Days of rest between games\n  - Games played in open vs. closed stadiums\n  \nThere may be solid opportunities to address some of these factors for the coming season. Pursuing viable options would improve player health and safety and continue the NFL's commitment to keep the game safe and relevant.","metadata":{}},{"cell_type":"markdown","source":"<a id='ax'></a>\n# Appendix","metadata":{}},{"cell_type":"markdown","source":"<div class=\"h3\" style=\"margin-top: 50px\">CALCULATING ACCELERATION</div>\n\nI made the following assumptions during my treatment of player movement data:\n\n  - Movement is assumed to be in the horizontal plane. I did not estimate acceleration in the up-down direction, which is proportional to a player's mass and the number of steps taken.\n  - Body mechanics and angles are not considered. Players with correct running technique and strong mechanics transfer lateral force on the joints to compression force.\n  - Instantaneous changes in direction and speed are smoothed by using a rolling mean over 3/10 of a second (three successive measurements).\n  - As mentioned previously, angular acceleration from twisting is not considered due to problems with orientation data.\n","metadata":{}}]}