{"cells":[{"metadata":{"_uuid":"9a5742a43f0eccde568a85a12e3e957f6d77abbc"},"cell_type":"markdown","source":"## Objective:\n- Preprocess 'PlayDescription' from the play information dataset to create subsets of data that will be used in other notebooks for analysis."},{"metadata":{"trusted":true,"_uuid":"beff664128f3948226eb00d26cbac1ca0759c2c9"},"cell_type":"code","source":"import pandas as pd\nimport numpy as np\nimport seaborn as sns\nimport matplotlib.pyplot as plt\nimport matplotlib.patches as mpatches\nimport re","execution_count":null,"outputs":[]},{"metadata":{"_uuid":"15c8ce0922c24764e8a6d97d48860b9498952d95"},"cell_type":"markdown","source":"# PLAY INFORMATION\n- <b>Play Information</b>: Play level data that describes the type of play, possession team, score and a brief narrative of each play. Plays are uniquely identified using a its PlayID along with the corresponding GameKey. \n- All plays are punts (just check counts of 'Play_Type')\n- I'll look specifically at <b>'PlayDescription'</b> to get a rough idea of how a play panned out and use that data to create one-hot encodings of types of plays (touchback, punt return, blocked kick, etc) and miscellaneous attributes of a play (fumble, muffed, etc). This will help to understand how many plays could be deemed 'interesting' (exciting, action after the catch, a blocked kick) and 'uninteresting' (out of bounds kicks, touchbacks, fair catches, etc.). This labeling is subjective and is used later to place value on the result of a punt both to a team and its fanbase."},{"metadata":{"trusted":true,"_uuid":"256827572049568df6e9cb017cdfd137f03108ca"},"cell_type":"code","source":"play_df = pd.read_csv('../input/play_information.csv')\nprint(play_df.shape)\nplay_df.head(1)","execution_count":null,"outputs":[]},{"metadata":{"trusted":true,"_uuid":"c15311c67802bca4e5af52db670555baa5c25ff2"},"cell_type":"code","source":"# HOW MANY GAMES HAD NO PUNTS AND WHICH GAMES\nstuff = []\n# Collect all game id's in punt data\nfor element in play_df['GameKey']:\n    stuff.append(element)\nprint('Number of games without a punt:', 666 - len(set(stuff)))\n\nfor element in [i for i in range(1, 667)]:\n    if element not in set(stuff):\n        print('Game', element, 'had no punts')","execution_count":null,"outputs":[]},{"metadata":{"_uuid":"0a41cf4aa76a9674ded50d88d946fa320795f4a4"},"cell_type":"markdown","source":"- Game 1: 'Hall of Fame Game' was cancelled due to weather\n- Game 333: Probowl game\n    - If you search the game and find the box-score, there were 3 punts\n- Game 399: Was cancelled due to Hurricane Harvey :(\n- Game 666: Probowl game\n    - If you search the game and find the box-score, there were 8 punts"},{"metadata":{"_uuid":"af1d39b1caeb65b0189283df6d1a1215219e7201"},"cell_type":"markdown","source":"- Example Play Descriptions for one-hot-encodings:\n    - <b>Interesting outcomes</b>:\n        - <b>Returned Punt</b>: B.Nortman punts 40 yards to BUF 23, Center-C.Holba. B.Tate to BUF 34 for 11 yards (D.Payne).\n        - <b>Muffed catch</b>: S.Waters punts 36 yards to BLT 15, Center-J.Jansen. K.Clay MUFFS catch, RECOVERED by CAR-F.Whittaker at BLT 12. F.Whittaker to BLT 12 for no gain (K.Clay).\n        - <b>Blocked Punt</b>: B.Wing punt is BLOCKED by B.Carter, Center-Z.DeOssie, recovered by NYG-J.Currie at NYG 15. J.Currie to NYG 15 for no gain (J.Burris).\n        - <b>Fumbles</b>: M.Darr punts 42 yards to TEN 14, Center-J.Denney. K.Reed to TEN 21 for 7 yards (Dan.Thomas). FUMBLES (Dan.Thomas), RECOVERED by MIA-J.Denney at TEN 23. J.Denney to TEN 23 for no gain (K.Byard).\n        - <b>Touchdown</b>: J.Locke punts 61 yards to CIN 20, Center-K.McDermott. A.Erickson for 80 yards, TOUCHDOWN.\n        - <b>Fake Punt</b>: P.McAfee pass deep right to E.Swoope to PIT 8 for 35 yards (J.Gilbert).\n            - Passing: P.McAfee pass deep right to E.Swoope to PIT 8 for 35 yards (J.Gilbert).\n            - Running: C.Jones left end to PHI 43 for 30 yards (D.Sproles). Fake punt run around left end.\n                - Lots of variations in descriptions for these bad boys\n    - Uninteresting outcomes:\n        - <b>Fair Catch</b>: J.Locke punts 47 yards to GB 10, Center-K.McDermott, fair catch by M.Hyde.\n        - <b>Downed Punt</b>: J.Locke punts 50 yards to GB 9, Center-K.McDermott, downed by MIN-J.Kearse.\n            - This is a play where the punting team controls the ball before any receiving team player after the ball has been punted\n        - <b>Touchbacks</b>: J.Hekker punts 50 yards to end zone, Center-J.Overbaugh, Touchback.\n        - <b>Out of Bounds Punt</b>: J.Schum punts 35 yards to MIN 34, Center-B.Goode, out of bounds.\n        - <b>Dead Ball</b>: B.Nortman punts 51 yards to BUF 34, Center-C.Holba. B.Tate, dead ball declared at BUF 34 for no gain.\n        - <b>No Play</b>: (:04) (Punt formation) PENALTY on ATL-M.Bosher, Delay of Game, 5 yards, enforced at ATL 49 - No Play.\n            - Some 'No Play' or '(Punt formation) Penalty' descriptions vary where a punt was executed and a penalty occurred that would negate the play, such that the punt is reattempted\n            - Such penalties include: False Start, Illegal Substitution, Delay of Game, Illegal Formation, Neutral Zone Infraction, Player Out of Bounds on Punt, Defensive 12 On-field, Ineligible Downfield Kick, Illegal Shift, Unnecessary Roughness, Roughing the Kicker, Defensive Offside, Ineligible Downfield Kick, Offensive Holding\n\n- Note: a play may have more than one of the above classifications."},{"metadata":{"trusted":true,"_uuid":"60ce0818d968964d8ab298ca8db446317d579abc"},"cell_type":"code","source":"# Create condensed version of play data\nkeeper_columns = ['GameKey', 'PlayID', 'PlayDescription', 'Poss_Team', 'YardLine']\ncondensed_play_df = play_df[keeper_columns].copy()","execution_count":null,"outputs":[]},{"metadata":{"trusted":true,"_uuid":"7da0f8f9aef04c0788b199aa0578a0e001817a88"},"cell_type":"code","source":"def find_that_play_word(keyword, df):\n    \"\"\"Help to find keywords\"\"\"\n    df[keyword] = 0\n    count = 0\n    for i, description in enumerate(df['PlayDescription']):\n        game_key = df.loc[i, 'GameKey']\n        play_id = df.loc[i, 'PlayID']\n        # Find keyword in lowercased string of play description\n        if description.lower().find(keyword) != -1:\n#             print('Keyword', keyword, 'found for (game, play):', '(' + str(game_key) + ',' + str(play_id) + ')')\n#             print('Play description:', description)\n#             print('---')\n                \n            # One-hot encode with keyword\n            df.loc[i, keyword] = 1\n            count += 1\n\n    print('# of', keyword, 'occuring on a punt play:', count)","execution_count":null,"outputs":[]},{"metadata":{"_uuid":"121318cbed296ca31fcb43eecebd673c55738b1b"},"cell_type":"markdown","source":"The coice of strings to parse for were determined based off reading through the Play Descriptions. There are probably cases where I'm still making poor assumptions, but I'll have to live with it.\n\n### Uninteresting Outcomes"},{"metadata":{"trusted":true,"_uuid":"fedbc41f736d8970cbc6fabdbf4678ad2d0a17d0"},"cell_type":"code","source":"find_that_play_word('fair catch', condensed_play_df)\nfind_that_play_word('touchback', condensed_play_df)\nfind_that_play_word('downed', condensed_play_df)\nfind_that_play_word(', out of bounds', condensed_play_df)\nfind_that_play_word('dead ball', condensed_play_df)\nfind_that_play_word('no play', condensed_play_df)\nfind_that_play_word('(punt formation) penalty on', condensed_play_df) # Picks up additional 'no play' type punts","execution_count":null,"outputs":[]},{"metadata":{"_uuid":"5944b7f25ece861f4f85e29417d7947c30e09cd6"},"cell_type":"markdown","source":"- Some of these counts may overlap, but won't matter for the processing"},{"metadata":{"trusted":true,"_uuid":"22f21f9b21358fa81fd0ac0175255bd9ed19b021"},"cell_type":"code","source":"# Reduce play_df even further \nwhere_condition = (\n    (condensed_play_df['fair catch'] == 1) |\n    (condensed_play_df['touchback'] == 1) |\n    (condensed_play_df['downed'] == 1) |\n    (condensed_play_df[', out of bounds'] == 1) |\n    (condensed_play_df['dead ball'] == 1) |\n    (condensed_play_df['no play'] == 1) |\n    (condensed_play_df['(punt formation) penalty on'] == 1))\ninteresting_plays_df = condensed_play_df[~where_condition].reset_index(drop=True)\n\nprint('There are now', len(interesting_plays_df), '\"interesting plays\" from', len(condensed_play_df), 'punt plays')\nprint('Proportion of interesting punts:', len(interesting_plays_df)/len(condensed_play_df))\ninteresting_plays_df.head(1)","execution_count":null,"outputs":[]},{"metadata":{"_uuid":"9fe05bb29afe4879f0265e9b5fe6a3e7eaca07c0"},"cell_type":"markdown","source":"- So we can see that around <b>55.4% of punts (3701/6681)</b> result in a play that is 'uninteresting'. Maybe the punt isn't worth the time.\n- The <b>touchback rate was 57.6% for kickoffs in 2016</b>. Just for perspective of 'uninteresting outcomes'.\n    - Reference: http://www.espn.com/nfl/story/_/id/18393780/kickoff-returns-reduced-18-percentage-points-2016-season\n\n\nNow that we have a condensed set of punt plays where something potentially interesting occurred, lets parse for the more interesting than interesting plays on punts (touchdowns, fumbles, blocks, etc.). I'll also create datasets that will be needed for other notebooks."},{"metadata":{"trusted":true,"_uuid":"8a9da467253711079ec5a12aa6aa579bd00beada"},"cell_type":"code","source":"'''I only have this here for reference of what I've filtered by'''\nuninteresting_keywords = ['fair catch', 'touchback.', 'downed', ', out of bounds', 'dead ball', 'no play',\n                         '(punt formation) penalty on']\ninteresting_keywords = ['muffs', 'blocked by','touchdown.', 'fumble', 'ruling', 'fake punt',\n                        'up the middle', 'pass', 'right end', 'left end', 'right guard',\n                        'direct snap', 'touchdown nullified']","execution_count":null,"outputs":[]},{"metadata":{"_uuid":"ce35969b99cca769cce1d313d703b07324cee000"},"cell_type":"markdown","source":"### Interesting outcomes"},{"metadata":{"trusted":true,"_uuid":"c9ffc40a390510059698a6c1eab26ddb2bbf0169"},"cell_type":"code","source":"# 'Interesting outcomes'\nfind_that_play_word('muffs', interesting_plays_df)\nfind_that_play_word('blocked by', interesting_plays_df)\nfind_that_play_word('touchdown.', interesting_plays_df)\nfind_that_play_word('fumble', interesting_plays_df)\nfind_that_play_word('ruling', interesting_plays_df)\nfind_that_play_word('fake punt', interesting_plays_df)\nfind_that_play_word('safety', interesting_plays_df)\nfind_that_play_word('up the middle', interesting_plays_df)\nfind_that_play_word('pass', interesting_plays_df)\nfind_that_play_word('right end', interesting_plays_df)\nfind_that_play_word('left end', interesting_plays_df)\nfind_that_play_word('right guard', interesting_plays_df)\nfind_that_play_word('direct snap', interesting_plays_df)\nfind_that_play_word('touchdown nullified', interesting_plays_df)","execution_count":null,"outputs":[]},{"metadata":{"trusted":true,"_uuid":"25f4cbfa0ad750964d18cd3a359e1f5faa2eda94"},"cell_type":"code","source":"# Create a dataset where plays are currently assumed to be actual punt returns \nwhere_condition = (\n    (interesting_plays_df['muffs'] == 1) |\n    (interesting_plays_df['blocked by'] == 1) |\n    (interesting_plays_df['touchdown.'] == 1) |\n    (interesting_plays_df['fumble'] == 1) |\n    (interesting_plays_df['ruling'] == 1) |\n    (interesting_plays_df['fake punt'] == 1) |\n    (interesting_plays_df['safety'] == 1) |\n    (interesting_plays_df['up the middle'] == 1) |\n    (interesting_plays_df['pass'] == 1) |\n    (interesting_plays_df['right end'] == 1) |\n    (interesting_plays_df['left end'] == 1) |\n    (interesting_plays_df['right guard'] == 1) |\n    (interesting_plays_df['direct snap'] == 1) |\n    (interesting_plays_df['touchdown nullified'] == 1))\nremainder_df = interesting_plays_df[~where_condition].reset_index(drop=True)\n\n# Isolate touchdowns that were from punt returns\nwhere_condition = ((interesting_plays_df['touchdown.'] == 1) &\n                   (interesting_plays_df['blocked by'] == 0) &\n                   (interesting_plays_df['direct snap'] == 0) &\n                   (interesting_plays_df['right guard'] == 0) &\n                   (interesting_plays_df['fumble'] == 0) &\n                   (interesting_plays_df['pass'] == 0))\ntd_df = interesting_plays_df[where_condition].reset_index(drop=True)\n\n# Combine touchdown punt returns and regular punt returns\nremainder_df = pd.concat([remainder_df, td_df], axis=0)\n\n# Drop unnecessary columns\nkeeper_columns = ['GameKey', 'PlayID', 'PlayDescription', 'Poss_Team', 'YardLine']\nremainder_df = remainder_df[keeper_columns]\nremainder_df.reset_index(inplace=True, drop=True)\nprint(remainder_df.shape)\nremainder_df.head()","execution_count":null,"outputs":[]},{"metadata":{"_uuid":"b30656e0461f2bd91b8f40495ad7486677da62d7"},"cell_type":"markdown","source":"**play-punt_retrn.csv and play-fair_catch.csv** are used in notebook: https://www.kaggle.com/jdemeo/preprocessing-ngs"},{"metadata":{"trusted":true,"_uuid":"7ac5b9ebf2218fb7574db078f48a640f3ceb5294"},"cell_type":"code","source":"# Create dataset of punt return plays\nremainder_df.to_csv('play-punt_return.csv', index=False)\n\n# Create dataset of fair catch plays\nwhere_condition = ((condensed_play_df['fair catch'] == 1))\nfc_df = condensed_play_df[where_condition].reset_index(drop=True)\\\n\n# Drop unnecessary columns\nkeeper_columns = ['GameKey', 'PlayID', 'PlayDescription', 'Poss_Team', 'YardLine']\nfc_df = fc_df[keeper_columns]\nfc_df.to_csv('play-fair_catch.csv', index=False)","execution_count":null,"outputs":[]},{"metadata":{"trusted":true,"_uuid":"a983b2951b27d6bce1de6f3f576162394bc0e510"},"cell_type":"code","source":"# Just for reference if you want to filter return plays that have a penalty\nfind_that_play_word('penalty on', remainder_df)","execution_count":null,"outputs":[]},{"metadata":{"_uuid":"f6792cfb689ef8ce811914800f1dc70e9e1a57ad"},"cell_type":"markdown","source":"So now we have a condensed set of punts that result in some return minus the above filtered 'interesting' plays. We'll now look at this set of plays and extract some information from the 'PlayDescription'"},{"metadata":{"_uuid":"1d7adaa0d9ed88fadf7c1f4afc03a06ed25be705"},"cell_type":"markdown","source":"### Play Description Parsing of Punt Return Plays\n- The following work-up/analysis does not adjust for penalties on the play. I know this isn't clean and big returns on punts do have a pretty good chance of a penalty was helping with the success of the return, but I'm only parsing the PlayDescription to get a rough idea of the return amounts and potential value of a return."},{"metadata":{"trusted":true,"_uuid":"9105badd51fa7bea72982ce2c936fbae47c97c06"},"cell_type":"code","source":"'''\nNeed to parse through PlayDescription in order to get return distance of play and distance to touchdown\nPatterns that return two distances for yardage on play are lateral plays\n'''\n\n# Regex for them patterns\npunt_distance_pattern = re.compile(r'punts ((-?)\\d+) yards? to(\\s| \\w+ )((-?)\\d+)')\nyards_gained_pattern = re.compile(r'for ((-?)\\d+) yard')\nno_yards_gained_pattern = re.compile(r'([A-Z]\\w+) ((-?)\\d+) (for no gain)')\n\nremainder_df['punt distance'] = 0\nremainder_df['side ball lands'] = ''\nremainder_df['yardline received'] = 0\nremainder_df['yardage on play'] = 0\n\nfor i, element in enumerate(remainder_df['PlayDescription']):\n\n    punt_distance = punt_distance_pattern.findall(element) # ('Punt distance', '', 'Side Ball Lands', 'Yardline Received')\n    yards_gained = yards_gained_pattern.findall(element)   # ('Yardage on Play', '', )\n    no_gain = no_yards_gained_pattern.findall(element)\n    \n#     print(punt_distance)\n#     print(yards_gained)\n#     print(no_gain)\n    \n    # A play that results in yards gained or lossed\n    if yards_gained != []:\n        remainder_df.loc[i, 'punt distance'] = int(punt_distance[0][0])\n        remainder_df.loc[i, 'side ball lands'] = punt_distance[0][2]\n        remainder_df.loc[i, 'yardline received'] = int(punt_distance[0][3])\n        \n        # A normal return\n        if len(yards_gained) == 1:\n            remainder_df.loc[i, 'yardage on play'] = int(yards_gained[0][0])\n            \n        # For laterals\n        else:\n            remainder_df.loc[i, 'yardage on play'] = int(yards_gained[0][0]) + int(yards_gained[1][0])\n            \n    # A play that resulted in no gain in yards\n    elif no_gain != []:\n        remainder_df.loc[i, 'punt distance'] = int(punt_distance[0][0])\n        remainder_df.loc[i, 'side ball lands'] = punt_distance[0][2]\n        remainder_df.loc[i, 'yardline received'] = int(punt_distance[0][3])\n\n#     print('---')","execution_count":null,"outputs":[]},{"metadata":{"trusted":true,"_uuid":"5cb7b5bf78284eae825001bc3a4d08a9ecdb5c8d"},"cell_type":"code","source":"# Doing some hand processing of specific returns where the yardage gained on return was\n# officially changed (I know not elegant, especially if dataframe indices change overtime)\nculprits = [476, 891, 1062, 1064, 1096, 2193]\nyard_changes = [14, 6, 0, 3, 0, 4]\nfor i, element in enumerate(culprits):\n    remainder_df.loc[element, 'yardage on play'] = yard_changes[i]","execution_count":null,"outputs":[]},{"metadata":{"_uuid":"6797cda0a3e4c476fd898651b77b7b6de6112ff8"},"cell_type":"markdown","source":"We'll calculate distance to a touchdown for each play to create a reward metric for each play. A more proper metric for the value of a punt return should also take into account the current score, time remaining, playoff implications, and return by the home team or not just to name a few factors. I don't do this just to have a simplified model for reward. "},{"metadata":{"trusted":true,"_uuid":"ad1c10b375100bf244e02702c5c68b9cb24bf834"},"cell_type":"code","source":"def calculate_distance_to_td (data_sample):\n    '''Calculate distance needed for touchdown for each play'''\n    # Punts that land on the 50 yard line\n    if data_sample['yardline received'] == 50:\n        distance_to_touchdown = 50\n    \n    # Punting on punting team's side of field\n    elif data_sample['Poss_Team'] == data_sample['YardLine'][:len(data_sample['Poss_Team'])]:\n        # Ball remains on punt team's side of field\n        if data_sample['side ball lands'] == data_sample['YardLine'][:len(data_sample['Poss_Team'])]:\n            distance_to_touchdown = data_sample['yardline received']\n        # Ball is punted to return team's side of field\n        else:\n            distance_to_touchdown = (50 - data_sample['yardline received']) + 50\n            \n    # Punting on opponents side of field\n    else:\n        distance_to_touchdown = (50 - data_sample['yardline received']) + 50\n    return distance_to_touchdown","execution_count":null,"outputs":[]},{"metadata":{"trusted":true,"_uuid":"91f16a171258bf87d29780dcd61e5a199a3b04a6"},"cell_type":"code","source":"# Calculate the value of a punt return based solely on the proportion of yardage gained on the return\n# Relative to how many yards are needed to score a touchdown from where the punt initially lands\nremainder_df['reward'] = 0\nfor i in range(len(remainder_df)):\n    yards_on_return = remainder_df.loc[i, 'yardage on play']\n    distance_to_touchdown = calculate_distance_to_td(remainder_df.iloc[i, :])\n    remainder_df.loc[i, 'reward'] = yards_on_return / distance_to_touchdown\n#     print('Value of return:', yards_on_return / distance_to_touchdown)\n\nremainder_df.head()","execution_count":null,"outputs":[]},{"metadata":{"trusted":true,"_uuid":"d3fc5c5ac8eea29e237a73969ba434151f0835d8"},"cell_type":"code","source":"# Cleanup\nkeepers = ['GameKey', 'PlayID', 'PlayDescription', 'yardage on play', 'reward']\nremainder_df = remainder_df[keepers]\n\n# Create dataset for external usage\nremainder_df.to_csv('play-punt_return-yardage.csv', index=False)","execution_count":null,"outputs":[]},{"metadata":{"_uuid":"c38eb36a399fa1efe24ba2d344eed6f1d748dca1"},"cell_type":"markdown","source":"**play-punt_return-yardage.csv** used in notebook: https://www.kaggle.com/jdemeo/analysis-punt-returns"},{"metadata":{"_uuid":"32f713cda3e4e9cfd9e26fe5afdd51863fe1ad7f"},"cell_type":"markdown","source":"### PLAY PLAYER ROLE DATA\n- <b>Play Player Role Data</b>: Play and player level data that specifies a punt specific player role. This dataset will specify each player that played in each play. A player’s role in a play is uniquely defined by the Gamekey PlayID and GSISID."},{"metadata":{"trusted":true,"_uuid":"efdad7b35e527a2b4b425ff4a82255c049ec3121"},"cell_type":"code","source":"play_player_role_df = pd.read_csv('../input/play_player_role_data.csv')\nprint(play_player_role_df.shape)\nplay_player_role_df.tail(2)","execution_count":null,"outputs":[]},{"metadata":{"trusted":true,"_uuid":"76935b1dd8891ebc3126474f1b0d6013bee005e2"},"cell_type":"code","source":"# How many unique plays in play_player_role dataset?\nprint('# of unique plays according to play_player_role dataset:',\n      len(play_player_role_df.groupby(['GameKey','PlayID']).size().reset_index().rename(columns={0:'count'})))\n\n# How many roles are there?\nprint('# of roles in dataset:', len(play_player_role_df['Role'].value_counts()))","execution_count":null,"outputs":[]},{"metadata":{"_uuid":"f51dc993ac826a8afebd70dc07a8a008e2ecec51"},"cell_type":"markdown","source":"- NGS dataset has 6666 punt plays\n- Play Information dataset has 6681 punt plays\n- **I wanted to note that there is some missing data between the datasets**"},{"metadata":{"_uuid":"633a07a7d1f9864a1c210fefb1793fae3b528f56"},"cell_type":"markdown","source":"# Links to other notebooks:\n- Concussion play analysis with proposed rule changes: https://www.kaggle.com/jdemeo/analysis-concussions\n- Analysis of uncalled penalties: https://www.kaggle.com/jdemeo/analysis-uncalled-penalties\n- Analysis of punt returns: https://www.kaggle.com/jdemeo/analysis-punt-returns\n- Analysis of fair catches: https://www.kaggle.com/jdemeo/analysis-fair-catches\n- Preprocessing of NGS data for the above notebooks: https://www.kaggle.com/jdemeo/preprocessing-ngs"},{"metadata":{"trusted":false,"_uuid":"35b25a237face3d401159ea9a042de5d3eef2cea"},"cell_type":"code","source":"","execution_count":null,"outputs":[]}],"metadata":{"kernelspec":{"display_name":"Python 3","language":"python","name":"python3"},"language_info":{"codemirror_mode":{"name":"ipython","version":3},"file_extension":".py","mimetype":"text/x-python","name":"python","nbconvert_exporter":"python","pygments_lexer":"ipython3","version":"3.7.1"}},"nbformat":4,"nbformat_minor":1}