{"cells":[{"metadata":{},"cell_type":"markdown","source":"# Q2: Wide Receiver Movement Patterns\n\n## Abstract\n\nIn this notebook, we will specifically look at movement patterns for Wide Receivers (WRs). Movement pattern varies significantly by player group, so we've choosen to focus in on a specific position group that moves in relatively distinct patterns, due to the nature of the WR route.\n\nWe look at specific route types included in the route tree, and analyze the different injury rates between routes. We also look at more specific movement variables, such as the maximum player acceleration and player speed, as well as game level variables, such as the temperature during the game and whether the game was played on grass or artificial turf.\n\nWe found that certain route groups -- specifically Go and Curl routes -- are overrepresented in plays with injuries as comapred to the overall dataset, appearing with x1.6 and x3.1 higher frequency than expected. Conversely, injuries on slant routes appeared to happen only x0.68 as often as anticipated. While our analysis had some interesting findings, our data set was not large enough to generate findings at a statistically significant level. This analysis should be repeated if more data is available.\n\nWhen looking at all movement variables for WRs, we found that more downfield player movement and higher max deceleration on a player is strongly associated with injuries. At a game level, we found that rain, warmer temperatures, and having a synthetic field also contribute the the liklihood of an injury on any given WR play.\n\nFinally, we found that the aggregate set of plays in which players were injured had 20% higher max speeds, player that traveled 54% farther down the field, and 32% quicker max deceleration as compared to the overall set of plays. \n\n\n## Preparation\n\n### Imports"},{"metadata":{"trusted":true},"cell_type":"code","source":"# Data analysis\nimport pandas as pd\nimport random\nimport scipy.stats as stats\n\n# Plotting\nimport numpy as np\nimport matplotlib.pyplot as plt\n\n# Regression\nimport statsmodels.api as sm\nimport statsmodels.formula.api as smf\n\n# Drawing\nfrom PIL import Image, ImageDraw\nimport sys\nfrom IPython.display import display\n\n\"\"\"\nImport datasets -- these datasets are quite big.\nTakes about ~1min to load\n\"\"\"\nKAGGLE = True\nif not KAGGLE:\n    PT_data = pd.read_csv('PlayerTrackData.csv')\n    IR_data = pd.read_csv('InjuryRecord.csv')\n    PL_data = pd.read_csv('PlayList.csv')\nelse:\n    df = pd.read_csv('/kaggle/input/q2data/Q2-Data.csv')\n\npd.set_option('display.max_columns', 500)\npd.set_option('display.max_rows', 500)","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"## Data Cleaning\n\n### Background\n\nFor this analysis, we want want to look at injuries (IR data), background conditions about each play (PlayList data), and play-player movement data (PlayerTrack data). We will need to merge all three datasets to do so.\n\n### Incomplete Injury Data Analysis\n\nThe majority of the injuries in our dataset are associated with a specific play, however others just are marked at a game level. As we want to look at specific movement patterns on the play each player was injured, it's important that we know the exact play that they were injured on. We could assume that the last play that the player played in the game was the play that the player was injured on, however without more information we cannot safely make that assumption, as players may play an additional play after injury, or attempt to enter the game later, play one additional play and realize they are still injured.\n\n### Subset to WR Data & Merge\n\nAs this analysis will only be focused on WRs, we can drop all non-WR rows from our analysis. We can then merge the three datasets on PlayKey."},{"metadata":{"trusted":true},"cell_type":"code","source":"if not KAGGLE:\n    WR_data = PL_data[PL_data['PositionGroup'] == 'WR']\n    df = pd.merge(PT_data, WR_data, on = 'PlayKey', how='right')\n    df = pd.merge(df, IR_data, on = 'PlayKey', how='left')\n    df.head()","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"### Subset to Route Start / End\n\nThe player track data gives more information than we need for our analysis. In addition to the player's movement at 0.1s intervals during the route, we have their movement before the play (huddle to the the snap) and after the play (a few seconds after a pass is ruled incomplete).\n\nThe data before and after the live play time (when the player is running the route) isn't helpful for this analysis, and adds extra data that we can assume has no association with causing injuries.\n\nWe went through each of the play labels and identified them as the start of the play (e.g. \"ball snap\") or the end of the play (e.g. \"out_of_bounds\"). We then remove all data that is not part of the route. Otherwise, there is often a few seconds of walking after the ball is dead included in the data.\n\n#### Run Time: ~20 minutes"},{"metadata":{"trusted":true},"cell_type":"code","source":"def labelPlayStartEnd(df):\n    START_CRIT = ['ball_snap','snap_direct','onside_kick','kickoff','free_kick','drop_kick',]\n    END_CRIT = ['pass_outcome_incomplete','out_of_bounds','tackle','touchdown','qb_kneel','qb_sack','pass_outcome_touchdown','two_point_conversion','qb_spike','touchback','safety','field_goal','fair_catch','punt_downed','extra_point','field_goal_missed']\n    END_PASS_CRIT = ['pass_outcome_incomplete', 'pass_outcome_caught', 'pass_outcome_interception'] # unused\n     \n    df.assign(playActive=False)\n    playArray = []\n    currentFlag = False\n    previousTime = 1000.0\n\n    print('Starting dataframe length', len(df))\n    \n    # Iter rows\n    for i, row in df.iterrows():\n    \n        event = row['event']\n        currTime = row['time']\n\n        # If we hit a start event, set flag to true\n        if pd.isnull(event) == False:\n            if event in START_CRIT:\n                currentFlag = True\n            elif event in END_CRIT:\n                currentFlag = False\n            elif currTime < previousTime: # Just to make sure we ended the play\n                currentFlag = False \n\n        previousTime = currTime\n        playArray.append(currentFlag)\n        \n    df['playActive'] = playArray\n    return df","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"if not KAGGLE:\n    df = labelPlayStartEnd(df)\n    df = df[df['playActive'] == True]\n    df = df.reset_index(drop=True)","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"### Count Games Missed\n\nOur original dataset tells us whether the player missed 1, 7, 28, or 42 days of gametime. For simplicity, we are going to create a new variable called \"games missed.\" We assume that missing 1 day means that the player got injured during the game and did not return, but was able to return the following week. We therefore assign this as 0.5 games missed. \n\nSince all injuries happened during a game, we assume that an injury results in 0.5 games missed for the game that caused the injury, as well as X number of games missed for the DM/7 following days missed.\n\n- DM_M1   = 0.5 games missed\n- DM_M7\t  = They missed this game and the next game. 1.5 games missed\n- DM_M28  = 4 weeks, so 4.5 games.\n- DM_M42  = 6 weeks, so 6.5 games"},{"metadata":{"trusted":true},"cell_type":"code","source":"# This function calculates the number of games missed and appends it to our dataframe\ndef countGamesMissed(df):\n    \n    print('Starting Length', len(df))\n    \n    GM_array = []\n    for i, row in df.iterrows():\n\n        DM1  = row['DM_M1']\n        DM7  = row['DM_M7']\n        DM28 = row['DM_M28']\n        DM42 = row['DM_M42']\n\n        if DM42 == 1:\n            GM_array.append(6.5)\n        elif DM28 == 1:\n            GM_array.append(4.5)\n        elif DM7 == 1:\n            GM_array.append(1.5)\n        elif DM1 == 1:\n            GM_array.append(.5)\n        else:\n            GM_array.append(0)\n\n    df['GamesMissed'] = GM_array\n    return df\n\nif not KAGGLE:\n    df = countGamesMissed(df)","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"## Route Data\n\n### Background\n\nThis analysis heavily relies on being able to visualize the routes, as we want to be able to classify them and get a better sense of the movement patterns\n\n### Function\n\nThis function can draw lists of plays with a variety of options, including whether the image should be presentation ready or used for computer vision, and what to do with the generated image."},{"metadata":{"scrolled":false,"trusted":true},"cell_type":"code","source":"def drawPlay(pks, scaler=1, displayBool=False, saveBool=False, returnImg=False, CV=False, TRANSPOSE_OPTION=False, injuryColorCode=False):\n    \n    # Format the plays if just one\n    multiplePlays = True\n    if not isinstance(pks, list):\n        multiplePlays = False\n        pks = [pks]\n    \n    # Set up image dimensions\n    fieldWidth = 54 * scaler \n    fieldHeight = 120 * scaler\n    LOS = 20 * scaler\n    \n    # CV or Presentation settings\n    if CV == False:\n        fieldColor = 'rgb(50, 168, 82)'\n        LOSColor = 'blue'\n        lineWidth = 2 * scaler       \n        hashWidth = 1 * scaler\n        endZoneColor = 'rgb(11, 117, 40)'\n        \n        # Init\n        im = Image.new(\"RGBA\", (fieldWidth, fieldHeight), color=fieldColor)\n        draw = ImageDraw.Draw(im)\n        \n        # Endzones\n        draw.rectangle(((0, 0), (54*scaler, 10*scaler)), fill = endZoneColor)\n        draw.rectangle(((0, (120-10)*scaler), (54*scaler, 120*scaler)), fill = endZoneColor)\n        \n        # Yard lines\n        for i in range(20, 110, 10):\n            YL = i * scaler\n            draw.line((0,YL) + (fieldHeight,YL), fill='white', width=hashWidth)\n            \n        # Adjustments for multiple plays\n        if multiplePlays == True:\n            lineWidth = scaler # Thinner lines\n        \n    else:\n        fieldColor = 'rgb(0,0,0)'\n        LOSColor = 'black'\n        lineWidth = 2 * scaler\n        \n        # Init\n        im = Image.new(\"RGBA\", (fieldWidth, fieldHeight), color=fieldColor)\n        draw = ImageDraw.Draw(im)\n        \n    # For each play, draw play\n    for pk in pks:\n        \n        # Get data on that play\n        playData = df[df['PlayKey'] == pk]\n        \n        # Figure out if we need to flip the play (we always go up)\n        FLIP = False\n        startY, endY = list(playData['x'])[0], list(playData['x'])[-1]\n        if startY < endY:\n            FLIP = True\n\n        # Draw route\n        firstRow = True\n        lastCoordinates = (0,0)\n        startX = None\n        LOS_offset = None\n        for i, row in playData.iterrows():\n\n            # Flip the play if needed\n            if FLIP:\n                yardLine = fieldHeight - (row['x'] * scaler)\n                yardHash = fieldWidth - (row['y'] * scaler)\n            else:\n                yardLine = row['x'] * scaler\n                yardHash = row['y'] * scaler\n            \n            coordinates = (yardHash, yardLine)\n            if firstRow:\n\n                firstRow = False\n                startY = yardLine\n                startX = yardHash\n                LOS_offset = startY - LOS\n                lastCoordinates = coordinates\n                \n                # Draw line of scimage if just one play\n                if multiplePlays == False:\n                    draw.line((0,startY) + (fieldHeight,startY), fill=LOSColor, width=lineWidth)\n                    \n            else:\n                if injuryColorCode:\n                    # If multiple plays, color code based on injury\n                    if (row['GamesMissed'] > 0):\n                        routeColor =  'rgb(255, 0, 0)'\n                    else:\n                        routeColor =  'rgb(0, 0, 255)'\n                    \n                elif CV:\n                    # White for CV model\n                    routeColor = 'white'\n                else:\n                    \n                    # Color code route based on speed\n                    playerSpeed = row['s']\n                    speedScale = int(min(playerSpeed, 12.5) * 20)\n                    routeColor =  'rgb(255,' + str(251 - speedScale) + ', 0)'\n\n                draw.line(lastCoordinates + coordinates, fill=routeColor, width=lineWidth)\n                endY = yardLine\n                lastCoordinates = coordinates\n\n    # Transpose so that we always start left of QB\n    if TRANSPOSE_OPTION:\n        if startY < endY:        \n            if startX < fieldWidth / 2:\n                im = im.transpose(Image.FLIP_LEFT_RIGHT)\n\n        elif startX > fieldWidth / 2:\n            im = im.transpose(Image.FLIP_LEFT_RIGHT)\n\n    del draw\n\n    # write to stdout\n    if displayBool:\n        display(im)\n    if saveBool:\n        im.save('wr/routes/' + str(pk) + '.jpg')\n    if returnImg:\n        return im","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"### Individual Play\n\nHere, the blue line represents the yard line where the WR started his route. The route line gradient shows the speed that the player was running."},{"metadata":{"trusted":true},"cell_type":"code","source":"drawPlay('38876-29-14', displayBool = True, CV=False, scaler=3)","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"### Multiple plays, same chart"},{"metadata":{"trusted":true},"cell_type":"code","source":"drawPlay(['38876-29-12', '38876-29-13', '38876-29-14'], displayBool = True, CV=False, scaler=3)","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"### Computer vision\n\nIf you're looking to make a computer vision model, it might be easier to use black and white images."},{"metadata":{"trusted":true},"cell_type":"code","source":"drawPlay(['38876-29-14'], displayBool = True, CV=True, scaler=3)","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"### Injury Plays\n\nLet's take a look at the plays that WRs got injured on"},{"metadata":{"scrolled":false,"trusted":true},"cell_type":"code","source":"WR_INJURY_PLAYS = list(set(df[df['GamesMissed'] > 0]['PlayKey']))\n\ndef drawManyRoutes(PLAY_LIST = [], labelRoute = False, routeLabels = []):\n    plays = []\n    for pk in PLAY_LIST:\n        plays.append(drawPlay(pk, returnImg=True))\n\n    \n    TOTAL_ROWS = int(len(PLAY_LIST) / 8) + 1\n    plt.figure(figsize=(14,5 * (TOTAL_ROWS)))\n    \n    for n in range(len(PLAY_LIST)):\n        ax = plt.subplot(TOTAL_ROWS,8,n+1)\n        plt.xticks([])\n        plt.yticks([])\n        plt.grid(False)\n        plt.imshow(plays[n], cmap=plt.cm.binary)\n        \n        if labelRoute:\n            plt.xlabel(routeLabels[n] + '\\n' + str(PLAY_LIST[n]))\n        else:\n            plt.xlabel(str(df[df['PlayKey'] == PLAY_LIST[n]]['GamesMissed'].max()) + \\\n                       ' GM \\n' + str(PLAY_LIST[n]))\n        \ndrawManyRoutes(WR_INJURY_PLAYS)","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"### Random Sample of Plays\n\nSummary view of 100 non-injury routes and 12 injury routes."},{"metadata":{"trusted":true},"cell_type":"code","source":"# Draw a random sample of plays\nALL_PLAYS = list(df.drop_duplicates([\"PlayKey\"])['PlayKey'])\ndrawPlay(random.choices(ALL_PLAYS, k=100) + random.choices(WR_INJURY_PLAYS, k=12), scaler=6, displayBool = True, injuryColorCode=True)\nprint('Red=Injury; Blue=Non-injury')","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"#### Heat Gradient View\n\nWe can see that players move the quickest on long routes."},{"metadata":{"trusted":true},"cell_type":"code","source":"# Draw a random sample of plays\ndrawPlay(random.choices(ALL_PLAYS, k=50), scaler=6, displayBool = True, CV=False)","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"### Classification\n\nWe want to classify routes depending on route type. This must be done manually.\n\nDue to insufficient data and that mirrored routes (post / corner) are similar in terms of movement type, we will classify routes into sub-categories.\n\nIt's not possible to cleanly classify every route: some plays combine multiple routes into one. Also, a go route on the opposing 20 looks a lot different than a go route on your own 20.\n\n"},{"metadata":{"trusted":true},"cell_type":"code","source":"from IPython.display import Image as DisplayInlineImage\nDisplayInlineImage(\"../input/routespng/routes.png\")","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"### Classify Injury Routes"},{"metadata":{"trusted":true},"cell_type":"code","source":"injuryRoutes = [\n    'Curl',    'Go',    'Go',    'NA',      'Slant',  'Go',     'Out',  'Go',\n    'NA',      'Out',   'Curl',   'Slant',   'Go',    'Slant',  'Go',   'Out',\n]\ndrawManyRoutes(WR_INJURY_PLAYS, labelRoute = True, routeLabels = injuryRoutes)","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"### Classify 200 Routes Manually\n\nTo get a good sample of the route distriubtion, we will classify 200 routes by hand. These were selected randomly from all WR Routes.\n\nThe most important thing is that the plays be grouped with similar plays.\n\n- Go routes need about 20 yards to qualify, otherwise routes are NA\n- The route should be decided based off the first 30 or so yards\n- Out needs to be about 90 degrees after 10 yards\n- Curl needs to hook back after about 10-20 yards.\n\nBecause of the classification difficulty, any uncertainty is labeled as NA."},{"metadata":{"trusted":true},"cell_type":"code","source":"ROUTES200 = ['41086-24-31', '42405-8-55', '38364-19-22', '40335-15-34', '44449-1-41', '42405-12-23', '44203-25-18', '38192-20-36', '42359-32-46', '41086-19-60', '30068-6-54', '47285-2-37', '45962-30-45', '38192-24-21', '42405-9-34', '44423-9-10', '44424-17-63', '47376-13-37', '34347-3-11', '46021-24-6', '45962-23-39', '42359-15-49', '44449-27-17', '44203-18-17', '34243-9-20', '45061-1-2', '40405-15-1', '42398-5-31', '42432-11-2', '44203-13-11', '38252-22-25', '43532-6-25', '42432-8-21', '30068-25-49', '38252-17-36', '44424-7-38', '44203-26-14', '42432-7-40', '38192-6-40', '30068-15-32', '42346-24-42', '34230-7-45', '42346-11-45', '42432-21-27', '45987-30-63', '40335-27-43', '42346-5-17', '42346-15-45', '38192-20-46', '44449-19-47', '38876-28-21', '42432-3-12', '34347-12-1', '45962-11-26', '42359-1-10', '44203-17-53', '45987-29-58', '45987-27-24', '45962-25-40', '42405-9-57', '45962-1-61', '45962-6-9', '46021-19-45', '45987-15-44', '42405-12-17', '40335-23-41', '38192-22-26', '42346-12-5', '35648-14-18', '38192-23-10', '42405-5-42', '40405-23-9', '45987-22-53', '42432-12-9', '44424-20-1', '36591-3-5', '36630-19-21', '44423-7-50', '34243-24-38', '47376-9-11', '34243-17-3', '35648-5-20', '46067-11-11', '42359-4-39', '45987-2-31', '47273-8-5', '42432-10-37', '42405-2-37', '38876-17-22', '44203-25-7', '44449-4-39', '41086-20-75', '43532-4-36', '45962-18-42', '42359-23-2', '46021-9-7', '39656-19-13', '43532-18-47', '42432-11-34', '42346-4-1', '44203-23-3', '46021-20-56', '30068-28-27', '38364-11-16', '36630-7-7', '35648-6-6', '36591-2-2', '39809-24-7', '42432-22-20', '30068-2-52', '42405-8-4', '45962-17-2', '46021-8-35', '44203-32-33', '34243-15-3', '43532-12-7', '45962-15-40', '47273-15-31', '38192-24-9', '42346-18-24', '44449-28-22', '34230-4-9', '42432-9-35', '47784-13-48', '38876-16-11', '42448-15-15', '34347-23-7', '47285-2-23', '42346-22-38', '34243-8-1', '42405-27-31', '42405-18-6', '34230-19-50', '46021-11-28', '42405-6-10', '38876-8-35', '47784-7-1', '42432-6-21', '44203-13-34', '46021-20-48', '44424-14-43', '44424-8-49', '42448-4-38', '44203-9-6', '45987-15-49', '47287-3-11', '42448-11-3', '47784-13-19', '42448-5-20', '38252-9-9', '45987-29-13', '34230-3-55', '44449-15-8', '38252-12-20', '46021-26-51', '42398-12-51', '41086-24-35', '42398-7-2', '34230-20-14', '47376-12-12', '38252-2-50', '45962-17-36', '44449-9-15', '42346-11-44', '38876-5-39', '30068-31-30', '42405-23-44', '30068-29-42', '34347-7-7', '42346-26-27', '42405-12-40', '42346-12-6', '44424-24-28', '47784-10-7', '30068-16-31', '38192-7-37', '30068-29-31', '44424-9-58', '39656-15-11', '40335-18-22', '44203-32-21', '38252-1-12', '47273-7-8', '47287-10-2', '35648-12-6', '39656-12-1', '43532-9-65', '45187-19-11', '46098-18-26', '44424-23-29', '47287-10-39', '42359-29-41', '42346-26-44', '46067-10-14', '41086-17-46', '42346-5-30', '42448-6-7', '47287-8-45', '47287-3-8', '44203-25-31']","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"ROUTES200Labels = [\n    'Slant', 'NA',    'Slant',  'Go',     'NA',     'Slant',  'NA',  'NA',  \n    'Slant', 'Go',    'Go',      'Slant',  'Go',     'Go',     'Go',     'Slant',\n    'Out',   'Out',   'Out',    'Curl',   'NA',     'NA',     'Go',     'Slant',\n    'Go',    'Go',    'Slant',  'Go',     'Slant',  'Slant',  'Out',    'Slant',\n    'Out',   'Slant', 'Out',    'Out',    'Slant',  'NA',     'NA',     'NA',\n    'Curl',  'Out',   'Go',     'Slant',  'Slant',  'Go',     'NA',     'Go',\n    'NA',    'NA',    'Go',     'Slant',  'Out',    'NA',     'Out',    'Go',\n    'NA',    'Go',    'Slant',  'NA',     'NA',     'Out',    'Go',     'NA',\n    'NA',    'Go',    'Out',    'Go',     'Go',     'Slant',  'Out',    'NA', \n    'NA',    'Slant', 'NA',     'NA',     'Slant',  'Slant',  'Slant',  'Go',\n    'NA',    'Slant', 'Go',     'Out',    'Go',     'Out',    'Go',     'Slant',\n    'Go',    'Slant', 'NA',     'NA',     'NA',     'Slant',  'Slant',  'Out',\n    'Out',   'Out',   'NA',    'NA',     'Slant',  'Slant',  'NA',     'Slant',\n    'Go',    'NA',    'Go',     'Go',     'Curl',   'Slant',  'Out',    'NA',\n    'Slant', 'NA',    'NA',     'NA',     'NA',     'Go',     'Slant',  'NA',\n    'Curl',  'NA',    'Slant',  'NA',     'NA',     'Out',    'Slant',  'Slant',\n    'Out',   'Out',   'Slant',  'Slant',  'Go',     'Go',     'NA',     'Slant',\n    'Go',    'Slant', 'Go',     'Out',    'Out',    'Curl',   'NA',     'Go',\n    'NA',    'NA',    'Slant',  'NA',     'Slant',  'NA',     'NA',     'NA',\n    'Go',    'NA',    'Slant',  'NA',     'Curl',   'Slant',  'NA',     'Go',\n    'Slant', 'Go',    'NA',     'Out',    'Slant',  'NA',     'NA',     'Slant',\n    'Go',    'Go',    'Curl',   'NA',     'NA',     'NA',     'NA',     'NA',\n    'NA',    'Curl',  'Slant',  'Slant',  'NA',     'NA',     'Go',     'Slant',\n    'NA',    'Slant', 'NA',     'NA',     'Slant',  'Out',    'Go',     'Slant',\n    'Out',   'Go',    'Out',    'NA',     'Go',     'NA',     'NA',     'Out',\n]","execution_count":null,"outputs":[]},{"metadata":{"scrolled":true,"trusted":true},"cell_type":"code","source":"drawManyRoutes(ROUTES200[0:200], labelRoute = True, routeLabels = ROUTES200Labels)","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"### Analyze Route Patterns\n\nNow that we have a sense of the overall route distribution and the injury route distribution, we can test to see if the distrubtions are equal. We run a p-test for each route group to determine if there are signficantly more or fewer routes of that type in the injury group. "},{"metadata":{"trusted":true},"cell_type":"code","source":"routeDF = pd.DataFrame(index = list(set(ROUTES200Labels)))\n\nrouteDF['Injury'] = [injuryRoutes.count(i) for i in list(routeDF.index)]\nrouteDF['Injury_Average'] = routeDF['Injury'] / sum(routeDF['Injury'])\n\nrouteDF['All'] = [ROUTES200Labels.count(i) for i in list(routeDF.index)]\nrouteDF['All_Average'] = routeDF['All'] / sum(routeDF['All'])\n\nrouteDF['FrequencyDifference'] = routeDF['Injury_Average'] / routeDF['All_Average']\n\n\ndef getPValue(x_True, x_Total, y_True, y_Total):\n    x_sample = [1] * x_True + [0] * (x_Total - x_True)\n    y_sample = [1] * y_True + [0] * (y_Total - y_True)\n    t_stat, p_val = stats.ttest_ind(x_sample, y_sample, equal_var=False)\n    return p_val\n\nrouteDF['P-Stat'] = [getPValue(int(routeDF.loc[i, 'Injury']), 12, \n                               int(routeDF.loc[i, 'All']), 200) for i in list(routeDF.index)]\n\nrouteDF","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"We can see that there are proportionally more Curl and Go routes in the injury set, and fewer Slant routes. Due to the lack of data our injury set (n=12) none of the P-Stats for a difference test are below a 0.05 interval. These results are still interesting, and expanding this analysis to a larger sample set may generate signifcant figures."},{"metadata":{},"cell_type":"markdown","source":"## Other Movement Data\n\nWe will now explore some other variables associated with player movement data -- as well as weather and external factors -- to see if we can identify any other variables associated with injury.\n\n### Variable List\n\n#### Weather\n\n- Snowing\n- Raining\n- Temperature\n\n#### Movement\n\n- Total Distance Traveled\n- X Distance Traveled (Downfield)\n- Y Distance Traveled (Sideways)\n- Max Acceleration\n- Max Deceleration\n- Max Speed\n- Total Degrees Turned\n\n#### Misc\n\n- Play Count (How many plays the player has played that game)\n- Synthetic Field\n\n### Merge Player Track Data\n\nTo calculate many of the movement metrics above, we need intraplay movement data. To speed up future calculations, we will build a dataframe by play that contains a column listing all matched PlayerTrack index values."},{"metadata":{"trusted":true},"cell_type":"code","source":"playKey = []\nplayKeyRows = []\nplayKeyRowsBuilder = []\npreviousRowPlay = '' \n\n# Add to the dataframe\nprint('Starting Rows', len(df))\nfor i, row in df.iterrows():\n    \n    # Get current play key\n    thisPlayKey = row['PlayKey']\n    if previousRowPlay != thisPlayKey:\n        if previousRowPlay != '':\n            playKeyRows.append(playKeyRowsBuilder)\n        playKeyRowsBuilder = []\n    previousRowPlay = thisPlayKey\n    playKeyRowsBuilder.append(i)\n\nplayKeyRows.append(playKeyRowsBuilder)\nplayKeyRowsBuilder = []","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"ALL_PLAYS = list(df.drop_duplicates([\"PlayKey\"])['PlayKey'])\nplay_df = pd.DataFrame()\nplay_df['PlayKey'] = ALL_PLAYS\nplay_df['Keys'] = playKeyRows\nplay_df = pd.merge(play_df, df, on = 'PlayKey', how='left').drop_duplicates([\"PlayKey\"])\nplay_df = play_df.drop(columns=['PlayerKey_y', 'GameID_y', 'playActive', 'time','event','x','y','dir','dis','o','s'])","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"### Custom Metrics\n\nCalculate custom movement metrics based on the player track data.\n\n#### Acceleration\n\nTo find the maximum acceleration and deceration, we look at the change in speed between timestamps 0.5 seconds apart, then take the largest and smallest value.\n\n#### Turn degrees\n\nWe sum the total degrees the player turns during the play."},{"metadata":{"trusted":true},"cell_type":"code","source":"def getAcceleration(play):\n    speeds = list(play['s'])\n    accelerations = []\n    if len(speeds) > 5:\n        for i in range(5, len(speeds)):\n            accelerations.append(speeds[i] - speeds[i-5])       \n        return max(accelerations) * 2, min(accelerations) * 2 # we do 0.5 second intervals, mulitply by 2\n    else:\n        return 0, 0 \n\n# Get the total degrees the player turned\ndef getTurnDegrees(play):\n\n    turns = list(play['o'])\n    degrees = 0\n    for i in range(len(turns)):\n        if i != 0:\n            turn = abs(turns[i] - turns[i-1])\n            if turn > 200:\n                turn = 0\n            degrees += turn\n    return degrees\n\ndistanceTraveled = []\nyMovement = []\nxMovement = []\n\ndegreesTurned = []\n\nmaxSpeed = []\nmaxAcceleration = []\nmaxDeceleration = []\n\nprint('Starting Rows', len(play_df))\nfor i, row in play_df.iterrows():\n    play = df.iloc[row['Keys']] \n        \n    # Distance\n    distanceTraveled.append(sum(play['s']) * .1)\n    yMovement.append(play['y'].max() - play['y'].min())\n    xMovement.append(play['x'].max() - play['x'].min())\n    \n    # Turning\n    degreesTurned.append(getTurnDegrees(play))\n    \n    # Speed and Acceleration\n    maxSpeed.append(play['s'].max())\n    acceleration, deceleration = getAcceleration(play)\n    maxDeceleration.append(deceleration)\n    maxAcceleration.append(acceleration)\n    \n    \nplay_df['MaxSpeed'] =  maxSpeed\nplay_df['distanceTraveled'] = distanceTraveled\nplay_df['degreesTurned'] = degreesTurned\nplay_df['yMovement'] =  yMovement\nplay_df['xMovement'] =  xMovement\nplay_df['maxAcceleration'] =  maxAcceleration\nplay_df['maxDeceleration'] =  maxDeceleration\n\nplay_df.head(2)","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"### Weather & Misc Variables \n\nFinally, we need to convert some variables into boolean variables for our regression, including whether it is snowing, whether it is raining, and whether the field is natural or synthetic.\n\nWe use 72 degrees as the game temperature for games indoors."},{"metadata":{"trusted":true},"cell_type":"code","source":"def convertVariablesForRegression(tmp):\n\n    def isIn(item, word):\n        return item in str(word).lower()\n\n    # Convert some fields to boolean\n    tmp['Is_Synthetic'] = tmp['FieldType'] == 'Synthetic'\n    tmp['Is_Rain'] = [1 if isIn('rain', i) or isIn('shower', i) else 0 for i in tmp['Weather']]\n    tmp['Is_Snow'] = [1 if isIn('snow', i) else 0 for i in tmp['Weather']]\n\n    # Clean some variables\n    tmp['Temperature_Adj'] = [i if i != -999 else 72 for i in tmp['Temperature']]\n    \n    return tmp\n    \nplay_df = convertVariablesForRegression(play_df)","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"### Regression\n\nWe can now run our regression to see which variables have predictive power in predicting the number of games missed due to injury per play. We use a Poisson regression."},{"metadata":{"trusted":true},"cell_type":"code","source":"X = 'GamesMissed'\nY = ['MaxSpeed', 'distanceTraveled', 'degreesTurned', 'yMovement', 'xMovement', 'Is_Synthetic', \\\n     'PlayerDay', 'PlayerGamePlay', 'Temperature_Adj', 'Is_Rain', 'Is_Snow', \\\n     'maxAcceleration', 'maxDeceleration']\nY_a = ' + '.join(Y)\n\nmodel = sm.GLM.from_formula(X + ' ~ ' + Y_a, data=play_df, family=sm.families.Poisson()).fit()\nprint(model.summary())","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"### Results\n\nThe following variables had predictive power:\n\n#### Weather\n\n- Raining: slick surfaces seem to make the field more dangerous\n- Temperature: warmer games tend to have more injuries -- could players be moving faster?\n\n#### Movement\n\n- X Distance Traveled (Downfield): longer routes are associated with more injuries\n- Max Deceleration: faster deceration is associated with more injuries\n\n#### Misc\n\n- Synthetic Field: as shown in our previous analysis, significantly more injuries happen on synthetic turf\n\n\n### Exploring Results\n\nIs there a correlation between temperature and max speed? It appears the answer is no."},{"metadata":{"trusted":true},"cell_type":"code","source":"play_df['Temperature_Adj'].corr(play_df['MaxSpeed'])","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"### Fundamental Differences Between Datasets\n\nLet's take an overall look at the plays were injuries happen as compared to all plays. Is there a fundamental difference in player movement?"},{"metadata":{"trusted":true},"cell_type":"code","source":"# Movement differences\nmovementDF = pd.DataFrame(columns = ['All', 'Injury', 'P-Val'])\nfor metric in Y:\n    allPlays = play_df[metric]\n    injuryPlays = play_df[play_df['GamesMissed'] > 0][metric]\n    t_stat, p_val = stats.ttest_ind(allPlays, injuryPlays, equal_var=False)\n    movementDF.loc[metric] = [allPlays.mean(), injuryPlays.mean(), round(p_val,3)]\n    \nmovementDF['Ratio'] = movementDF['Injury'] / movementDF['All']\nmovementDF","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"On plays in which players were injured, players were\n\n- Moving at 20% higher max speeds\n- Going 54% farther down the field\n- Having 32% quicker max deceleration\n\nAll these results are significant at a p<0.1 level."}],"metadata":{"kernelspec":{"display_name":"Python 3","language":"python","name":"python3"},"language_info":{"codemirror_mode":{"name":"ipython","version":3},"file_extension":".py","mimetype":"text/x-python","name":"python","nbconvert_exporter":"python","pygments_lexer":"ipython3","version":"3.7.5"}},"nbformat":4,"nbformat_minor":1}