{"cells":[{"metadata":{},"cell_type":"markdown","source":"# The issue\nThe purpose of this notebook is to adjust player orientation between the two seasons for which we have data. The orientation issue is described in [this post](https://www.kaggle.com/c/nfl-big-data-bowl-2020/discussion/113384) from Peter Hurford as part of the NFL DataBowl challenge.\n\nASSUMPTION: We are given data for 2017 and 2018, or at least for two seasons with the same apparent difference in measuring orientation.\n\nCAUTION: Use this data adjustment at your own risk. It may or may not be valid. But it probably is directionally correct."},{"metadata":{},"cell_type":"markdown","source":"# Evidence\n\nHere you see the distribution of quarterback directions at the time of ball snap. It makes sense that the peaks are at 90 and 270 depending on whether the offense is moving left or right. "},{"metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"_kg_hide-input":true},"cell_type":"code","source":"import numpy as np\nimport pandas as pd\nimport dask.dataframe as dd\nimport matplotlib.pyplot as plt\n\nplaylist = dd.read_csv('../input/nfl-playing-surface-analytics/PlayList.csv')\ntracks = dd.read_csv('../input/nfl-playing-surface-analytics/PlayerTrackData.csv', \n                     usecols=['PlayKey', 'event', 'dir', 'o'])\n\nplays = playlist.loc[playlist.Position == 'QB', 'PlayKey'].unique().compute().tolist()\nsnaps = tracks[(tracks.PlayKey.isin(plays)) & (tracks.event == 'ball_snap')]\nsnaps_df = snaps.compute()\nsnaps_df.dir.plot.hist(bins=90, xticks = np.arange(0, 361, 45))","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"Compare that plot with the distribution of orientations. Peaks are at 0, 90, 180, and 270. The difference indicates that we may have a 90 degree difference between the two seasons."},{"metadata":{"trusted":true,"_kg_hide-input":true},"cell_type":"code","source":"snaps_df.o.plot.hist(bins=90, xticks = np.arange(0, 361, 45))\n","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"# Insight\n\nTo make the adjustment, we need to separate the data by seasons. We can use PlayerDays as a clue along with the knowledge that the regular season runs Sep-Dec each year. Here are the top values of the last player day for each player."},{"metadata":{"trusted":true,"_kg_hide-input":true},"cell_type":"code","source":"playlist_df = playlist.compute()\nprint(playlist_df.groupby('PlayerKey')['PlayerDay'].max().value_counts().head())","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"The top two values are 113 and 477. 113 is the approximate length of one season. 477 is the approximate length of two seasons spearated by a 8-month gap between seasons. This chart of plays shows the timing for players making it through 350 days or more. Notice the dead space between seasons."},{"metadata":{"trusted":true,"_kg_hide-input":true},"cell_type":"code","source":"tough_guys = playlist_df.loc[playlist_df.PlayerDay >= 350, 'PlayerKey'].unique()\nplaylist_tough = playlist_df[playlist_df.PlayerKey.isin(tough_guys)].copy()\ndays = playlist_tough.groupby('PlayerDay')['PlayerGamePlay'].mean()\ndays.reset_index().plot.scatter('PlayerDay', 'PlayerGamePlay')","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"We can separate plays by season based on this knowledge!"},{"metadata":{},"cell_type":"markdown","source":"# The fix\nThe first thing to do is separate games by season. Here I take an oversimplified approach to show proof of concept. Only players with over 350 days are included, and any player day over 350 is considered as part of the 2nd season. Next we rotate all orientations from the 1st season by 90 degrees. The NFL DataBowl competition has more examples of how to adjust orientations. Here we see the adjusted distribution overlayed onto the directions."},{"metadata":{"trusted":true,"_kg_hide-input":true},"cell_type":"code","source":"playlist_tough['Season'] = np.where(playlist_tough.PlayerDay<350, 1, 2)\ngames = playlist_tough.drop_duplicates('GameID')[['GameID', 'Season']]\n\nsnaps_tough = snaps_df.merge(playlist_tough[['GameID', 'PlayKey']], on='PlayKey')\nsnaps_tough = snaps_tough.merge(games, on='GameID')\n\nsnaps_tough['o'] = np.where(snaps_tough.Season == 1,\n                            np.mod(snaps_tough.o+90, 360),\n                            snaps_tough.o\n                            )\n\ndisplay(snaps_tough.dir.plot.hist(bins=90, alpha=0.8),\n        snaps_tough.o.plot.hist(bins=90, alpha=0.8, xticks = np.arange(0, 361, 45))\n       )","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"This output looks more like what we expect to see."},{"metadata":{},"cell_type":"markdown","source":"# Edit: Alternate Method\nNot all players played for two full seasons. Another method you can try is to compare the difference between direction traveled and orientation. They should be generally the same during the play. In the code below I change the angles so that 0 degrees is along the x axis toward the goal line. You can see below that it's consistent with angles based on x-y coordinates."},{"metadata":{"trusted":true},"cell_type":"code","source":"tracks = dd.read_csv('../input/nfl-playing-surface-analytics/PlayerTrackData.csv')\none_play = tracks[tracks.PlayKey == '47820-1-2'].compute()","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"start_time = one_play.loc[one_play.event == 'ball_snap', 'time'].to_numpy()[0]\nend_time = one_play.loc[one_play.event == 'out_of_bounds', 'time'].to_numpy()[0]\nplay = one_play[one_play.time.between(start_time, end_time, inclusive=True)].copy()\n\nplay['dir_calcd'] =  360 - np.arctan2((play.y-play.y.shift()), \n                                      (play.x-play.x.shift())) *180/np.pi\nplay['dir_fixd'] = np.mod(play.dir+270, 360)\nplay['o_fixd'] = np.mod(play.o+270, 360)\ndisplay(play.head(10))","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"(play.dir_fixd - play.o_fixd).hist()","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"My calcs may not be quite right, but it appears that orientation should be shifted for this play."},{"metadata":{},"cell_type":"markdown","source":"# A lingering question\n\nOne might ask why the seasons aren't explicitly given in the data. More generally, it's not clear why games, players, locations, etc. aren't clearly identified. I'm sure there's a good reason why our NFL hosts have masked the data. Please share any thoughts or opinions. And, good luck with your analysis!"}],"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"pygments_lexer":"ipython3","nbconvert_exporter":"python","version":"3.6.4","file_extension":".py","codemirror_mode":{"name":"ipython","version":3},"name":"python","mimetype":"text/x-python"}},"nbformat":4,"nbformat_minor":1}