{"cells":[{"metadata":{"_uuid":"ddaa4adfe18e66da03f7df1f7806d45451c0f076"},"cell_type":"markdown","source":"# What's in common for concussion caused by blocking?\nPlayers on the punting team have limited opportunities to defend themselves from blind-side blocks. While they are pursuing the punt returner, who’s in front of them, they cannot find or brace themselves from the blockers behind, who are running toward them at full speed.  \n\nIf this statement is true, we should observe that, at the point of collision, players on the return team initiate blocking toward their its own endzone (PR end zone) while players on the punting team also look at the same direction, where the punt returner is.  This kernel sets out to perform such analysis, and it does show the statement is true.  While current NFL rules penalize blocking from behind,  the proposed rule change expands the rule to penalize blocking toward the direction of the PR end zone, regardless the opposing player was facing the blocker or not.  This rule has eliminated 9 out of 10 blocking-induced injury plays in the analysis.  This rule change would only allow the punt return team players to block from the front, thus helping to eliminate blind-side blocks and reduce injury. \n\nThis analysis is composed of the following cells.  First, a cell that includes libraries and functions that will be used later.  It will be easier to start with later cells and refer functions to the first cell.  Second, it plots blocking-induced injury plays to visualize the routes of the concussed player, the partner, and the collision.  It also shows the initial formation of both teams when the ball was snapped, and the route of the punt returner.  Next, it aggregates the movement direction of the blockers and the head direction of the blocked player at the point of collision, which shows the blocking direction is predominantly toward the PR end zone.  Finally, it shows that the concussed player is equally likely from the blocker or the blocked player."},{"metadata":{"trusted":false,"_uuid":"3dd25385e05afd75363dcd08e1c5ceab6d428b8e"},"cell_type":"code","source":"import pandas as pd\nimport numpy as np\nimport seaborn as sns\nimport matplotlib.pyplot as plt\nfrom pathlib import Path\nfrom os import listdir\nfrom matplotlib import rc as pltrc\n\n'''\nThis function first load the video_footage-injury.csv to identify injury plays,\nand then loop through all the NGS files to gather NGS data of these plays.\n\n- parameters:\n    - outname, the file name to save NGS data of injury plays\n    - datapath, where original NGS data are stored\n- return:\n    - data frame of NGS data for the injury plays\n'''\ndef extract_injury_play_NGS(outname='ngs_injury.csv', datapath='../input/'):\n\n    task = 'injury'    \n    if task == 'injury':\n        videos = pd.read_csv(datapath + 'video_footage-injury.csv')        \n    else:\n        videos = pd.read_csv(datapath + 'video_footage-control.csv')\n        \n    ngsfiles = [filename for filename in listdir(datapath) if filename.startswith(\"NGS\")]\n\n    out = None\n    for playfile in ngsfiles:\n    \n        print(playfile)\n        p0 = pd.read_csv(datapath + playfile, low_memory=False)\n        \n        for i in range(videos.shape[0]):\n            p1 = p0.loc[(p0['GameKey']==videos.gamekey[i]) & \n                        (p0['PlayID']==videos.playid[i])].copy()\n            if p1.shape[0] > 0:\n                if out is None:\n                    out = p1\n                else:\n                    out = out.append(p1)\n                    \n        p0 = None\n        print('injury NGS data frame shape: ', end='')    \n        print(out.shape)\n                    \n    out.to_csv(outname)\n    print('save to ', outname)\n    \n    return out\n \n'''\nGetting index of NGS data where collision happened.  It assumes collision happened when\nthe distance between the two players were the shortest.  This assumption is reasonable\nbut imperfect. So the index of one play was corrected manually by visual inspection.\n- parameters:\n    - concuss, NGS data of the injured player\n    - partner, NGS data of the partner\n- return:\n    - index of collision from the NGS data \n'''    \ndef get_index_of_collision(concuss, partner):    \n    if partner.shape[0] == 0:\n        return np.float('nan')\n    \n    dist = np.sqrt(pow(concuss['x']-partner['x'],2)+\n                   pow(concuss['y']-partner['y'],2))    \n    \n    ind = np.where(dist == dist.min())\n    ind = ind[0][0]\n    \n    if concuss.at[0,'GameKey']==364 and concuss.at[0,'PlayID']==2489:     \n        ind = 192 # hack for this particular play, because their distance was shorter at line of scrimmage\n                            \n    return ind\n        \n'''\nGet GSISID of a player by its role in a play\n- parameters:\n    - players, all the players of a play from play_player_role_data.csv\n    - role, role of interest\n- return:\n    - GSISID of the player of the role\n'''\ndef get_GSISID_by_role(players, role):\n    x = players.loc[(players.Role==role),'GSISID'].copy()\n    x.reset_index(drop=True, inplace=True)\n    return x[0]\n\n'''\nConvert angle 'o' and 'dir' from NGS to the x and y component of the direction, \nmultiplied by speed.  From my experiment, 'o' and 'dir' seem to be different from \nhow the manual describes them. They both increase clockwise, instead of counter-clockwise\nin the manual.  The 0 degree of 'o' is toward the positive x direction instead of \npositive y.  Nevertheless, the following conversion makes the x and y components\nreasonable among all the plays.\n- parameters:\n    - theta, either 'o' or 'dir'\n    - speed, speed at the direction of theta\n    - is_dir, is 'dir' provided instead of 'o'\n- return:\n    - u, x component of the direction\n    - v, y component of the direction\n'''\ndef theta_to_UV(theta, speed, is_dir):\n    if is_dir:\n        theta = np.deg2rad(theta-90) # zero degree is toward the positive y direction      \n    else: \n        theta = np.deg2rad(theta) # zero degree is toward the positive x direction\n    \n    # it seems that theta increases clockwise\n    u = speed * np.cos(theta)\n    v = speed * -np.sin(theta)\n    \n    return u, v\n    \n'''\nNGS data of a route of a player\n- parameters:\n    - ngs, NGS data of a play\n    - player_id, GSISID of the player of interest\n- return:\n    - route of the player in NGS format\n'''    \ndef get_route(ngs, player_id):\n    route = ngs[(ngs.GSISID==player_id)].copy()\n    \n    if route.empty:\n        return route\n    \n    # Reorder by Time and reset index\n    route.sort_values(by=['Time'], inplace=True)\n    route.reset_index(drop=True, inplace=True)    \n    \n    # end event of the play\n    endevent = route.loc[(route.Event == 'tackle') |                                                \n                         (route.Event == 'touchdown') |                        \n                         (route.Event=='fair_catch') |                        \n                         (route.Event=='punt_downed') |              \n                         (route.Event == 'out_of_bounds')]\n    \n    # takes out routes after the play ended\n    route = route.drop(route.index[endevent.index[0]+1:route.shape[0]])\n    return route\n\n'''\nremove part of the route that was logged after the end of the play\n- parameter:\n    - route, route of a play in NGS format\n    - keep_len, length of the route to keep\n- return:\n    - truncated route\n'''\ndef remove_route_after_play(route, keep_len):\n    return route.drop(route.index[(keep_len+1):route.shape[0]])\n\n'''\nplot the route in simple line, and an arrow in the end of the route to indicate direction\n- parameters:\n    - route, route of a player in a play in NGS format\n    - is_dir, True to plot movement direction in the end; False to plot head direction\n'''\ndef plot_route_line(route, is_dir=True):\n\n    plt.plot(route.loc[:,'x'], route.loc[:,'y'], color='black')\n    \n    if is_dir:\n        theta = route.loc[:,'dir']\n    else:\n        theta = route.loc[:,'o']    \n    u, v = theta_to_UV(theta, route.loc[:,'dis']/0.1, is_dir)\n    \n    last_ind = route.shape[0]-1\n#     last_ind = np.arange(10,last_ind, 20)\n    plt.quiver(route.loc[last_ind,'x'], route.loc[last_ind,'y'], u[last_ind], v[last_ind],\n              scale_units='xy', scale=3, width=0.005)\n\n'''\nplot the position and head direction of all 22 players: punting team in orange, \nand return team in blue.\n- parameters:\n    - init_pos, NGS data of all the players at a point of time\n'''\ndef plot_init_xy_head(init_pos):\n    init_return = init_pos.loc[(init_pos.Role.isin(return_team_roles))]\n    init_punt = init_pos.loc[~(init_pos.Role.isin(return_team_roles))]    \n    \n    # plot return team\n    u, v = theta_to_UV(init_return['o'], 5*np.ones(init_return['o'].shape), False)\n    plt.quiver(init_return['x'], init_return['y'], u, v,\n              scale_units='xy', scale=3, width=0.005, color=[0,0.6,1])\n    \n    # plot punting team\n    u, v = theta_to_UV(init_punt['o'], 5*np.ones(init_punt['o'].shape), False)\n    plt.quiver(init_punt['x'], init_punt['y'], u, v,\n              scale_units='xy', scale=3, width=0.005, color=[1,0.6,0])    \n\n'''\nlabel events on the route of the injured player, partner, and the punt returner.\nEvents such as ball snap (o), and when the ball was received (+)\n- parameters:\n    - concuss, NGS data of a play of the injured player \n    - partner, NGS data of a play of the partner\n    - returner, NGS data of a play of the punt returner\n'''\ndef label_event_on_route(concuss, partner, returner):\n    eoi = ['ball_snap', 'punt_received']\n    eoi_shape = ['o', '+']\n    returner_s = [None, 200]\n    for i in range(len(eoi)):\n        concuss_evt = concuss[(concuss.Event == eoi[i])]\n        plt.scatter(concuss_evt['x'], concuss_evt['y'], c='red', marker=eoi_shape[i])\n        returner_evt = returner[(returner.Event == eoi[i])]        \n        plt.scatter(returner_evt['x'], returner_evt['y'], c='black', marker=eoi_shape[i], s=returner_s[i])        \n        if not np.isnan(gsisid['partner']):\n            partner_evt = partner[(partner.Event == eoi[i])]\n            plt.scatter(partner_evt['x'], partner_evt['y'], c='green', marker=eoi_shape[i])      \n\n'''\nplot collision on the routes. The blue arrow indicates the direction and speed of the blocker, \nand the orange arrow indicates the head direction and speed of the blocked player.\n- parameters:\n    - concuss, NGS data of the injured player in a play\n    - partner, NGS data of the partner in the same play\n    - blocking_id, GSISID that initiated the block\n    - contact_ind, the index of when the collision happened\n    - advance, how many data points ahead to plot the directions. E.g., if the contact index\n               is 100 and advance is 3, then the plot shows the direction at index 97\n- return:\n    - blocked_uv, the head direction of the blocked player in x, y components\n    - blocking_uv, the movement direction of the blocking player in x, y components\n'''\ndef plot_collision(concuss, partner, blocking_id, contact_ind, advance=0):            \n    \n    if blocking_id == concuss.at[0,'GSISID']:\n        blocking = concuss\n        blocked = partner        \n    else:\n        blocking = partner\n        blocked = concuss\n    \n    # plot the head direction who's blocked    \n    u, v = theta_to_UV(blocked.loc[contact_ind-advance,'o'], blocked.loc[contact_ind-advance,'dis']/0.1, False)\n    plt.quiver(blocked.loc[contact_ind,'x'], blocked.loc[contact_ind, 'y'], u, v, color=[1,0.6,0])\n    blocked_uv = [u, v]\n\n    # plot the movement direction of the blocker\n    u, v = theta_to_UV(blocking.loc[contact_ind-advance,'dir'], blocking.loc[contact_ind-advance,'dis']/0.1, True)\n    plt.quiver(blocking.loc[contact_ind,'x'], blocking.loc[contact_ind, 'y'], u, v, color=[0,0.6,1])\n    blocking_uv = [u, v]\n    \n    return blocked_uv, blocking_uv\n    \n'''\nGet the GSISID of the player who initiated the block\n- parameters:\n    - block_injury, a play from video_review.csv\n    - gsisid, a dict of GSISID of players of interest\n'''    \ndef who_is_blocking(block_injury, gsisid):    \n    # who initiate the block? (blocking)\n    if block_injury.at['Player_Activity_Derived']=='Blocked':\n        gsisid['blocking'] = gsisid['partner']\n    elif block_injury.at['Player_Activity_Derived']=='Blocking':\n        gsisid['blocking'] = gsisid['injury']\n    else:\n        gsisid['blocking'] = None\n    \n    return gsisid\n\n'''\nCheck whether the collision occured at the line of scrimmage\n- paramters:\n    - ngs, full NGS data\n    - gsisid, a dict of GSISID of players of interest\n- return:\n    - True or False\n'''\ndef injury_at_return_team_backfield(ngs, gsisid):\n    concuss = get_route(ngs, gsisid['injury'])\n    partner = get_route(ngs, gsisid['partner'])\n    \n    # get the index of the route when the concussed player and the partner collided\n    contact_ind = get_index_of_collision(concuss, partner)\n    \n    # punter's initial position\n    punter_init = ngs.loc[(ngs.GSISID==gsisid['punter'])&(ngs.Event=='ball_snap')]\n\n    diffx = abs(np.array(concuss.loc[contact_ind, ['x']]) - np.array(punter_init['x']))\n    if max(diffx) < 15:\n        return True\n    else:\n        return False    \n\n\n'''\nPlot football field, credit to https://www.kaggle.com/coakeson/watch-the-videos-showing-injury\n'''\ndef plot_field():\n    fontsize = 18\n    \n    # Normal length of field is 120 yards\n    plt.xlim(-10, 130)\n    plt.xticks(np.arange(0, 130, step=10),\n               ['End', 'Goal Line', '10', '20', '30', '40', '50', '40', '30', '20', '10', 'Goal Line', 'End'])\n    # Normal width is 53.3 yards\n    plt.ylim(-10, 65)\n    plt.yticks(np.arange(0, 65, 53.3), ['Sideline', 'Sideline'])\n    plt.title('Playing Field', fontsize=fontsize)\n    plt.xlabel('yardline', fontsize=fontsize)\n    plt.ylabel('width of field', fontsize=fontsize)       \n    \n'''\nPlot a play\n- parameters:\n    - ngs, NGS data of all players in a play\n    - gsisid, a dict of GSISID of players of interest\n    - players, a play from play_player_role_data.csv\n'''    \ndef plot_play(ngs, gsisid, players):\n    # get the route of concussed player and the partner\n    concuss = get_route(ngs, gsisid['injury'])\n    partner = get_route(ngs, gsisid['partner'])\n    \n    # compute their speed\n    speed_concuss = concuss['dis']/0.1\n    speed_partner = partner['dis']/0.1    \n    \n    # get the index of the route when the concussed player and the partner collided\n    contact_ind = get_index_of_collision(concuss, partner)\n    \n    # plot the route of the concussed player and the partner, and color coding the speed. \n    # Red for concussed player and blue for the partner\n    sns.set()\n    plt.figure(figsize=(15,7.5))\n    cmap = plt.get_cmap('Spectral')\n    vmax = 10\n    plt.scatter(concuss['x'], concuss['y'], c=-1.0*speed_concuss, cmap=cmap, alpha=0.8, vmin=-vmax, vmax=vmax) \n    if not np.isnan(gsisid['partner']):\n        plt.scatter(partner['x'], partner['y'], c=speed_partner, cmap=cmap, alpha=0.8, vmin=-vmax, vmax=vmax)        \n    plt.clim(-vmax, vmax)\n    plt.colorbar(label='yards/sec')\n    \n    # plot initial player location\n    init_pos = ngs.loc[(ngs.Event=='ball_snap'), ['GSISID', 'x','y','o', 'dir']]  \n    init_pos = pd.merge(init_pos, players, on=['GSISID'])\n    plot_init_xy_head(init_pos)\n    \n    # plot collision location, head direction of the blocked player, and movement direction of the blocker\n    if not np.isnan(contact_ind):        \n        blocked_uv, blocking_uv = plot_collision(\n                concuss, partner, gsisid['blocking'], contact_ind, advance=3)    \n        \n    # plot route of the punt returner\n    returner = get_route(ngs, gsisid['returner'])\n    returner = remove_route_after_play(returner, concuss.shape[0])\n    plot_route_line(returner, True)    \n    \n    # label events of interest\n    label_event_on_route(concuss, partner, returner)      \n    \n    plot_field()\n    plt.show()   \n\n    # statistics of interest for quantitative analysis\n    out = {'blocking_dir': blocking_uv,\n           'blocked_o': blocked_uv,\n           'punter_init': init_pos.loc[(init_pos.Role=='P',['x','y'])],\n           'returner_init': init_pos.loc[(init_pos.Role=='PR',['x','y'])],\n           'is_blocker_injured': gsisid['blocking'] == concuss.at[0,'GSISID']}\n\n    return out\n\n\n# set font for figures\nfont = {'weight' : 'bold',\n        'size'   : 18}\npltrc('font', **font)\npd.set_option('display.max_colwidth', -1)","execution_count":null,"outputs":[]},{"metadata":{"_uuid":"55382aef1488ac85d64977efe344cef0863f3443"},"cell_type":"markdown","source":"All the libraries and functions are loaded, below is the main loop to plot the plays."},{"metadata":{"trusted":false,"_uuid":"bfbab10d0fd1b376c213233712bf2eca562f2841"},"cell_type":"code","source":"datapath = '../input/'\n    \n# load data frames in need\nplayer_roles = pd.read_csv(datapath + 'play_player_role_data.csv')\nvideo = pd.read_csv(datapath + 'video_footage-injury.csv')\nreview = pd.read_csv(datapath + 'video_review.csv')\n\n# convert Primary_Partner_GSISID to numeric values, and discard plays that do not have a partner\nreview.loc[:,'Primary_Partner_GSISID'] = pd.to_numeric(\n    review.loc[:,'Primary_Partner_GSISID'].copy(), errors='coerce')\nreview = review.loc[~pd.isna(review.Primary_Partner_GSISID)]\n\n# select only blocking-induced injuries, and exclude those were caused by friendly-fire\nblock_injury = review[(review.Player_Activity_Derived.isin(['Blocked','Blocking'])&\n                       (review.Friendly_Fire=='No'))]\nblock_injury.reset_index(drop=True, inplace=True)\n\n# get NGS data for the injury plays\nngs_injury_file = Path('ngs_injury.csv')\nif ngs_injury_file.is_file():\n    ngs_concussion = pd.read_csv(ngs_injury_file)\nelse:\n    ngs_concussion = extract_injury_play_NGS(outname=ngs_injury_file, \n                                             datapath=datapath)\n# list all the return team positions\nreturn_team_roles = ['PDL2', 'PDR3', 'PLR2', 'PDR4', 'VRi', 'VRo', 'VLo', \n                     'PDL3', 'PLL', 'PLL2', 'PDR2', 'PDL5', 'PLM', 'PR', \n                     'PDL4', 'VL', 'PDL1', 'PDR1', 'PDR5', 'PDM', 'PDL6', \n                     'PDR6', 'PFB', 'PLL1', 'VR', 'PLR', 'PLR3', 'PLR1', 'PLM1', \n                     'PLL3', 'VLi']  \n    \n# loop through each play do plot the play and extract statistics of interest    \nsoi = [] # statistics of interest\nfor i in range(len(block_injury)):\n\n    # print info of this play\n    print('***** ', i, ' *****')\n    print(block_injury.iloc[i])\n\n    # Get necessary values for query of NGS data\n    gamekey = block_injury.loc[i, 'GameKey']\n    playid = block_injury.loc[i, 'PlayID']\n    players = player_roles[(player_roles.GameKey==gamekey)&\n                            (player_roles.PlayID==playid)].copy()\n    players.reset_index(drop=True, inplace=True)\n    \n    # select NGS data of this play \n    play_ngs = ngs_concussion[(ngs_concussion.GameKey==gamekey)&\n                             (ngs_concussion.PlayID==playid)].copy()\n    play_ngs.reset_index(drop=True, inplace=True)\n        \n    # players of interest in this plot\n    gsisid = {'injury': block_injury.loc[i, 'GSISID'], \n              'partner': block_injury.loc[i, 'Primary_Partner_GSISID'], \n              'punter': get_GSISID_by_role(players, 'P'), \n              'returner': get_GSISID_by_role(players, 'PR')}\n    gsisid = who_is_blocking(block_injury.iloc[i], gsisid)\n    \n    # discard plays where injuried occured at line of scrimmage\n    if injury_at_return_team_backfield(play_ngs, gsisid):\n        print('discard the play because injury occured at the line of scrimmage')\n        continue\n    \n    # plot the play and gather statistics\n    out = plot_play(play_ngs, gsisid, players)\n    soi.append(out)\n\n    # print out the video of this play\n    print(video.loc[(video.gamekey==gamekey)&(video.playid==playid), 'PREVIEW LINK (5000K)'])    ","execution_count":null,"outputs":[]},{"metadata":{"_uuid":"3592ce208cff73539090e7d66770bc62451b9efb"},"cell_type":"markdown","source":"Now we have seen the routes, the head direction of the blocked players, and the movement direction of the blocking players.  Here all the directions were aligned and pulled together in the same figure.  The punter always punted from the right side of the figure to the left.  "},{"metadata":{"trusted":false,"_uuid":"4fd77615dd79be9338ccbb50c16fa42d06afc0b4"},"cell_type":"code","source":"n_inj = len(soi) # the number of blocking-induced injury plays\nblocking_dir = np.zeros((n_inj,2))\nblocked_o = np.zeros((n_inj,2)) \nis_blocker_injured = np.zeros((n_inj,1))\n    \n# reformat the list of dict into np arrays    \nfor i in range(n_inj):\n    \n    # align punting to the same direction \n    if soi[i]['punter_init']['x'].unique() > soi[i]['returner_init']['x'].unique():\n        reverse = 1\n    else:\n        reverse = -1\n        \n    blocking_dir[i,0] = reverse*soi[i]['blocking_dir'][0]    \n    blocking_dir[i,1] = soi[i]['blocking_dir'][1]        \n    blocked_o[i,0] = reverse*soi[i]['blocked_o'][0]    \n    blocked_o[i,1] = soi[i]['blocked_o'][1]       \n    is_blocker_injured[i] = soi[i]['is_blocker_injured'] \n\n# set font for figures\nfont = {'weight' : 'bold',\n        'size'   : 18}\npltrc('font', **font)    \n    \n# plot it    \nplt.figure(figsize=(8,5))\nplt.quiver(np.zeros((n_inj,1)), np.zeros((n_inj,1)), blocked_o[:,0], blocked_o[:,1],\n           color=[1,0.6,0], units='width', scale=35)\nplt.quiver(np.zeros((n_inj,1)), np.zeros((n_inj,1)), blocking_dir[:,0], blocking_dir[:,1],\n           color=[0,0.6,1], units='width', scale=35)\nplt.xticks([0],[])\nplt.yticks([0],[])\nplt.legend(['blocked, head direction','blocking, movement direction'], \n           fontsize=14, loc=1)\n\nn_blocked_backdir = sum(blocked_o[:,0]<=0)\nn_blocking_backdir = sum(blocking_dir[:,0]<=0)\n\nprint('Out of {} injuries, '.format(n_inj))     \nprint('{} ({:.2f}%) blocking in the backfield direction,'.format(n_blocking_backdir, 100*n_blocking_backdir/n_inj))\nprint('{} ({:.2f}%) blocked players were facing backfield'.format(n_blocked_backdir, 100*n_blocked_backdir/n_inj))\nprint('* Backfield from the perspective of the return team')","execution_count":null,"outputs":[]},{"metadata":{"_uuid":"e9f2fc3b09512b3dfec54b3412eff23bbb3b6f7f"},"cell_type":"markdown","source":"Plots P(X|concussion).  Given that a player was concussed in a play, what's the probability that he was blocking or being blocked."},{"metadata":{"trusted":false,"_uuid":"245b2943ea4e8b61d183a550bc570fc7f9aa918c"},"cell_type":"code","source":"# %%\nplt.figure(figsize=(5,5))\nplt.bar(['Blocked', 'Blocking'], \n        [np.sum(is_blocker_injured==0)/is_blocker_injured.size, \n         np.sum(is_blocker_injured==1)/is_blocker_injured.size],\n        alpha=0.3, width=0.35, color='blue')\nplt.ylim([0,1])\nplt.xlim([-1,2])\nplt.ylabel('P(X|concussion)', fontsize=18)","execution_count":null,"outputs":[]},{"metadata":{"trusted":false,"_uuid":"9106e4209a8b197f13d67b9882fcbe09e8c705f7"},"cell_type":"code","source":"","execution_count":null,"outputs":[]}],"metadata":{"kernelspec":{"display_name":"Python 3","language":"python","name":"python3"},"language_info":{"name":"python","version":"3.6.6","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"}},"nbformat":4,"nbformat_minor":1}