{"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_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"# Table of content\n\n* [Introduction](#intro)\n* [Kickoff as a special team play](#kickoff)\n* [Outcomes of a kickoff](#outcome)\n* [Gained yards by kicking teams](#gainedyards)\n* [Kick the ball far, if you want to gain more yards](#far)\n* [Initial analysis - How to kick the ball far](#initial)\n* [Variable 1 - Foot speed](#speed)\n* [Variable 2 - Body orientation](#orientation)\n* [Conclusion and future research](#conclusion)\n* [Reference](#reference)","metadata":{}},{"cell_type":"markdown","source":"# Introduction <a id=\"intro\"></a>\n\n**\"Kick the ball harder, if you want to kick it further.\"** This is what I learned from a lot of people saying. \n\nBut how true is it? Or what does it exactly mean when we say \"kicking it harder\"? Data from NFL games reveal to us some insight which I'd like to share with you in this notebook.\n\nKickoff play is the most common type in special plays from season 2018 to 2020. Even though with six different outcomes, the kicking team has basically one ultimate goal - gaining as many yards as possible after the session ends. In this notebook, I'd bring up the **kick length** of the ball as an actionable and robust metric to evaluate how good a kickoff it is. This metric has a high positive association with the number of gained yards, since the receiving team needs to return the ball from a very low yard line if the ball is kicked long distance. Apart from that, with using the multiple linear regression method, I also find two relevant variables which might affect the kick length. Those are \n\n1. **Foot speed** in the approach, usually seen in a long approach distance, and it is also a predictor of the last step length, which is related with how much engergy will be transmitted to the ball, according to the formula of kinetic energy.\n\n2. The **direction of the ball being kicked to**. If the ball was kicked to the right side of the kicker, the length is usually shorter than to the center or to the left. The hypothesis behind is kicking to the right takes more energy from the body because of a larger pivoting angle. This cost of energy results in less volume brought to the ball.\n\n","metadata":{}},{"cell_type":"code","source":"# This Python 3 environment comes with many helpful analytics libraries installed\n# It is defined by the kaggle/python Docker image: https://github.com/kaggle/docker-python\n# For example, here's several helpful packages to load\n\nimport numpy as np # linear algebra\nimport pandas as pd # data processing, CSV file I/O (e.g. pd.read_csv)\nimport statsmodels.api as sm # using linear regression library for analysis\nfrom scipy.stats import pearsonr # calculating pearson correlation\n\n# Input data files are available in the read-only \"../input/\" directory\n# For example, running this (by clicking run or pressing Shift+Enter) will list all files under the input directory\n\n# ployly is the main tool for visualization in this notebook\nimport plotly.express as px\nimport plotly.graph_objects as go\nfrom plotly.subplots import make_subplots\n\n# make sure the code in plotly is able to run properly\nfrom plotly.offline import plot, iplot, init_notebook_mode\ninit_notebook_mode(connected=True)\n\n\n# Style for the visualization\nfonts = ['Rockwell','Oswald','Lato']\ncolors = ['#013369','#013f82','#0264ce','#d50a0a','#ffffff']\n\n# remove unnecessary warnings in the output\npd.options.mode.chained_assignment = None  # default='warn'\n","metadata":{"_kg_hide-input":true,"execution":{"iopub.status.busy":"2022-01-05T13:25:15.201620Z","iopub.execute_input":"2022-01-05T13:25:15.202703Z","iopub.status.idle":"2022-01-05T13:25:15.213258Z","shell.execute_reply.started":"2022-01-05T13:25:15.202628Z","shell.execute_reply":"2022-01-05T13:25:15.212642Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"# Kickoff as a special team play <a id=\"kickoff\"></a>\n\n**Special teams** are units that are on the field during kicking plays. While many players who appear on offensive or defensive squads also play similar roles on special teams (offensive linemen to block or defensive players to tackle), there are some specialist roles that are unique to the kicking game([1](https://en.wikipedia.org/wiki/American_football_positions#Special_teams)).\n\nIn a special play, it usually has 4 play types - those are \n* Kickoff\n* Punt\n* Extra Point\n* Field Goal\n\n**Kickoff** is the most commont one in terms of number. By definition in NFL official website, It is a kick that puts the ball in play at the start of each half, at the start of overtime, after each try, and after a successful field goal([2](https://operations.nfl.com/learn-the-game/nfl-basics/terms-glossary/)). Because of its popularity, I'm interested to dig it further so that we can know how to evaluate the performance of a kicking team.","metadata":{}},{"cell_type":"code","source":"# fetch play data\nplay_data = pd.read_csv(\"/kaggle/input/nfl-big-data-bowl-2022/plays.csv\")\n\n# fetch game data\ngame_data = pd.read_csv(\"/kaggle/input/nfl-big-data-bowl-2022/games.csv\")\n\n# join play_data and game_data on gameId and playId\ngamePlay_data = pd.merge(\n    play_data,\n    game_data,\n    how=\"inner\",\n    left_on=[\"gameId\"],\n    right_on=[\"gameId\"],\n    sort=True,\n    suffixes=(\"_x\", \"_y\"),\n    copy=True,\n    indicator=False,\n    validate=None,\n)\n\n# select 'season','specialTeamsPlayType','gameId','playId' columns into a new table season_play\nseason_play = gamePlay_data[['season','specialTeamsPlayType','gameId','playId']]\n\n# create a unique id for each play per game\nseason_play['uniqueId'] = season_play['gameId'].astype(str) + season_play['playId'].astype(str)\n\n# get number of plays per specialTeamsPlayType during the three seasons\nseason_play_number = season_play.groupby(['season','specialTeamsPlayType']).nunique().reset_index()[['season','specialTeamsPlayType','uniqueId']]\nseason_play_number['total'] = season_play_number.groupby('season')['uniqueId'].transform('sum')\n\n# get the percentage of each type per year\nseason_play_number['perc_type'] = round(season_play_number['uniqueId'] * 100 / season_play_number['total'],1)\n\n# Visualize data into a stacked bar chart\nx=list(set(season_play_number['season'].astype(str)))\ny_ExtraPoint = season_play_number.loc[season_play_number['specialTeamsPlayType'] == 'Extra Point']['perc_type']\ny_FieldGoal = season_play_number.loc[season_play_number['specialTeamsPlayType'] == 'Field Goal']['perc_type']\ny_Kickoff = season_play_number.loc[season_play_number['specialTeamsPlayType'] == 'Kickoff']['perc_type']\ny_Punt = season_play_number.loc[season_play_number['specialTeamsPlayType'] == 'Punt']['perc_type']\n\nfig = go.Figure(go.Bar(x=x, y=y_Kickoff, \n                       name='Kickoff',\n                       width = 0.35,\n                       hovertemplate =\n                            '<i>Season</i>: %{x} <br>' + \n                            '<i>% of total plays</i>: %{y}',\n                      marker_color=colors[3]))\nfig.add_trace(go.Bar(x=x, y=y_Punt, \n                     name='Punt',\n                     width = 0.35,\n                    hovertemplate =\n                    '<i>Season</i>: %{x} <br>' + \n                    '<i>% of total plays</i>: %{y}',\n                    marker_color=colors[0]))\nfig.add_trace(go.Bar(x=x, y=y_ExtraPoint, \n                     name='Extra Point',\n                     width = 0.35,\n                    hovertemplate =\n                    '<i>Season</i>: %{x} <br>' + \n                    '<i>% of total plays</i>: %{y}',\n                    marker_color=colors[1]))\nfig.add_trace(go.Bar(x=x, y=y_FieldGoal, \n                     name='Field Goal',\n                     width = 0.35,\n                    hovertemplate =\n                    '<i>Season</i>: %{x} <br>' + \n                    '<i>% of total plays</i>: %{y}',\n                    marker_color=colors[2]))\n\n# update layout of the chart\nfig.update_yaxes(tickvals=[25, 50, 75, 100])\n\nfig.update_layout(title='Number of special plays per type from 2018 to 2020 <br><sup>Kickoff is the most common type, with a yearly average percentage over 37%.</sup>',\n                  paper_bgcolor='rgba(0,0,0,0)',\n                  plot_bgcolor='rgba(0,0,0,0)',\n                  autosize = True,\n                  font=dict(\n                            family=fonts[0],\n                            size=14\n                  ),\n                  xaxis_tickfont_size=14,\n                  yaxis=dict(\n                          title='% of total plays',\n                          titlefont_size=16,\n                          tickfont_size=14,\n                    ),\n                  hoverlabel=dict(\n                    bgcolor=\"white\",\n                    font_size=14,\n                    font_family=\"Rockwell\"\n                    ),\n                  legend=dict(\n                    #x=0,\n                    #y=1.0,\n                    bgcolor='rgba(255, 255, 255, 0)',\n                    bordercolor='rgba(255, 255, 255, 0)',\n                    font=dict(\n                            size=12\n                        ),\n                    ),\n                  bargap=0.05,\n                  bargroupgap=0.05,\n                  barmode='stack', \n                  xaxis={'categoryorder':'category ascending'})\n\nfig.show()","metadata":{"_kg_hide-input":true,"jupyter":{"source_hidden":true},"execution":{"iopub.status.busy":"2022-01-05T13:25:15.214766Z","iopub.execute_input":"2022-01-05T13:25:15.215693Z","iopub.status.idle":"2022-01-05T13:25:15.629847Z","shell.execute_reply.started":"2022-01-05T13:25:15.215648Z","shell.execute_reply":"2022-01-05T13:25:15.628729Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"# Outcomes of a kickoff <a id=\"outcome\"></a>\n\nThere are seven outcomes in general. **Touchback** and **return** are the most two popular ones. In 2018, in order to make the game more safer for both sides, and avoid unneccessary collisions. NFL introduced new rules on the touchback -\n\n> Kickoffs that hit the end zone without being touched by a member of the receiving team automatically become touchbacks([3](https://profootballtalk.nbcsports.com/2018/07/06/another-tweak-to-the-kickoff-rule-promotes-more-touchbacks/)).\n\nFor years, the automatic touchback rule has applied to punts that enter the end zone, with or without being touched. For kickoffs, the touchback becomes automatic only if it strikes the ground in the end zone without being touched by a member of the receiving team; the player can still catch the kickoff and choose to return it. It’s not a change that will come into play very often, but it’s another example of the league’s broader effort to encourage touchbacks on kickoffs.\n\n\nThis policy pushes the touchback to a more popular status since 2018, as we can see it in the chart below. ","metadata":{}},{"cell_type":"code","source":"# get data where play type is kickoff\nkickoff_data = gamePlay_data.loc[gamePlay_data['specialTeamsPlayType'] == 'Kickoff']\n\n# create a unique id\nkickoff_data['uniqueId'] = kickoff_data['gameId'].astype(str) + kickoff_data['playId'].astype(str)\nkickoff_data_result = kickoff_data[['season','uniqueId','specialTeamsResult']]\n\n# get number unique id per specialTeamsResult\nkickoff_data_result_aggr = kickoff_data_result.groupby(['specialTeamsResult']).nunique().reset_index()[['specialTeamsResult','uniqueId']]\n\n# get the total number of plays \nkickoff_data_result_aggr['total'] = kickoff_data_result_aggr['uniqueId'].sum()\n\n# get the percentage of plays out of total per result\nkickoff_data_result_aggr['share_of_total'] = round(kickoff_data_result_aggr['uniqueId'] * 100 / kickoff_data_result_aggr['total'],1)\n\n# get number unique id per specialTeamsResult per year\nkickoff_data_result_year_aggr = kickoff_data_result.groupby(['season','specialTeamsResult']).nunique().reset_index()[['season','specialTeamsResult','uniqueId']]\n\n# get number plays per year\nkickoff_data_result_year_aggr['total'] = kickoff_data_result_year_aggr.groupby('season')['uniqueId'].transform('sum')\nkickoff_data_result_year_aggr['share_of_total'] = round(kickoff_data_result_year_aggr['uniqueId'] * 100 / kickoff_data_result_year_aggr['total'],1)\n\n# get the share of plays are touchback in the three seasons\nkickoff_data_result_year_touchback = kickoff_data_result_year_aggr.loc[kickoff_data_result_year_aggr['specialTeamsResult'] == 'Touchback']\n# turn season data into categorical\nkickoff_data_result_year_touchback['season'] = kickoff_data_result_year_touchback['season'].astype(str)","metadata":{"_kg_hide-input":true,"jupyter":{"source_hidden":true},"execution":{"iopub.status.busy":"2022-01-05T13:25:15.631882Z","iopub.execute_input":"2022-01-05T13:25:15.632234Z","iopub.status.idle":"2022-01-05T13:25:15.702347Z","shell.execute_reply.started":"2022-01-05T13:25:15.632181Z","shell.execute_reply":"2022-01-05T13:25:15.701427Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# visualize the chart \nfig = make_subplots(rows=1, cols=2,\n                    subplot_titles=(\"Touchback is the most common one\", \n                                    \"...and its popularity has slightly increased in the three years\"),\n                    vertical_spacing=0.1,\n                    column_widths=[0.55, 0.45])\n\nfig.add_trace(\n    go.Bar(\n        x=kickoff_data_result_aggr['specialTeamsResult'], \n        y=kickoff_data_result_aggr['uniqueId'],\n        name = '',\n        hovertemplate =\n                    '<i>Play result</i>: %{x} <br>' + \n                    '<i>Nr. of plays</i>: %{y}',\n        marker_color=colors[0]\n          ),\n    row=1, col=1\n)\n\nfig.add_trace(\n    go.Scatter(\n        x=kickoff_data_result_year_touchback['season'], \n        y=kickoff_data_result_year_touchback['share_of_total'],\n        name = '',\n        hovertemplate =\n                    '<i>Season</i>: %{x} <br>' + \n                    '<i>% of plays end up as touchback</i>: %{y}',\n        marker_color=colors[3]\n              ),\n    row=1, col=2\n)\n\n# update chart layout\nfig.update_layout(\n                  title_text=\"Out of the seven results in kickoff plays,\",\n                  paper_bgcolor='rgba(0,0,0,0)',\n                  plot_bgcolor='rgba(0,0,0,0)',\n                  font=dict(\n                            family=fonts[0],\n                            size=14\n                  ),\n                  xaxis_tickfont_size=14,\n                  yaxis=dict(\n                          #title='Nr of plays',\n                          titlefont_size=16,\n                          tickfont_size=14,\n                    ),\n                  hoverlabel=dict(\n                    bgcolor=\"white\",\n                    font_size=14,\n                    font_family=\"Rockwell\"\n                    ),\n                  #showlegend=True,\n                  bargap=0.05,\n                  bargroupgap=0.05,\n                  barmode='stack', \n                  xaxis={'categoryorder':'category ascending'}\n)\n\nfig.update_yaxes(title_text=\"Nr of plays\", \n                 tickvals=[2000, 3500, 5000],\n                 row=1, col=1)\nfig.update_yaxes(title_text=\"touchback perc. (%)\", \n                 #tickvals=[60.5, 61, 61.5],\n                 row=1, col=2)\n\nfig.show()","metadata":{"_kg_hide-input":true,"jupyter":{"source_hidden":true},"execution":{"iopub.status.busy":"2022-01-05T13:25:15.705176Z","iopub.execute_input":"2022-01-05T13:25:15.705681Z","iopub.status.idle":"2022-01-05T13:25:15.776389Z","shell.execute_reply.started":"2022-01-05T13:25:15.705634Z","shell.execute_reply":"2022-01-05T13:25:15.775722Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"To understand how does each outcome exactly mean, I've created an interactive visualization below, showing the ball route per outcome and a table containg the definition.","metadata":{}},{"cell_type":"code","source":"# Kickoff data\nkickoff_play = play_data.loc[play_data['specialTeamsPlayType'] == 'Kickoff']\n\n# load tracking data\ntracking_datafile = [\n                    \"/kaggle/input/nfl-big-data-bowl-2022/tracking2018.csv\",\n                    \"/kaggle/input/nfl-big-data-bowl-2022/tracking2019.csv\",\n                    \"/kaggle/input/nfl-big-data-bowl-2022/tracking2020.csv\"\n                    ]\n\n# merge tracking data with play data\nkickoff_tracking_dataset = []\nfor file in tracking_datafile:\n    data = pd.read_csv(file)\n    # select rows relevent with the route of the ball\n    data = data.loc[data['displayName'] == 'football']\n    # join kickoff_play and tracking_data on gameId and playId\n    kickoff_tracking_data = pd.merge(\n    kickoff_play,\n    data,\n    how=\"inner\",\n    left_on=[\"gameId\",\"playId\"],\n    right_on=[\"gameId\",\"playId\"],\n    sort=True,\n    suffixes=(\"_x\", \"_y\"),\n    copy=True,\n    indicator=False,\n    validate=None,\n    )\n    kickoff_tracking_dataset.append(kickoff_tracking_data)\n\n# merge three seasons into one dataset\nkickoff_tracking_dataset = pd.concat(kickoff_tracking_dataset)\n\n# create unique id\nkickoff_tracking_dataset['uniqueId'] = kickoff_tracking_dataset['gameId'].astype(str) + kickoff_tracking_dataset['playId'].astype(str)\n# select revelant columns\nball_route_dataset = kickoff_tracking_dataset[['uniqueId','specialTeamsResult','playResult',\n                         'time', 'x', 'y', 's', 'a', 'dis','event','playDirection']]\n\n# creating a new column - showing the time of each move start from time zero\nball_route_dataset['rank'] = ball_route_dataset.groupby(\"uniqueId\")[\"time\"].rank(\"dense\", ascending=True)\nball_route_dataset['sec'] = ball_route_dataset['rank'] * 0.1 - 0.1","metadata":{"_kg_hide-input":true,"execution":{"iopub.status.busy":"2022-01-05T13:25:15.777533Z","iopub.execute_input":"2022-01-05T13:25:15.778438Z","iopub.status.idle":"2022-01-05T13:27:46.070195Z","shell.execute_reply.started":"2022-01-05T13:25:15.778398Z","shell.execute_reply":"2022-01-05T13:27:46.068941Z"},"jupyter":{"source_hidden":true},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# visualize the ball route per kickoff outcome\n\n# taking one sample play per one outcome\nball_route_right = ball_route_dataset.loc[ball_route_dataset['playDirection'] == 'right']\n\nunique_ids = [\n\"20210103152847\", # Touchback\n\"20210103154182\", # Return\n\"2020120611395\", # Muffed\n\"202101030239\", # Out of Bounds\n\"20181111003659\", # Kickoff Team Recovery\n\"20190922044363\", # Fair Catch\n\"20201108043603\" # Downed\n            ]\n\n# visualizing ball routes and the field\n# make the chart interactive so that the reader can choose the type he or she is interested\nfig = go.Figure()\n\nfigs = []\ncount = 0\nfor i in unique_ids:\n    sample = ball_route_right.loc[ball_route_right['uniqueId'] == i]\n    sample['x'] = sample['x'] - 10\n    # set the first trace visible, otherwise not once the fig is loaded\n    if count == 0:\n        # add ball route\n        f = fig.add_trace(go.Scatter(x=sample['x'], \n                         y=sample['y'],\n                            # yaxis='y0',\n                        mode='lines+markers+text',\n                        text = [None if x=='None' else x for x in sample['event'].values],\n                        textposition=\"top center\",\n                        textfont=dict(\n                                family=fonts[0],\n                                size=12,\n                                color= colors[0]\n                                ),\n                        name=\"\",\n                        hovertemplate =\n                        '<i>x</i>: %{x} <br>' + \n                        '<i>y</i>: %{y} <br>',\n                        #'<i>event</i>: %{text} <br>' ,\n                        marker_color=np.where(sample['event'] == 'None', colors[0], colors[3]),\n                        line_color = colors[0],\n                        #marker_color=colors[0],\n                        visible = True,\n                        opacity = 0.75))\n    else:\n        # add ball route\n        f = fig.add_trace(go.Scatter(x=sample['x'], \n                         y=sample['y'],\n                            # yaxis='y0',\n                        mode='lines+markers+text',\n                        text = [None if x=='None' else x for x in sample['event'].values],\n                        textposition=\"top center\",\n                        textfont=dict(\n                                family=fonts[0],\n                                size=12,\n                                color= colors[0]\n                                ),\n                        name=\"\",\n                        hovertemplate =\n                        '<i>x</i>: %{x} <br>' + \n                        '<i>y</i>: %{y} <br>',\n                        #'<i>event</i>: %{text} <br>' ,\n                        marker_color=np.where(sample['event'] == 'None', colors[0], colors[3]),\n                        line_color = colors[0],\n                        #marker_color=colors[0],\n                        visible = False,\n                        opacity = 0.75))\n\n    figs.append(f)\n    count += 1\n\n\n# add field visualization\nfig.add_shape(type=\"rect\",\n    x0=0, y0=0, x1=10, y1=53.5,\n    line_width = 0,\n    layer=\"below\",\n    fillcolor=\"#717785\")\n\nfig.add_shape(type=\"rect\",\n    x0=110, y0=0, x1=120, y1=53.5,\n    line_width = 0,\n    layer=\"below\",\n    fillcolor=\"#717785\")\n\n# add yard line\nfor i in range(11):\n    fig.add_shape(type=\"line\",\n    x0= (i + 1) * 10, y0= 0, \n    x1= (i + 1) * 10, y1= 53.5,\n    layer=\"below\",\n    line=dict(color=\"white\",width=2))\n    \nfor i in range(10):\n    fig.add_shape(type=\"line\",\n    x0= (i + 1) * 10 + 5, y0= 0, \n    x1= (i + 1) * 10 + 5, y1= 53.5,\n    layer=\"below\",\n    line=dict(color=\"white\",width=1.2))\n\n# add annotation\nfig.add_annotation(text=\"Home Endzone\",\n                  #xref=\"paper\", yref=\"paper\",\n                  x=5, y=26.5, showarrow=False,\n                  font = dict(\n                      size = 20,\n                      family=fonts[0],\n                      color= colors[0]\n                  ),\n                   opacity = 0.75,\n                  textangle=-90)\n\nfig.add_annotation(text=\"Visitor Endzone\",\n                  #xref=\"paper\", yref=\"paper\",\n                  x=115, y=26.5, showarrow=False,\n                  font = dict(\n                      size = 20,\n                      family=fonts[0],\n                      color= colors[0]\n                  ),\n                  opacity = 0.75,\n                  textangle=90)\n\n# add yard line number\nfor i in range(9):\n    fig.add_annotation(text=\"0\",\n                       x= (i+2) * 10 + 1, \n                       y = 3, \n                       showarrow=False, \n                       font = dict(size = 15, color = 'white'))\n\n    fig.add_annotation(text=\"0\",\n                       x= (i+2) * 10 - 1, \n                       y = 50.5, \n                       showarrow=False, \n                       font = dict(size = 15, color = 'white'))\n\nfor i in range(5):\n        fig.add_annotation(text= str(i+1),\n                       x= (i+2) * 10 - 1, \n                       y = 3, \n                       showarrow=False, \n                       font = dict(size = 15, color = 'white'))\n        fig.add_annotation(text= str(i+1),\n                       x= (i+2) * 10 + 1, \n                       y = 50.25, \n                       showarrow=False, \n                       font = dict(size = 15, color = 'white'),\n                          textangle=180)\n        \n# add yard line number\nfor i in range(4):\n        fig.add_annotation(text= str(5 - (i+1)),\n                       x= (i+7) * 10 - 1, \n                       y = 3, \n                       showarrow=False, \n                       font = dict(size = 15, color = 'white'))\n        fig.add_annotation(text= str(5 - (i+1)),\n                       x= (i+7) * 10 + 1, \n                       y = 50.25, \n                       showarrow=False, \n                       font = dict(size = 15, color = 'white'),\n                          textangle=180)\n# add goal zone line\nfig.add_annotation(text= \"G\",\n                       x=  11,\n                       y = 3,\n                       showarrow=False, \n                       font = dict(size = 15, color = 'white'))\nfig.add_annotation(text= \"G\",\n                       x= 11, \n                       y = 50, \n                       showarrow=False, \n                       font = dict(size = 15, color = 'white'),\n                  textangle=180)\n\nfig.add_annotation(text= \"G\",\n                       x=  109,\n                       y = 3,\n                       showarrow=False, \n                       font = dict(size = 15, color = 'white'))\nfig.add_annotation(text= \"G\",\n                       x= 109, \n                       y = 50, \n                       showarrow=False, \n                       font = dict(size = 15, color = 'white'),\n                  textangle=180)\n\n\n\n# remove x,y axis\nfig.update_yaxes(showticklabels=False, showgrid=False, zeroline=False)\nfig.update_xaxes(showticklabels=False, showgrid=False, zeroline=False)\n\nfig.update_layout(\n                  title_text=\"Ball route of seven kickoff results\",\n                  autosize=True,\n                  paper_bgcolor='#96b78c',\n                  plot_bgcolor='#96b78c',\n                  font=dict(\n                            family=fonts[0],\n                            size=14\n                  ),\n                  xaxis_tickfont_size=14,\n                  yaxis=dict(\n                          #title='Nr of plays',\n                          titlefont_size=16,\n                          tickfont_size=14,\n                    ),\n                  hoverlabel=dict(\n                    bgcolor=\"white\",\n                    font_size=14,\n                    font_family=\"Rockwell\"\n                    ),\n                  showlegend=False,\n                  #bargap=0.05,\n                  #bargroupgap=0.05,\n                  #barmode='stack', \n                  xaxis={'visible':False},\n                  updatemenus=[\n                    dict(\n                        type=\"buttons\",\n                        #direction=\"right\",\n                        active=0,\n                        #x=0.57,\n                        #y=1.2,\n                        buttons=list([\n                            dict(label=\"Touchback\",\n                                 method=\"update\",\n                                 args=[{\"visible\": [True, False, False,False,False,False,False]},\n                                       {\"annotations\": figs[0]}]),\n                            dict(label=\"Return\",\n                                 method=\"update\",\n                                 args=[{\"visible\": [False, True, False,False,False,False,False]},\n                                       {\"annotations\": figs[1]}]),\n                            dict(label=\"Muffed\",\n                                 method=\"update\",\n                                 args=[{\"visible\": [False, False, True,False,False,False,False]},\n                                       {\"annotations\": figs[2]}]),\n                            dict(label=\"Out of Bounds\",\n                                 method=\"update\",\n                                 args=[{\"visible\": [False, False, False,True,False,False,False]},\n                                       {\"annotations\": figs[3]}]),\n                            dict(label=\"Kickoff Team Recovery\",\n                                 method=\"update\",\n                                 args=[{\"visible\": [False, False, False,False,True,False,False]},\n                                       {\"annotations\": figs[4]}]),\n                            dict(label=\"Fair Catch\",\n                                 method=\"update\",\n                                 args=[{\"visible\": [False, False, False,False,False,True,False]},\n                                       {\"annotations\": figs[5]}]),\n                            dict(label=\"Downed\",\n                                 method=\"update\",\n                                 args=[{\"visible\": [False, False, False,False,False,False,True]},\n                                       {\"annotations\": figs[6]}])\n                        ]),\n                    )\n                ]\n)\n\nfig.show()","metadata":{"_kg_hide-input":true,"jupyter":{"source_hidden":true},"execution":{"iopub.status.busy":"2022-01-05T13:27:46.072649Z","iopub.execute_input":"2022-01-05T13:27:46.072914Z","iopub.status.idle":"2022-01-05T13:27:48.778956Z","shell.execute_reply.started":"2022-01-05T13:27:46.072884Z","shell.execute_reply":"2022-01-05T13:27:48.777631Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"| Outcome      | Definition |\n| ----------- | ----------- |\n| **Touchback**      | When a player downs the ball after a free kick behind his team’s own goal line,or the ball is kicked  <br>through the back of the end zone, the play is dead and the ball is spotted on the 25-yard line.       |\n| **Return**   | Once the receiving team possess the ball, the objective of them is to score a touchdown, <br>i.e. returning the ball to the end zone of the kicking team, though that is very unlikely on return plays.        |\n|**Muffed**|When a player touches a loose ball while unsuccessfully attempting to gain possession. <br>Muffs most frequently occur when a kick or punt returner fails to successfully execute a catch on a free kick or a punt.|\n|**Out of Bounds**|A player is out of bounds when he touches any boundary line or touches anything — except a player, <br> an official, or a pylon — that is on or outside a boundary line.|\n|**Kickoff Team Recovery**|The kickoff team gains the possession of the ball.|\n|**Fair Catch**|A player in position to receive a punt can signal for a fair catch by raising one arm above his head and waving it <br> from side to side. Once the receiver signals for a fair catch, he cannot advance the ball and the play is over when he <br>catches the ball <br>and the opponent may not interfere with or tackle him.|\n|**Downed**|The ball is caused to be out of play.|","metadata":{}},{"cell_type":"markdown","source":"# Gained yards by kicking teams <a id=\"gainedyards\"></a>\n\nHow to evaluate a kickoff is good or not? Considering the ultimate goal is to take the ball towards the end zone of the opponent side, one relevant metric could be how much yards the kickoff team gained after the play. In other words, it is the net yards gained by the kicking team, including penalty yardage, which is documented as the **playResult** in the **Play** data dataset.\n\nOn the contrary, for the receiving team, the goal of them is to limit the gained yard of the kick team as small as possible, or we can say, they should return the ball as further as they can once they possess it. \n\nIf we break down the performance by kickoff outcome, we'll see the **Return** is the most exciting one due to its wide range of potential yards the kickoff team can gain. Unlike the other types of outcome, a kicking team could possibly lose yards in the play which makes the game a bit risky. But on the other hand, they could also gain yards over 40 yards in the return.","metadata":{}},{"cell_type":"code","source":"# select column 'uniqueId','playResult','specialTeamsResult'\nkickoff_gained_yards = kickoff_data[['gameId','playId','playDescription','playResult','specialTeamsResult']]\n\n# create subdataset about each outcome\nkickoff_gained_touchback_yards = kickoff_gained_yards.loc[kickoff_gained_yards['specialTeamsResult'] == 'Touchback']\nkickoff_gained_return_yards = kickoff_gained_yards.loc[kickoff_gained_yards['specialTeamsResult'] == 'Return']\nkickoff_gained_muffed_yards = kickoff_gained_yards.loc[kickoff_gained_yards['specialTeamsResult'] == 'Muffed']\nkickoff_gained_recovery_yards = kickoff_gained_yards.loc[kickoff_gained_yards['specialTeamsResult'] == 'Kickoff Team Recovery']\nkickoff_gained_outbound_yards = kickoff_gained_yards.loc[kickoff_gained_yards['specialTeamsResult'] == 'Out of Bounds']\nkickoff_gained_fair_yards = kickoff_gained_yards.loc[kickoff_gained_yards['specialTeamsResult'] == 'Fair Catch']\nkickoff_gained_downed_yards = kickoff_gained_yards.loc[kickoff_gained_yards['specialTeamsResult'] == 'Downed']\n\n# create a visualization to show the gained yards per play grouping by different kickoff outcomes\nfig = go.Figure()\n\nfig.add_trace(go.Box(y=kickoff_gained_touchback_yards['playResult'],\n                     marker = dict(color = colors[0]),\n                     pointpos = 0,\n                     line = dict(color = 'rgba(0,0,0,0)'),\n                     fillcolor = 'rgba(0,0,0,0)',\n                     boxpoints='all', \n                     jitter=0.5,                                              \n                     marker_size=2,\n                     name = 'Touchback',\n                     #text = kickoff_gained_touchback_yards['playDescription'].values,\n                     hoverinfo='skip'\n              ))\n\nfig.add_trace(go.Box(y=kickoff_gained_return_yards['playResult'],\n                     marker = dict(color = colors[3]),\n                     pointpos = 0,\n                     line = dict(color = 'rgba(0,0,0,0)'),\n                     fillcolor = 'rgba(0,0,0,0)',\n                     boxpoints='all', \n                     jitter=0.5,                                              \n                     marker_size=2,\n                     name = 'Return',\n                     hoverinfo='skip'\n              ))\n\nfig.add_trace(go.Box(y=kickoff_gained_muffed_yards['playResult'],\n                     marker = dict(color = colors[0]),\n                     pointpos = 0,\n                     line = dict(color = 'rgba(0,0,0,0)'),\n                     fillcolor = 'rgba(0,0,0,0)',\n                     boxpoints='all', \n                     jitter=0.5,                                              \n                     marker_size=2,\n                     name = 'Muffed',\n                     hoverinfo='skip'\n              ))\n\nfig.add_trace(go.Box(y=kickoff_gained_recovery_yards['playResult'],\n                     marker = dict(color = colors[0]),\n                     pointpos = 0,\n                     line = dict(color = 'rgba(0,0,0,0)'),\n                     fillcolor = 'rgba(0,0,0,0)',\n                     boxpoints='all', \n                     jitter=0.5,                                              \n                     marker_size=2,\n                     name = 'Recovery',\n                     hoverinfo='skip'\n              ))\n\nfig.add_trace(go.Box(y=kickoff_gained_outbound_yards['playResult'],\n                     marker = dict(color = colors[0]),\n                     pointpos = 0,\n                     line = dict(color = 'rgba(0,0,0,0)'),\n                     fillcolor = 'rgba(0,0,0,0)',\n                     boxpoints='all', \n                     jitter=0.5,                                              \n                     marker_size=2,\n                     name = 'Out of Bound',\n                     hoverinfo='skip'\n              ))\n\nfig.add_trace(go.Box(y=kickoff_gained_fair_yards['playResult'],\n                     marker = dict(color = colors[0]),\n                     pointpos = 0,\n                     line = dict(color = 'rgba(0,0,0,0)'),\n                     fillcolor = 'rgba(0,0,0,0)',\n                     boxpoints='all', \n                     jitter=0.5,                                              \n                     marker_size=2,\n                     name = 'Fair Catch',\n                     hoverinfo='skip'\n              ))\n\nfig.add_trace(go.Box(y=kickoff_gained_downed_yards['playResult'],\n                     marker = dict(color = colors[0]),\n                     pointpos = 0,\n                     line = dict(color = 'rgba(0,0,0,0)'),\n                     fillcolor = 'rgba(0,0,0,0)',\n                     boxpoints='all', \n                     jitter=0.5,                                              \n                     marker_size=2,\n                     name = 'Downed',\n                     hoverinfo='skip'\n              ))\n\n# add a line where y equals zero \nfig.add_shape(type=\"line\",\n    x0=0, y0=0, x1=6, y1=0,\n    line=dict(\n        color=\"grey\",\n        width=1,\n        dash=\"dashdot\",\n    )\n)\n\n\nfig.update_layout(title_text=\"Gained yards of the kicking team per result <br><sup>Return is the most exicting play for its wide range of scores tha a kickoff team could gain or lose</sup>\",\n                  paper_bgcolor='rgba(0,0,0,0)',\n                  plot_bgcolor='rgba(0,0,0,0)',\n                  font=dict(\n                            family=fonts[0],\n                            size=14\n                  ),\n                  xaxis_tickfont_size=14,\n                  yaxis=dict(\n                          title='Gained yards',\n                          titlefont_size=16,\n                          tickfont_size=14,\n                    ),\n                  hoverlabel=dict(\n                    bgcolor=\"white\",\n                    font_size=14,\n                    font_family=\"Rockwell\"\n                    ),\n                  showlegend=False)\n\n\nfig.show()","metadata":{"_kg_hide-input":true,"jupyter":{"source_hidden":true},"execution":{"iopub.status.busy":"2022-01-05T13:27:48.781153Z","iopub.execute_input":"2022-01-05T13:27:48.781417Z","iopub.status.idle":"2022-01-05T13:27:48.861937Z","shell.execute_reply.started":"2022-01-05T13:27:48.781382Z","shell.execute_reply":"2022-01-05T13:27:48.861263Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"# Kick the ball far, if you want to gain more yards <a id=\"far\"></a>\nKicking the ball far, closing to the end zone will result in a lower yard line once the receiving team possess the ball. And it will require the latter one a longer distance to return the ball. On the other hand, the kicking team players will have much more time and space to tackle the ball.\nFrom the output below, it implies that\n\n* When comparing two kicks differ by one yard in the kick length, the longer kick length on average have **~0.36** units more yards gained than the shorter one.\n* The difference is statistically **significant** (p < 0.05) and it means there is strong evidence that there is a real association between kick length and number of gained yards in this population.\n* The R-squared value is 0.238, indicating that **23.8%** of the variation of number of gained yards can be explained by the kick length.\n\nAnd based on the findings, I'd suggest taking the kick length of the ball as a metric for evaluating how good a kick off is.","metadata":{}},{"cell_type":"code","source":"# relationship between the kickLength and number of gained yards\nmodel = sm.OLS.from_formula(\"playResult ~ kickLength\", data=kickoff_data)\nresult = model.fit()\nresult.summary()","metadata":{"_kg_hide-input":true,"jupyter":{"source_hidden":true},"execution":{"iopub.status.busy":"2022-01-05T13:27:48.863106Z","iopub.execute_input":"2022-01-05T13:27:48.863542Z","iopub.status.idle":"2022-01-05T13:27:48.942609Z","shell.execute_reply.started":"2022-01-05T13:27:48.863503Z","shell.execute_reply":"2022-01-05T13:27:48.941381Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"# Initial analysis - How to kick the ball far <a id=\"initial\"></a>\nThe next question is how to kick a ball long distance, which should be a skill for kickers to handle. Before diving into what the data reveals to us, let's re-cap some physics at first. The whole process of kicking a ball is a transition of energy from the kicker's body to the ball. The energy exists due to the motion of an object is known as **Kinetic Energy**. Here is its formula ([4](https://en.wikipedia.org/wiki/Kinetic_energy)) -\n> **KE = 1/2 * m$v^2$**\n\ninidicating that, given the mass of the kicker's body is unchanged, **the higher the velocity of the kicking leg, the more energy it will be generated and transmitted to the ball**, which contributes to a longer kick length. \n\nBased on the knowledge above, here we come up with two assumptions -\n* The part of the body contacting the ball is the kicker's foot, so ultimately, **is the speed of the kicking foot a useful predictor for kick length?** \n* **Does knowing the body orientation makes a difference in terms of the kicking length?** - Assuming since much more angle pivoted of the body will absorb more energy from transmitting to the ball\n\n\nBy using multiple linear regression, we can see from the output that \n\n* The **kicker_s_50**, i.e. the median of the kicker's speed, which we use as a proxy of the foot speed in this case, is positively correlated with the kick length. It indicates that comparing two kicks that the ball was being kicked to the same direction, the one kicker has a faster speed by 1 yard per 0.1 second, its estimated kick length is **~7.1** units longer than the slower one.\n\n* The **direction of the ball being kicked to** is also postively associated with the kick length. The result implies that comparing three kicks with the same kicker's foot speed, if the ball was being kicked to the right, the kick length is estimated to be shorter by **~6.8** units, **~5.2** units than to the center or to the left, respectively.\n\n* The model fit above has an R-squared value as 0.257, which indicates that **25.7%** of the variation of the kick length can be explained by the two independent variables, kicking foot speed and direction, together.","metadata":{}},{"cell_type":"code","source":"# get tracking data of the kicker and the ball\n# load tracking data\ntracking_datafile = [\n                    \"/kaggle/input/nfl-big-data-bowl-2022/tracking2018.csv\",\n                    \"/kaggle/input/nfl-big-data-bowl-2022/tracking2019.csv\",\n                    \"/kaggle/input/nfl-big-data-bowl-2022/tracking2020.csv\"\n                    ]\nkickoff_tracking_dis_dir = []\n\n# merge kickoff tracking data with kickoff play data\nfor file in tracking_datafile:\n    data = pd.read_csv(file)\n    # join kickoff_play and tracking_data on gameId and playId\n    kickoff_tracking_data = pd.merge(\n    kickoff_play,\n    data,\n    how=\"inner\",\n    left_on=[\"gameId\",\"playId\"],\n    right_on=[\"gameId\",\"playId\"],\n    sort=True,\n    suffixes=(\"_x\", \"_y\"),\n    copy=True,\n    indicator=False,\n    validate=None,\n    )\n    # select rows relevent with the route of ball and the kicker\n    kickoff_tracking_data = kickoff_tracking_data.loc[(kickoff_tracking_data['nflId'] == kickoff_tracking_data['kickerId']) |\n                                                     (kickoff_tracking_data['displayName'] == 'football')]\n\n    kickoff_tracking_dis_dir.append(kickoff_tracking_data)\n\n# merge three seasons\nkickoff_tracking_dis_dir_merged = pd.concat(kickoff_tracking_dis_dir)\n\n# Load pff data \npff_data = pd.read_csv(\"/kaggle/input/nfl-big-data-bowl-2022/PFFScoutingData.csv\")\n\n# join tracking data with pff data\nkickoff_tracking_pff_data = pd.merge(\n    kickoff_tracking_dis_dir_merged,\n    pff_data,\n    how=\"inner\",\n    left_on=[\"gameId\",\"playId\"],\n    right_on=[\"gameId\",\"playId\"],\n    sort=True,\n    suffixes=(\"_x\", \"_y\"),\n    copy=True,\n    indicator=False,\n    validate=None,\n    )\n\n# create unique id to identify play\nkickoff_tracking_pff_data['uniqueId'] = kickoff_tracking_pff_data['gameId'].astype(str) + kickoff_tracking_pff_data['playId'].astype(str)\n#kickoff_tracking_pff_data.columns\nkickoff_tracking_pff_data_select = kickoff_tracking_pff_data[['uniqueId','kickerId',\n                                                      'time', 'x', 'y',  'dis', 'o', 's','a',\n                                                       'event', 'displayName']]\n# choose kickoff events\nkickoff_events = ['kickoff',\n                 'onside_kick',\n                 'free_kick',\n                 #'kickoff_play',\n                 'autoevent_kickoff']\n\n# select rows which are kickoff events\nkickoff_tracking_pff_events = kickoff_tracking_pff_data_select.loc[kickoff_tracking_pff_data_select['event'].isin(kickoff_events)]\n# create a list containing uniqueId\nunique_ids_pre_kick = kickoff_tracking_pff_events['uniqueId'].unique()\n\n# select relevant columns\nprekick_dataset = pd.DataFrame(columns=['uniqueId', 'event','kickerId','kicker_x', 'kicker_y','kicker_o',\n                                        'kicker_s_25','kicker_s_50','kicker_s_75','kicker_s_max',\n                                        'kicker_a_25','kicker_a_50','kicker_a_75','kicker_a_max',\n                                        'ball_x','ball_y','kicker_kick_dis','kicker_approach_dis','kicker_last_step'])\nunique_ids_pre_kick = kickoff_tracking_pff_events['uniqueId'].unique()\ncount = 0\n\n# create a new dataset containing aggregated metrics\nfor i in unique_ids_pre_kick:\n    #print(i)\n    dataset = kickoff_tracking_pff_data_select.loc[kickoff_tracking_pff_data_select['uniqueId'] == i]\n    #print(i)\n    time = dataset.loc[(dataset['event'].isin(kickoff_events)) & (dataset['displayName'] == 'football')]['time'].iloc[0]\n    pre_data = dataset.loc[dataset['time'] <=time]\n    event = pre_data.loc[(pre_data['event'].isin(kickoff_events)) & (pre_data['displayName'] != 'football')]['event'].iloc[0]\n    kickerId = pre_data.loc[(pre_data['event'].isin(kickoff_events)) & (pre_data['displayName'] != 'football')]['kickerId'].iloc[0]\n    kicker_x = pre_data.loc[(pre_data['event'].isin(kickoff_events)) & (pre_data['displayName'] != 'football')]['x'].iloc[0]\n    #print(i)\n    kicker_y  = pre_data.loc[(pre_data['event'].isin(kickoff_events)) & (pre_data['displayName'] != 'football')]['y'].iloc[0]\n    kicker_o  = pre_data.loc[(pre_data['event'].isin(kickoff_events)) & (pre_data['displayName'] != 'football')]['o'].iloc[0]\n    kicker_s_25 = pre_data.loc[pre_data['displayName'] != 'football']['s'].quantile(0.25)\n    kicker_s_50 = pre_data.loc[pre_data['displayName'] != 'football']['s'].quantile(0.5)\n    kicker_s_75 = pre_data.loc[pre_data['displayName'] != 'football']['s'].quantile(0.75)\n    kicker_s_max = max(pre_data.loc[pre_data['displayName'] != 'football']['s'])\n    kicker_a_25 = pre_data.loc[pre_data['displayName'] != 'football']['a'].quantile(0.25)\n    kicker_a_50 = pre_data.loc[pre_data['displayName'] != 'football']['a'].quantile(0.5)\n    kicker_a_75 = pre_data.loc[pre_data['displayName'] != 'football']['a'].quantile(0.75)\n    kicker_a_max = max(pre_data.loc[pre_data['displayName'] != 'football']['a'])\n    kicker_last_step  = pre_data.loc[(pre_data['event'].isin(kickoff_events)) & (pre_data['displayName'] != 'football')]['dis'].iloc[0]\n    kicker_approach_dis = sum(pre_data.loc[pre_data['displayName'] != 'football']['dis'])\n    ball_x = pre_data.loc[(pre_data['event'].isin(kickoff_events)) & (pre_data['displayName'] == 'football')]['x'].iloc[0]\n    ball_y = pre_data.loc[(pre_data['event'].isin(kickoff_events)) & (pre_data['displayName'] == 'football')]['y'].iloc[0]\n    kicker_kick_dis = ((kicker_x - ball_x) ** 2 + (kicker_y - ball_y) ** 2) ** 0.5\n    row = [i,event,kickerId,kicker_x,kicker_y,kicker_o,\n           kicker_s_25,kicker_s_50,kicker_s_75,kicker_s_max,\n           kicker_a_25,kicker_a_50,kicker_a_75,kicker_a_max,\n           ball_x,ball_y,kicker_kick_dis,kicker_approach_dis,kicker_last_step]\n    prekick_dataset.loc[count] = row\n    count += 1\n    \nkickoff_pff_info = kickoff_tracking_pff_data[['uniqueId','kickLength','specialTeamsResult','playResult',\n                                        'playDirection','kickDirectionActual']]\nkickoff_pff_info_aggr = kickoff_pff_info.groupby(['uniqueId','kickLength','specialTeamsResult',\n                            'playDirection','kickDirectionActual']).max().reset_index()[['uniqueId','kickLength','specialTeamsResult','playResult',\n                                        'playDirection','kickDirectionActual']]\n\n# merge pff data with kicking data\nkickoff_move_data = pd.merge(\n    kickoff_pff_info_aggr,\n    prekick_dataset,\n    how=\"inner\",\n    left_on=[\"uniqueId\"],\n    right_on=[\"uniqueId\"],\n    sort=True,\n    suffixes=(\"_x\", \"_y\"),\n    copy=True,\n    indicator=False,\n    validate=None,\n    )\n\n#Load player data\nplayer_data = pd.read_csv(\"../input/nfl-big-data-bowl-2022/players.csv\")\n\n# turn player height into cm\nh_ft_in = (player_data.height.str.contains('-'), 'height')\nplayer_data.loc[h_ft_in] = player_data.loc[h_ft_in].str.split('-').str[0].astype(int)*12 \\\n    + player_data.loc[h_ft_in].str.split('-').str[1].astype(int)\nplayer_data['height'] = player_data.height.astype(int) / 39.37\n\n# merge kicking data with player data\nkickoff_move_player_data = pd.merge(\n    kickoff_move_data,\n    player_data,\n    how=\"inner\",\n    left_on=[\"kickerId\"],\n    right_on=[\"nflId\"],\n    sort=True,\n    suffixes=(\"_x\", \"_y\"),\n    copy=True,\n    indicator=False,\n    validate=None,\n    )\n\nkickoff_move_player_data = kickoff_move_player_data[['uniqueId', 'kickLength', 'specialTeamsResult', \n       'playDirection', 'kickDirectionActual', 'event', 'kickerId', 'kicker_x',\n       'kicker_y', 'kicker_o', 'kicker_s_25', 'kicker_s_50', 'kicker_s_75',\n       'kicker_s_max', 'kicker_a_25', 'kicker_a_50', 'kicker_a_75',\n       'kicker_a_max', 'ball_x', 'ball_y', 'kicker_kick_dis',\n       'kicker_approach_dis', 'kicker_last_step','height', 'weight', 'birthDate']]","metadata":{"_kg_hide-input":true,"jupyter":{"source_hidden":true},"execution":{"iopub.status.busy":"2022-01-05T13:27:48.946624Z","iopub.execute_input":"2022-01-05T13:27:48.947405Z","iopub.status.idle":"2022-01-05T13:58:06.002758Z","shell.execute_reply.started":"2022-01-05T13:27:48.947345Z","shell.execute_reply":"2022-01-05T13:58:06.001357Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# sort the categorical values in column kickDirectionActual \nkickoff_move_player_data.kickDirectionActual = pd.Categorical(kickoff_move_player_data.kickDirectionActual, \n                      categories=[\"R\",\"C\",\"L\"],\n                      ordered=True)\n\nmodel = sm.OLS.from_formula(\"kickLength ~ kicker_s_50 + kickDirectionActual\", data=kickoff_move_player_data)\nresult = model.fit()\nresult.summary()","metadata":{"_kg_hide-input":true,"jupyter":{"source_hidden":true},"execution":{"iopub.status.busy":"2022-01-05T13:58:06.004914Z","iopub.execute_input":"2022-01-05T13:58:06.006002Z","iopub.status.idle":"2022-01-05T13:58:06.066980Z","shell.execute_reply.started":"2022-01-05T13:58:06.005945Z","shell.execute_reply":"2022-01-05T13:58:06.065431Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"We also confirmed that the foot speed and direction is nearly uncorrelated (~0.16 is negligible). Thus it is expected that when we add kicking direction to the model, the foot speed coefficient is unaffected.","metadata":{}},{"cell_type":"code","source":"# need to switch the direction from text into numeric variable\nkickoff_move_player_data[\"kickDirectionActual\"] = kickoff_move_player_data.kickDirectionActual.replace({\"R\": 1, \"L\":2,\"C\":3})\n\n# calculate Pearson's correlation\ncorr, _ = pearsonr(kickoff_move_player_data['kicker_s_50'], kickoff_move_player_data['kickDirectionActual'])\nprint('The correlation between foot speed and the kicking direction: %.3f' % corr)","metadata":{"_kg_hide-input":true,"execution":{"iopub.status.busy":"2022-01-05T14:02:30.741905Z","iopub.execute_input":"2022-01-05T14:02:30.742272Z","iopub.status.idle":"2022-01-05T14:02:30.750831Z","shell.execute_reply.started":"2022-01-05T14:02:30.742234Z","shell.execute_reply":"2022-01-05T14:02:30.750107Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"# Variable 1 - Foot speed <a id=\"speed\"></a>\nThe positive relation between the kicking foot speed and the kick length has also been found in Australian Rules football([5](https://acephysed.files.wordpress.com/2015/01/atricle-1-reference-list.pdf)). Inspired by that paper, I was looking at whether there is a strong association between the foot speed and the last step length. I used the data **dis**, the distance traveled from prior time point, in yards (numeric), as a measure for estimating the length of the last step of each kick. And the outcome is promising. We can clearly see that the longer the last step length, the faster speed of the kick's foot. The foot speed has a very high positive correlation (**95.6%**) with the larger last step length before the kick, which usually indicates a larger angle of the pelvis. \n\n> So for the NFL professions and fans, if you see a kicker has a very large step before kicking the ball, it usually means he runs very fast towards the ball and the length of the kick shouldn't be short.","metadata":{}},{"cell_type":"code","source":"# calculate Pearson's correlation\ncorr, _ = pearsonr(kickoff_move_player_data['kicker_s_50'], kickoff_move_player_data['kicker_last_step'])\nprint('The correlation between foot speed and the last step length: %.3f' % corr)","metadata":{"_kg_hide-input":true,"execution":{"iopub.status.busy":"2022-01-05T14:00:28.256577Z","iopub.execute_input":"2022-01-05T14:00:28.257474Z","iopub.status.idle":"2022-01-05T14:00:28.264728Z","shell.execute_reply.started":"2022-01-05T14:00:28.257419Z","shell.execute_reply":"2022-01-05T14:00:28.263821Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"Apart from that, it is also found that a longer approach run can help you accelerate to the high speed before contacting the ball - the correlation between the two is highly positive, **~55%**.","metadata":{}},{"cell_type":"code","source":"# calculate Pearson's correlation\ncorr, _ = pearsonr(kickoff_move_player_data['kicker_s_50'], kickoff_move_player_data['kicker_approach_dis'])\nprint('The correlation between foot speed and the approach distance: %.3f' % corr)","metadata":{"_kg_hide-input":true,"execution":{"iopub.status.busy":"2022-01-05T14:00:55.338850Z","iopub.execute_input":"2022-01-05T14:00:55.339221Z","iopub.status.idle":"2022-01-05T14:00:55.348107Z","shell.execute_reply.started":"2022-01-05T14:00:55.339185Z","shell.execute_reply":"2022-01-05T14:00:55.347026Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"# Variable 2 - Body orientation <a id=\"orientation\"></a>\nAnother interesting fact is the ball kicked to the right direction usually has a shorter length than the one to the left or to the middle. ","metadata":{}},{"cell_type":"code","source":"# merge the pff data with play and tracking data\nkickoff_pff_data = pd.merge(\n    kickoff_data,\n    pff_data,\n    how=\"inner\",\n    left_on=[\"gameId\",\"playId\"],\n    right_on=[\"gameId\",\"playId\"],\n    sort=True,\n    suffixes=(\"_x\", \"_y\"),\n    copy=True,\n    indicator=False,\n    validate=None,\n    )\n\n# select relevant columns\nkickoff_direction_length = kickoff_pff_data[['season','uniqueId','specialTeamsResult','kickLength','kickDirectionActual']]\n# turn the season data type from int to str\nkickoff_direction_length['season'] = kickoff_direction_length['season'].astype(str)\n# sorting the L,C,R values in the kickDirectionActual column\nkickoff_direction_length.kickDirectionActual = pd.Categorical(kickoff_direction_length.kickDirectionActual, \n                      categories=[\"L\",\"C\",\"R\"],\n                      ordered=True)\n\n# replace values into full\nkickoff_direction_length['kickDirectionActual'] = kickoff_direction_length['kickDirectionActual'].replace('C', 'Central')\nkickoff_direction_length['kickDirectionActual'] = kickoff_direction_length['kickDirectionActual'].replace('L', 'Left')\nkickoff_direction_length['kickDirectionActual'] = kickoff_direction_length['kickDirectionActual'].replace('R', 'Right')\n\n# get data on number of plays per direction of a kicking ball in the three seasons\nkickoff_direction_length_aggr = kickoff_direction_length.groupby(['season','kickDirectionActual']).nunique().reset_index()[['season','kickDirectionActual','uniqueId']]\nkickoff_direction_length_aggr['total'] = kickoff_direction_length_aggr.groupby('season')['uniqueId'].transform('sum')\nkickoff_direction_length_aggr['share_total'] = round(kickoff_direction_length_aggr['uniqueId'] * 100 / kickoff_direction_length_aggr['total'],1)\nkickoff_direction_length_median = kickoff_direction_length.groupby(['season','kickDirectionActual']).median().reset_index()[['season','kickDirectionActual','kickLength']]\n\n# visualization of the kick direction data\nfig = make_subplots(rows=2, cols=1,\n                    shared_xaxes=True,\n                    vertical_spacing=0.02)\n\ndirections = kickoff_direction_length_aggr['kickDirectionActual'].unique()\nbox_colors = ['#013369',\n          '#d50a0a',\n         '#0264ce']\ncount = 0\nfor d in directions:\n    dataset_1 = kickoff_direction_length.loc[kickoff_direction_length['kickDirectionActual'] == d]\n    \n    fig.add_trace(go.Box(x=dataset_1['season'], \n                             y=dataset_1['kickLength'],\n                         marker = dict(color = box_colors[count]),\n                        pointpos = 0,\n                        #line = dict(color = 'rgba(0,0,0,0)'),\n                     #fillcolor = 'rgba(0,0,0,0)',\n                     boxpoints='all',\n                     name = d,\n                     hovertemplate =\n                        '<i>Kickoff Length (yrd)</i>: %{y}',\n                         #hoverinfo = 'skip',\n                     jitter=0.5,\n                     showlegend=False,\n                     marker_size=2),\n              row=1, col=1)\n    \n    dataset_2 = kickoff_direction_length_aggr.loc[kickoff_direction_length_aggr['kickDirectionActual'] == d]\n    #dataset_3 = kickoff_direction_length_median.loc[kickoff_direction_length_median['kickDirectionActual'] == d]\n    fig.add_trace(go.Bar(x=dataset_2['season'], \n                         y=dataset_2['share_total'],\n                         marker_color=box_colors[count],\n                         name = d,\n                         text = d,\n                         showlegend=True,\n                         hovertemplate =\n                        '<i>% of Kickoffs</i>: %{y}',\n                    ),\n              row=2, col=1)\n\n    count += 1\n\n\n\nfig.update_layout(title_text=\"Number of kickoffs per direction and kick length scatter plot from 2018 to 2020<br><sup>Most kicks fly in a central direction. </sup>\" \n                  + \"<sup>Kicks to the left is more likely than to the right.</sup>\",\n                  paper_bgcolor='rgba(0,0,0,0)',\n                  plot_bgcolor='rgba(0,0,0,0)',\n                  font=dict(\n                            family=fonts[0],\n                            size=14\n                  ),\n                  xaxis_tickfont_size=14,\n                  yaxis2=dict(\n                          title='% of Kickoffs',\n                          titlefont_size=12,\n                          tickfont_size=12,\n                    ),\n                  yaxis=dict(\n                          title='Length (yrd)',\n                          titlefont_size=12,\n                          tickfont_size=12,\n                    ),\n                  hoverlabel=dict(\n                    bgcolor=\"white\",\n                    font_size=14,\n                    font_family=\"Rockwell\"\n                    ),\n                  #showlegend=False,\n                  bargap=0.05,\n                  bargroupgap=0.05,\n                  boxmode='group',\n                  boxgap = 0.05,\n                  legend_title_text='Ball Direction'\n                  #barmode='stack'\n                 )\n\nfig.show()","metadata":{"_kg_hide-input":true,"jupyter":{"source_hidden":true},"execution":{"iopub.status.busy":"2022-01-05T13:58:06.167063Z","iopub.execute_input":"2022-01-05T13:58:06.167558Z","iopub.status.idle":"2022-01-05T13:58:06.427091Z","shell.execute_reply.started":"2022-01-05T13:58:06.167505Z","shell.execute_reply":"2022-01-05T13:58:06.426170Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"The hypothesis is, based on data from average people, most of the players are right footed ([6](https://www.psychologytoday.com/us/blog/the-asymmetric-brain/202002/5-scientific-facts-about-left-footedness#:~:text=Most%20people%20are%20right%2Dfooted,et%20al.%2C%202020)), and they most likely choose to position at the left back side of the ball before the approach.\n\nThis is because players need to place their support leg, which is most likely the left leg at the left side near the ball ([7](https://ftvs.cuni.cz/FTVS-2332-version1-the_biomechanics_of_kicking_in_soccer_a_review.pdf)). If they come from the right side, the route will be more curved and make the kicking action awkward.","metadata":{}},{"cell_type":"code","source":"# position before approaching to the ball\nkickoff_move_player_data['diff_x'] = kickoff_move_player_data['kicker_x'] - kickoff_move_player_data['ball_x']\nkickoff_move_player_data['diff_y'] = kickoff_move_player_data['kicker_y'] - kickoff_move_player_data['ball_y']\nkickoff_move_player_data = kickoff_move_player_data.loc[(kickoff_move_player_data['diff_x'] <= 1.5) & (kickoff_move_player_data['diff_x'] >= -1.5)]\nkickoff_move_player_data = kickoff_move_player_data.loc[(kickoff_move_player_data['diff_y'] <= 1.5) & (kickoff_move_player_data['diff_y'] >= -1.5)]\n\n# visualization\n# fig = make_subplots(rows=1, cols=2)\n\nfig = go.Figure(data=go.Scatter(x=kickoff_move_player_data.loc[kickoff_move_player_data['playDirection'] == 'left']['diff_x'], \n                                y=kickoff_move_player_data.loc[kickoff_move_player_data['playDirection'] == 'left']['diff_y'], \n                                mode='markers',\n                                name = 'Left',\n                                marker_color = ['#013369','#d50a0a','#0264ce'][0]\n                               ))\n\nfig.add_trace(go.Scatter(x=kickoff_move_player_data.loc[kickoff_move_player_data['playDirection'] == 'right']['diff_x'], \n                                y=kickoff_move_player_data.loc[kickoff_move_player_data['playDirection'] == 'right']['diff_y'], \n                                mode='markers',\n                                name = 'Right',\n                                marker_color = ['#013369','#d50a0a','#0264ce'][1]\n                               ))\n# add the ball\nfig.add_shape(type=\"circle\",\n    xref=\"x\", yref=\"y\",\n    x0=0.02, y0=0.05,\n    x1=-0.02, y1=-0.05,\n    opacity=0.75,\n    fillcolor=\"orange\",\n    line_color=\"orange\",\n)\n\nfig.add_annotation(x=0.01, y=0.015,\n            text=\"Ball\",\n            showarrow=True,\n            arrowhead=1)\n\n# left back side\nfig.add_shape(type=\"circle\",\n    xref=\"x\", yref=\"y\",\n    x0=0.05, y0=0.5,\n    x1=-1.45, y1=1.45,\n    opacity=0.75,\n    #fillcolor=False,\n    line_color=\"orange\",\n)\n\nfig.add_shape(type=\"circle\",\n    xref=\"x\", yref=\"y\",\n    x0=0.05, y0=-0.5,\n    x1=1.5, y1=-1.4,\n    opacity=0.75,\n    #fillcolor=False,\n    line_color=\"orange\",\n)\n\nfig.update_layout(title_text=\"Position of the kicker before approaching to the ball<br><sup>Kickers most likely stand at the left back side of the ball.</sup>\",\n                  paper_bgcolor='rgba(0,0,0,0)',\n                  plot_bgcolor='rgba(0,0,0,0)',\n                  font=dict(\n                            family=fonts[0],\n                            size=14\n                  ),\n                  xaxis_tickfont_size=14,\n                  xaxis=dict(\n                          title='horizontal distance between ball and kicker',\n                          titlefont_size=12,\n                          tickfont_size=12,\n                    ),\n                  yaxis=dict(\n                          title='vertical distance between ball and kicker',\n                          titlefont_size=12,\n                          tickfont_size=12,\n                    ),\n                  hoverlabel=dict(\n                    bgcolor=\"white\",\n                    font_size=14,\n                    font_family=\"Rockwell\"\n                    ),\n                  #showlegend=False,\n                  legend_title_text='Play Direction'\n                  #barmode='stack'\n                 )\n\n\nfig.show()","metadata":{"_kg_hide-input":true,"jupyter":{"source_hidden":true},"execution":{"iopub.status.busy":"2022-01-05T13:58:06.428250Z","iopub.execute_input":"2022-01-05T13:58:06.429407Z","iopub.status.idle":"2022-01-05T13:58:06.531271Z","shell.execute_reply.started":"2022-01-05T13:58:06.429339Z","shell.execute_reply":"2022-01-05T13:58:06.530375Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"In this case, if the player wants to kick the ball to his right, his body needs to be pivoted at a larger angle, which is confirmed by our data as shown below.","metadata":{}},{"cell_type":"code","source":"#kickoff_move_player_data_o\nkickoff_move_player_data_o = kickoff_move_player_data.groupby(['playDirection','kickDirectionActual']).median().reset_index()[['playDirection',\n                                                                                                                              'kickDirectionActual',\n                                                                                                                              'kickLength',\n                                                                                                                              'kicker_o']]\n# replace values into full\nkickoff_move_player_data_o[\"kickDirectionActual\"] = kickoff_move_player_data_o.kickDirectionActual.replace({1:\"R\", 2 : \"L\", 3:\"C\"})\nkickoff_move_player_data_o['kickDirectionActual'] = kickoff_move_player_data_o['kickDirectionActual'].replace('C', 'Central')\nkickoff_move_player_data_o['kickDirectionActual'] = kickoff_move_player_data_o['kickDirectionActual'].replace('L', 'Left')\nkickoff_move_player_data_o['kickDirectionActual'] = kickoff_move_player_data_o['kickDirectionActual'].replace('R', 'Right')\n\n# get the difference in degrees between left and central; left and right\nkickoff_move_player_data_o_pivoted = pd.pivot_table(kickoff_move_player_data_o, values = 'kicker_o', index=['playDirection'], columns = 'kickDirectionActual').reset_index()\nkickoff_move_player_data_o_pivoted['Right_Central'] = round(kickoff_move_player_data_o_pivoted['Right'] - kickoff_move_player_data_o_pivoted['Central'],1)\nkickoff_move_player_data_o_pivoted['Right_Left'] = round(kickoff_move_player_data_o_pivoted['Right'] - kickoff_move_player_data_o_pivoted['Left'],1)\n\nleft_right_to_central = kickoff_move_player_data_o_pivoted['Right_Central'][0]\nleft_right_to_left = kickoff_move_player_data_o_pivoted['Right_Left'][0]\nright_right_to_central = kickoff_move_player_data_o_pivoted['Right_Central'][1]\nright_right_to_left = kickoff_move_player_data_o_pivoted['Right_Left'][1]\n\nprint(\"Kicking the ball to the right side of the field needs to orient the kicker's body by \")\nprint(\"    - \" + str(left_right_to_central) + \" more degrees than kicking to the central, and \" + str(left_right_to_left) + \" more degrees than kicking to the left, if the play direction is to the left.\")\nprint(\"    - \" + str(right_right_to_central) + \" more degrees than kicking to the central, and \" + str(right_right_to_left) + \" more degrees than kicking to the left, if the play direction is to the right.\")\n","metadata":{"_kg_hide-input":true,"jupyter":{"source_hidden":true},"execution":{"iopub.status.busy":"2022-01-05T13:58:06.532559Z","iopub.execute_input":"2022-01-05T13:58:06.533029Z","iopub.status.idle":"2022-01-05T13:58:06.574107Z","shell.execute_reply.started":"2022-01-05T13:58:06.532994Z","shell.execute_reply":"2022-01-05T13:58:06.572931Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"This larger angle will absorb some extent of energy from the body that makes the energy transmitting to the ball become less.","metadata":{}},{"cell_type":"markdown","source":"# Conclusion and further research <a id=\"conclusion\"></a>\n\n* This notebook suggests taking the kick length as a valid metric for evaluating performance of the kicking team in a kickoff special game play. It also reveals from data, that two potential drivers to a long kick length. Those are \n    * foot speed\n    * kicking direction\n* Adding new measures of the tracking could benefit the research on a more granuarly level. To be more specific, the current competition don't provide data on movement of kickers' toe, knee, hip, support leg, shoulder and foot.","metadata":{}},{"cell_type":"markdown","source":"# Reference <a id=\"reference\"></a>\n\n1. [The definition of special teams](https://en.wikipedia.org/wiki/American_football_positions#Special_teams)\n2. [The definition of kickoff play?](https://operations.nfl.com/learn-the-game/nfl-basics/terms-glossary/)\n3. [Another tweak to the kickoff rule promotes more touchbacks](https://profootballtalk.nbcsports.com/2018/07/06/another-tweak-to-the-kickoff-rule-promotes-more-touchbacks/)\n4. [The formula of kinetic energy](https://en.wikipedia.org/wiki/Kinetic_energy)\n5. [Biomechanical considerations of distance kicking in Australian Rules football](https://acephysed.files.wordpress.com/2015/01/atricle-1-reference-list.pdf)\n6. [5 Scientific Facts About Left-Footedness](https://www.psychologytoday.com/us/blog/the-asymmetric-brain/202002/5-scientific-facts-about-left-footedness)\n7. [The biomechanics of kicking in soccer: A review](https://ftvs.cuni.cz/FTVS-2332-version1-the_biomechanics_of_kicking_in_soccer_a_review.pdf)","metadata":{}}]}