{"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":"code","source":"import numpy as np\nimport pandas as pd\n\nimport re\nimport os\nimport datetime\n\nfrom datetime import datetime\nfrom datetime import timedelta\n\nimport seaborn as sns","metadata":{"execution":{"iopub.status.busy":"2023-01-28T05:17:58.122295Z","iopub.execute_input":"2023-01-28T05:17:58.123384Z","iopub.status.idle":"2023-01-28T05:17:58.670103Z","shell.execute_reply.started":"2023-01-28T05:17:58.123280Z","shell.execute_reply":"2023-01-28T05:17:58.668820Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"## The Surrender Index\n\nThis index is based on the beautiful video made by Jon Bois. [Please watch the video here! It is incredible!](https://www.youtube.com/watch?v=F9H9LwGmc-0&t=390s)\n\nSo lets get the specifics! There are 4 factors that one needs to get to calculate the index\n\n1. Field Position: 1 if inside 40 yard. Then increases every 20% for every yard past own 40\n2. First Down distance: 1 if it is 4th and 1, 0.8  if 4th and 2-3.... 4th & 10+ will be 10+\n3. Score differential: 1 if winning, 2 if tied, 3 if losing by 2+ scores, 4 if losing by 1 score\n4. Time remaining: 1 if leading or its before halftime (((0.001t)^3)+1) where t = number of seconds since halftime if losing AND it’s after halftime)","metadata":{}},{"cell_type":"code","source":"df_games = pd.read_csv('/kaggle/input/nfl-big-data-bowl-2022/games.csv')\ndf_plays = pd.read_csv('/kaggle/input/nfl-big-data-bowl-2022/plays.csv')\ndf_plays.info()","metadata":{"execution":{"iopub.status.busy":"2023-01-28T05:17:58.677064Z","iopub.execute_input":"2023-01-28T05:17:58.677399Z","iopub.status.idle":"2023-01-28T05:17:58.802782Z","shell.execute_reply.started":"2023-01-28T05:17:58.677368Z","shell.execute_reply":"2023-01-28T05:17:58.801514Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"df_plays.describe()","metadata":{"execution":{"iopub.status.busy":"2023-01-28T05:17:58.804293Z","iopub.execute_input":"2023-01-28T05:17:58.804728Z","iopub.status.idle":"2023-01-28T05:17:58.876335Z","shell.execute_reply.started":"2023-01-28T05:17:58.804691Z","shell.execute_reply":"2023-01-28T05:17:58.875152Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"## Preprocess\n\nWe should get the punts. For simplicity's sake, I have removed the punts that did not happen on 4th down (in case there is any and also the surrender index is not clear if we should handle that case)","metadata":{}},{"cell_type":"code","source":"df_punt = df_plays[df_plays['specialTeamsPlayType'] == 'Punt']\ndf_punt = df_punt[df_punt['down'] == 4]\ndf_punt.head()","metadata":{"execution":{"iopub.status.busy":"2023-01-28T05:17:58.879112Z","iopub.execute_input":"2023-01-28T05:17:58.879448Z","iopub.status.idle":"2023-01-28T05:17:58.912346Z","shell.execute_reply.started":"2023-01-28T05:17:58.879418Z","shell.execute_reply":"2023-01-28T05:17:58.911478Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"temp = df_punt.copy()","metadata":{"execution":{"iopub.status.busy":"2023-01-28T05:17:58.913576Z","iopub.execute_input":"2023-01-28T05:17:58.913878Z","iopub.status.idle":"2023-01-28T05:17:58.918397Z","shell.execute_reply.started":"2023-01-28T05:17:58.913851Z","shell.execute_reply":"2023-01-28T05:17:58.917596Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"The yards are max 50 which means it is in form own 40 or opposing 30. I converted that to a number between 0 and 120 so we have the real yard number","metadata":{}},{"cell_type":"code","source":"def get_real_yard_number(row):\n    if row['possessionTeam'] == row['yardlineSide']:\n        return row['yardlineNumber']\n    return 50 + (50 - row['yardlineNumber'])\ntemp['absolute_yard'] = temp.apply(get_real_yard_number, axis=1)","metadata":{"execution":{"iopub.status.busy":"2023-01-28T05:17:58.920377Z","iopub.execute_input":"2023-01-28T05:17:58.920872Z","iopub.status.idle":"2023-01-28T05:17:59.053043Z","shell.execute_reply.started":"2023-01-28T05:17:58.920797Z","shell.execute_reply":"2023-01-28T05:17:59.052063Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"## Surrender Index\n\nNow we have to calculate the surrender index. I have added the helper methods below. This is so that we can have the apply function for the dataframe. Also to keep the code organized.","metadata":{}},{"cell_type":"code","source":"def calc_field_pos(val):\n    if val <= 40:\n        return 1\n    elif val > 40 and val < 50:\n        return pow(1.1, (val - 40))\n    elif val == 50:\n        return pow(1.1, 10)\n    return pow(1.2, (val - 50)) * (pow(1.1, 10))\n\ndef yard_discount(yard):\n    if yard >= 10:\n        return 0.2\n    elif yard >= 7:\n        return 0.4\n    elif yard >= 4:\n        return 0.6\n    elif yard >= 2:\n        return 0.8\n    else:\n        return 1.0\n\ndef score_of_game(game_score):\n    if game_score > 0:\n        return 1.\n    if game_score == 0:\n        return 2.\n    if game_score < -8.:\n        return 3.\n    return 4.\n\ndef secs_elapsed_after_halftime(q, t):\n    time_str = t\n    time_object = datetime.strptime(time_str, '%M:%S:%f').time()\n    qt = timedelta(minutes=time_object.minute, seconds=time_object.second)\n    timeElapsed = (900 * (q - 1)) + (900 - qt.total_seconds())\n    return max(0, timeElapsed - 1800)\n\ndef game_clock(quarter, clock, game_score):\n    x = secs_elapsed_after_halftime(quarter, clock)\n    if x > 0 and game_score < 1:\n        return ((x * 0.001) ** 3.) + 1.\n    return 1.\n\ndef calc_surrender_index(yards, quarter, yard_to_go, clock, home, away, game_id, possessionTeam):\n    if game_id in df_games['gameId'].values:\n        game_infos = df_games[df_games['gameId'] == game_id]\n        home_team = game_infos['homeTeamAbbr']\n        away_team = game_infos['visitorTeamAbbr']\n        score = 0\n        if possessionTeam in home_team.values:\n            score = home - away\n        else:\n            score = away - home\n        return calc_field_pos(yards) * yard_discount(yards) * score_of_game(score) * game_clock(quarter, clock, score)\n    return 0\n\ndef process_row(row):\n    return calc_surrender_index(row['absolute_yard'], row['quarter'], row['yardsToGo'], \n                                row['gameClock'], row['preSnapHomeScore'], row['preSnapVisitorScore'], \n                                row['gameId'], row['possessionTeam'])","metadata":{"execution":{"iopub.status.busy":"2023-01-28T05:17:59.055102Z","iopub.execute_input":"2023-01-28T05:17:59.055585Z","iopub.status.idle":"2023-01-28T05:17:59.072692Z","shell.execute_reply.started":"2023-01-28T05:17:59.055538Z","shell.execute_reply":"2023-01-28T05:17:59.071444Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"### Testing\n\nWe can see if the functions worked. If you take a look at the video you can see the chart with the yard line numbers and how the penalty increases. Below is the graph that visualizes that. ","metadata":{}},{"cell_type":"code","source":"yard_lines = np.arange(121)\ntest_data = pd.DataFrame(data=yard_lines, columns = ['yy'])\ntest_data['field_pos'] = test_data.apply(lambda x: calc_field_pos(x['yy']), axis=1)\nsns.lineplot(data=test_data, x='yy', y='field_pos')","metadata":{"execution":{"iopub.status.busy":"2023-01-28T05:17:59.074009Z","iopub.execute_input":"2023-01-28T05:17:59.074348Z","iopub.status.idle":"2023-01-28T05:17:59.331468Z","shell.execute_reply.started":"2023-01-28T05:17:59.074318Z","shell.execute_reply":"2023-01-28T05:17:59.330354Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"df_final = temp.copy()\ndf_final['surrender_index'] = df_final.apply(lambda x: process_row(x), axis=1)\ndf_final.describe()","metadata":{"execution":{"iopub.status.busy":"2023-01-28T05:17:59.332920Z","iopub.execute_input":"2023-01-28T05:17:59.333258Z","iopub.status.idle":"2023-01-28T05:18:03.031343Z","shell.execute_reply.started":"2023-01-28T05:17:59.333228Z","shell.execute_reply":"2023-01-28T05:18:03.030173Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"sns.set(rc={'figure.figsize':(25.7,8.27)})\nsns.scatterplot(data=df_final, x=\"gameId\", y=\"surrender_index\")","metadata":{"execution":{"iopub.status.busy":"2023-01-28T05:18:03.033174Z","iopub.execute_input":"2023-01-28T05:18:03.033657Z","iopub.status.idle":"2023-01-28T05:18:03.447806Z","shell.execute_reply.started":"2023-01-28T05:18:03.033613Z","shell.execute_reply":"2023-01-28T05:18:03.446512Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"## Conclusion\n\nI hope this helps you in case you want to use it. I think surrender index is an interesting stat that the teams would be intereseted in using. Or for fans to decide if they should be angry at a punt attempt or not. \n\n## Resources\n\n1. https://github.com/andrew-shackelford/Surrender-Index\n2. https://surrender-index.glitch.me/\n3. https://www.youtube.com/watch?v=F9H9LwGmc-0\n4. https://www.buckys5thquarter.com/2019/5/28/18237355/wisconsin-football-punting-paul-chryst-anthony-lotti-chart-party","metadata":{}}]}